Skip to content

Commit 9abc84b

Browse files
committed
feature: release
1 parent 9a91aae commit 9abc84b

File tree

9 files changed

+221
-59
lines changed

9 files changed

+221
-59
lines changed

src/torusdk/_common.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99

1010
from torusdk.types.types import Ss58Address
1111

12-
IPFS_REGEX = re.compile(r"^Qm[1-9A-HJ-NP-Za-km-z]{44}$")
12+
IPFS_REGEX = re.compile(r"^Qm[1-9A-HJ-NP-Za-km-z]{44}$|bafk[1-7a-z]{52}$/i")
13+
CID_REGEX = re.compile(
14+
r"^(?:ipfs://)?(?P<cid>Qm[1-9A-HJ-NP-Za-km-z]{44,}|b[A-Za-z2-7]{58,}|B[A-Z2-7]{58,}|z[1-9A-HJ-NP-Za-km-z]{48,}|F[0-9A-F]{50,})(?:/[\d\w.]+)*$"
15+
)
1316
SS58_FORMAT = 42
1417

1518

src/torusdk/cli/_common.py

Lines changed: 70 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from torustrateinterface import Keypair
1414
from typer import Context
1515

16-
from torusdk._common import IPFS_REGEX, TorusSettings, get_node_url
16+
from torusdk._common import CID_REGEX, TorusSettings, get_node_url
1717
from torusdk.balance import dict_from_nano, from_rems, to_rems
1818
from torusdk.client import TorusClient
1919
from torusdk.errors import InvalidPasswordError, PasswordNotProvidedError
@@ -54,10 +54,10 @@ def merge_models(model_a: T, model_b: BaseModel) -> T:
5454

5555

5656
def extract_cid(value: str):
57-
cid_hash = re.match(IPFS_REGEX, value)
57+
cid_hash = re.match(CID_REGEX, value)
5858
if not cid_hash:
5959
raise typer.BadParameter(f"CID provided is invalid: {value}")
60-
return cid_hash.group()
60+
return cid_hash.group("cid")
6161

6262

6363
def input_to_rems(value: float | None):
@@ -277,23 +277,86 @@ def print_table_from_plain_dict(
277277
console.print(table)
278278

279279

280+
def render_pydantic_subtable(value: BaseModel | dict[Any, Any]) -> Table:
281+
"""
282+
Renders a subtable for a nested Pydantic object or dictionary.
283+
284+
Args:
285+
value: A nested Pydantic object or dictionary.
286+
287+
Returns:
288+
A rich Table object representing the subtable.
289+
"""
290+
subtable = Table(
291+
show_header=False,
292+
padding=(0, 0, 0, 0),
293+
border_style="bright_black",
294+
)
295+
if isinstance(value, BaseModel):
296+
for subfield_name, _ in value.model_fields.items():
297+
subfield_value = getattr(value, subfield_name)
298+
subtable.add_row(f"{subfield_name}: {subfield_value}")
299+
else:
300+
for subfield_name, subfield_value in value.items(): # type: ignore
301+
subtable.add_row(f"{subfield_name}: {subfield_value}")
302+
return subtable
303+
304+
305+
def render_single_pydantic_object(
306+
obj: BaseModel, console: Console, title: str = ""
307+
) -> None:
308+
"""
309+
Renders a rich table from a single Pydantic object.
310+
311+
Args:
312+
obj: A single Pydantic object.
313+
console: The rich Console object.
314+
title: Optional title for the table.
315+
"""
316+
table = Table(
317+
title=title,
318+
show_header=True,
319+
header_style="bold magenta",
320+
title_style="bold magenta",
321+
)
322+
323+
table.add_column("Field", style="white", vertical="middle")
324+
table.add_column("Value", style="white", vertical="middle")
325+
326+
for field_name, _ in obj.model_fields.items():
327+
value = getattr(obj, field_name)
328+
if isinstance(value, BaseModel):
329+
subtable = render_pydantic_subtable(value)
330+
table.add_row(field_name, subtable)
331+
else:
332+
table.add_row(field_name, str(value))
333+
334+
console.print(table)
335+
console.print("\n")
336+
337+
280338
def render_pydantic_table(
281-
objects: list[T],
339+
objects: T | list[T],
282340
console: Console,
283341
title: str = "",
284342
ignored_columns: list[str] = [],
285343
) -> None:
286344
"""
287-
Renders a rich table from a list of Pydantic objects.
345+
Renders a rich table from a list of Pydantic objects or a single Pydantic object.
288346
289347
Args:
290-
objects: A list of Pydantic objects.
348+
objects: A list of Pydantic objects or a single Pydantic object.
291349
console: The rich Console object.
292350
title: Optional title for the table.
351+
ignored_columns: List of column names to ignore.
293352
"""
294353
if not objects:
295354
return
296355

356+
if isinstance(objects, BaseModel):
357+
render_single_pydantic_object(objects, console, title)
358+
return
359+
297360
table = Table(
298361
title=title,
299362
show_header=True,
@@ -313,14 +376,7 @@ def render_pydantic_table(
313376
continue
314377
value = getattr(obj, field_name)
315378
if isinstance(value, BaseModel):
316-
subtable = Table(
317-
show_header=False,
318-
padding=(0, 0, 0, 0),
319-
border_style="bright_black",
320-
)
321-
for subfield_name, _ in value.model_fields.items():
322-
subfield_value = getattr(value, subfield_name)
323-
subtable.add_row(f"{subfield_name}: {subfield_value}")
379+
subtable = render_pydantic_subtable(value)
324380
row_data.append(subtable)
325381
else:
326382
row_data.append(str(value))

src/torusdk/cli/agent.py

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
11
from typing import Any, Optional, cast
22

33
import typer
4+
from pydantic import ValidationError
45
from typer import Context
56

67
from torusdk._common import intersection_update
78
from torusdk.balance import from_rems
89
from torusdk.cli._common import (
910
HIDE_FEATURES,
11+
extract_cid,
1012
make_custom_context,
1113
print_module_info,
1214
print_table_from_plain_dict,
1315
render_pydantic_table,
1416
)
1517
from torusdk.errors import ChainTransactionError
1618
from torusdk.misc import get_governance_config, get_map_modules
19+
from torusdk.types.types import AgentMetadata
20+
from torusdk.util import get_json_from_cid
1721

1822
agent_app = typer.Typer(no_args_is_help=True)
1923

@@ -26,19 +30,29 @@ def register(
2630
name: str,
2731
key: str,
2832
url: str,
29-
metadata: str,
33+
metadata: str = typer.Argument(..., callback=extract_cid),
3034
):
3135
"""
3236
Registers an agent.
3337
"""
3438
context = make_custom_context(ctx)
3539
client = context.com_client()
36-
37-
if metadata and len(metadata) > 59:
38-
raise ValueError("Metadata must be less than 60 characters")
39-
40+
data = get_json_from_cid(metadata)
41+
try:
42+
_ = AgentMetadata.model_validate(data)
43+
except ValidationError:
44+
context.error(
45+
"Your ipfs file is invalid. "
46+
"You can find the schema definition "
47+
"at https://docs.torus.network/agents/register-a-agent"
48+
)
49+
exit(1)
4050
resolved_key = context.load_key(key, None)
41-
51+
burn = client.get_burn()
52+
if not context.confirm(
53+
f"{from_rems(burn)} tokens will be burned. Do you want to continue?"
54+
):
55+
raise typer.Abort()
4256
with context.progress_status(f"Registering Agent {name}..."):
4357
response = client.register_agent(
4458
resolved_key,
@@ -60,7 +74,6 @@ def list_applications(ctx: Context):
6074
"""
6175
context = make_custom_context(ctx)
6276
client = context.com_client()
63-
6477
with context.progress_status("Getting applications..."):
6578
applications = client.query_map_applications()
6679
if len(applications) == 0:
@@ -140,9 +153,20 @@ def update(
140153
context = make_custom_context(ctx)
141154
client = context.com_client()
142155

143-
if metadata and len(metadata) > 59:
144-
raise ValueError("Metadata must be less than 60 characters")
145-
156+
# if metadata and len(metadata) > 59:
157+
# raise ValueError("Metadata must be less than 60 characters")
158+
# TODO: create a validator for agent metadata
159+
if metadata:
160+
data = get_json_from_cid(metadata)
161+
try:
162+
_ = AgentMetadata.model_validate(data)
163+
except ValidationError:
164+
context.error(
165+
"Your ipfs file is invalid. "
166+
"You can find the schema definition "
167+
"at https://docs.torus.network/agents/register-a-agent"
168+
)
169+
exit(1)
146170
resolved_key = context.load_key(key)
147171

148172
agents = get_map_modules(client, include_balances=False)

src/torusdk/cli/network.py

Lines changed: 2 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
import typer
22
from typer import Context
33

4-
import torusdk.balance as c_balance
54
from torusdk.cli._common import (
65
make_custom_context,
7-
print_table_from_plain_dict,
8-
tranform_network_params,
6+
render_pydantic_table,
97
)
108
from torusdk.misc import (
119
get_global_params,
@@ -42,27 +40,4 @@ def params(ctx: Context):
4240

4341
with context.progress_status("Getting global network params ..."):
4442
global_params = get_global_params(client)
45-
global_params = global_params.model_dump()
46-
printable_params = tranform_network_params(global_params)
47-
print_table_from_plain_dict(
48-
printable_params, ["Global params", "Value"], context.console
49-
)
50-
51-
52-
@network_app.command()
53-
def registration_burn(
54-
ctx: Context,
55-
netuid: int,
56-
):
57-
"""
58-
Appraises the cost of registering a agent on the torus network.
59-
"""
60-
61-
context = make_custom_context(ctx)
62-
client = context.com_client()
63-
64-
burn = client.get_burn()
65-
registration_cost = c_balance.from_rems(burn)
66-
context.info(
67-
f"The cost to register on a netuid: {netuid} is: {registration_cost} $TORUS"
68-
)
43+
render_pydantic_table(global_params, context.console)

src/torusdk/cli/proposal.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from rich.progress import track
66
from typer import Context
77

8-
from torusdk._common import IPFS_REGEX
8+
from torusdk._common import CID_REGEX
99
from torusdk.balance import to_rems
1010
from torusdk.cli._common import (
1111
CustomCtx,
@@ -103,7 +103,7 @@ def add_custom_proposal(ctx: Context, key: str, cid: str):
103103
Adds a custom proposal.
104104
"""
105105
context = make_custom_context(ctx)
106-
if not re.match(IPFS_REGEX, cid):
106+
if not re.match(CID_REGEX, cid):
107107
context.error(f"CID provided is invalid: {cid}")
108108
exit(1)
109109
else:
@@ -221,6 +221,7 @@ def propose_emission(
221221
cid: str = typer.Argument(..., callback=extract_cid),
222222
recycling_percentage: Optional[int] = typer.Option(None),
223223
treasury_percentage: Optional[int] = typer.Option(None),
224+
incentives_ratio: Optional[int] = typer.Option(None),
224225
):
225226
local_variables = locals()
226227
proposal_args = OptionalEmission.model_validate(local_variables)
@@ -231,3 +232,4 @@ def propose_emission(
231232
proposal = merge_models(emission_params, proposal_args)
232233
kp = context.load_key(key)
233234
client.add_emission_proposal(kp, proposal, cid)
235+
context.info("Proposal added.")

src/torusdk/client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1181,7 +1181,7 @@ def update_agent(
11811181

11821182
params = {
11831183
"name": name,
1184-
"address": url,
1184+
"url": url,
11851185
"metadata": metadata,
11861186
"staking_fee": staking_fee,
11871187
"weight_control_fee": weight_control_fee,

src/torusdk/misc.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ def get_emission_params(c_client: TorusClient):
113113
{
114114
"Emission0": [
115115
("EmissionRecyclingPercentage", []),
116+
("IncentivesRatio", []),
116117
],
117118
"Governance": [
118119
("TreasuryEmissionFee", []),
@@ -122,12 +123,24 @@ def get_emission_params(c_client: TorusClient):
122123
raw_emission = {
123124
"recycling_percentage": query_all["EmissionRecyclingPercentage"],
124125
"treasury_percentage": query_all["TreasuryEmissionFee"],
126+
"incentives_ratio": query_all["IncentivesRatio"],
125127
}
126128
emission_params = Emission.model_validate(raw_emission)
127129

128130
return emission_params
129131

130132

133+
def get_fees(c_client: TorusClient):
134+
fees = c_client.query_batch(
135+
{
136+
"Torus0": [
137+
("FeeConstraints", []),
138+
],
139+
}
140+
)["FeeConstraints"]
141+
return MinFee.model_validate(fees)
142+
143+
131144
def get_global_params(c_client: TorusClient):
132145
"""
133146
Returns global parameters of the whole commune ecosystem

src/torusdk/types/proposal.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ class OptionalEmission(BaseModel):
9797
class Emission(BaseModel):
9898
recycling_percentage: int = Field(..., ge=0, le=100)
9999
treasury_percentage: int = Field(..., ge=0, le=100)
100+
incentives_ratio: int = Field(..., ge=0, le=100)
100101

101102

102103
class GlobalCustom(BaseModel):
@@ -164,7 +165,9 @@ def extract_value(data: Any, key_to_extract: str):
164165
if not data.get(key_to_extract):
165166
raise ValueError("Data must contain a 'data' key")
166167
if not isinstance(data[key_to_extract], dict):
167-
raise ValueError("Extracted key must contain a dictionary")
168+
value = data[key_to_extract]
169+
data[key_to_extract] = {key_to_extract: value}
170+
return data
168171
if len(data.get(key_to_extract)) != 1:
169172
raise ValueError("Data must contain only one key")
170173
data[key_to_extract] = [*data[key_to_extract].values()][0]

0 commit comments

Comments
 (0)