htcli 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. htcli-1.1.0.dist-info/METADATA +509 -0
  2. htcli-1.1.0.dist-info/RECORD +140 -0
  3. htcli-1.1.0.dist-info/WHEEL +4 -0
  4. htcli-1.1.0.dist-info/entry_points.txt +2 -0
  5. htcli-1.1.0.dist-info/licenses/LICENSE +21 -0
  6. src/__init__.py +0 -0
  7. src/htcli/__init__.py +5 -0
  8. src/htcli/client/__init__.py +338 -0
  9. src/htcli/client/extrinsics/__init__.py +26 -0
  10. src/htcli/client/extrinsics/base.py +487 -0
  11. src/htcli/client/extrinsics/consensus.py +79 -0
  12. src/htcli/client/extrinsics/governance.py +714 -0
  13. src/htcli/client/extrinsics/identity.py +490 -0
  14. src/htcli/client/extrinsics/node.py +1054 -0
  15. src/htcli/client/extrinsics/overwatch.py +401 -0
  16. src/htcli/client/extrinsics/staking.py +1504 -0
  17. src/htcli/client/extrinsics/subnet.py +2218 -0
  18. src/htcli/client/extrinsics/validator.py +203 -0
  19. src/htcli/client/extrinsics/wallet.py +323 -0
  20. src/htcli/client/offchain/__init__.py +10 -0
  21. src/htcli/client/offchain/backup.py +385 -0
  22. src/htcli/client/offchain/config.py +541 -0
  23. src/htcli/client/offchain/wallet.py +839 -0
  24. src/htcli/client/rpc/__init__.py +20 -0
  25. src/htcli/client/rpc/chain.py +568 -0
  26. src/htcli/client/rpc/node.py +783 -0
  27. src/htcli/client/rpc/overwatch.py +680 -0
  28. src/htcli/client/rpc/staking.py +216 -0
  29. src/htcli/client/rpc/subnet.py +2104 -0
  30. src/htcli/client/rpc/wallet.py +912 -0
  31. src/htcli/commands/__init__.py +31 -0
  32. src/htcli/commands/chain/__init__.py +66 -0
  33. src/htcli/commands/chain/display.py +204 -0
  34. src/htcli/commands/chain/handlers.py +260 -0
  35. src/htcli/commands/config/__init__.py +158 -0
  36. src/htcli/commands/config/display.py +353 -0
  37. src/htcli/commands/config/handlers.py +347 -0
  38. src/htcli/commands/config/prompts.py +357 -0
  39. src/htcli/commands/consensus/__init__.py +61 -0
  40. src/htcli/commands/consensus/handlers.py +100 -0
  41. src/htcli/commands/governance/__init__.py +49 -0
  42. src/htcli/commands/governance/handlers.py +81 -0
  43. src/htcli/commands/node/__init__.py +304 -0
  44. src/htcli/commands/node/display.py +749 -0
  45. src/htcli/commands/node/error_handling.py +470 -0
  46. src/htcli/commands/node/handlers.py +844 -0
  47. src/htcli/commands/node/prompts.py +346 -0
  48. src/htcli/commands/overwatch/__init__.py +219 -0
  49. src/htcli/commands/overwatch/display.py +396 -0
  50. src/htcli/commands/overwatch/error_handling.py +276 -0
  51. src/htcli/commands/overwatch/handlers.py +443 -0
  52. src/htcli/commands/overwatch/prompts.py +359 -0
  53. src/htcli/commands/stake/__init__.py +736 -0
  54. src/htcli/commands/stake/display.py +1103 -0
  55. src/htcli/commands/stake/error_handling.py +425 -0
  56. src/htcli/commands/stake/handlers.py +1902 -0
  57. src/htcli/commands/stake/prompts.py +1080 -0
  58. src/htcli/commands/subnet/__init__.py +639 -0
  59. src/htcli/commands/subnet/display.py +801 -0
  60. src/htcli/commands/subnet/error_handling.py +524 -0
  61. src/htcli/commands/subnet/handlers.py +2855 -0
  62. src/htcli/commands/subnet/prompts.py +1225 -0
  63. src/htcli/commands/validator/__init__.py +192 -0
  64. src/htcli/commands/validator/display.py +54 -0
  65. src/htcli/commands/validator/handlers.py +340 -0
  66. src/htcli/commands/wallet/__init__.py +546 -0
  67. src/htcli/commands/wallet/display.py +806 -0
  68. src/htcli/commands/wallet/error_handling.py +210 -0
  69. src/htcli/commands/wallet/handlers.py +3040 -0
  70. src/htcli/commands/wallet/prompts.py +1518 -0
  71. src/htcli/config.py +184 -0
  72. src/htcli/dependencies.py +186 -0
  73. src/htcli/errors/__init__.py +63 -0
  74. src/htcli/errors/base.py +141 -0
  75. src/htcli/errors/display.py +20 -0
  76. src/htcli/errors/handlers.py +710 -0
  77. src/htcli/main.py +343 -0
  78. src/htcli/models/__init__.py +21 -0
  79. src/htcli/models/enums/enum_types.py +35 -0
  80. src/htcli/models/errors.py +103 -0
  81. src/htcli/models/requests/__init__.py +197 -0
  82. src/htcli/models/requests/config.py +70 -0
  83. src/htcli/models/requests/consensus.py +19 -0
  84. src/htcli/models/requests/governance.py +38 -0
  85. src/htcli/models/requests/identity.py +51 -0
  86. src/htcli/models/requests/key.py +22 -0
  87. src/htcli/models/requests/node.py +91 -0
  88. src/htcli/models/requests/overwatch.py +64 -0
  89. src/htcli/models/requests/staking.py +580 -0
  90. src/htcli/models/requests/subnet.py +195 -0
  91. src/htcli/models/requests/validator.py +139 -0
  92. src/htcli/models/requests/wallet.py +118 -0
  93. src/htcli/models/responses/__init__.py +147 -0
  94. src/htcli/models/responses/base.py +18 -0
  95. src/htcli/models/responses/chain.py +39 -0
  96. src/htcli/models/responses/config.py +58 -0
  97. src/htcli/models/responses/identity.py +102 -0
  98. src/htcli/models/responses/overwatch.py +51 -0
  99. src/htcli/models/responses/staking.py +502 -0
  100. src/htcli/models/responses/subnet.py +856 -0
  101. src/htcli/models/responses/wallet.py +185 -0
  102. src/htcli/ui/__init__.py +87 -0
  103. src/htcli/ui/colors.py +309 -0
  104. src/htcli/ui/components/__init__.py +60 -0
  105. src/htcli/ui/components/panels.py +174 -0
  106. src/htcli/ui/components/progress.py +166 -0
  107. src/htcli/ui/components/spinners.py +92 -0
  108. src/htcli/ui/components/tables.py +809 -0
  109. src/htcli/ui/components/trees.py +721 -0
  110. src/htcli/ui/display.py +336 -0
  111. src/htcli/ui/prompts.py +870 -0
  112. src/htcli/utils/__init__.py +76 -0
  113. src/htcli/utils/blockchain/__init__.py +75 -0
  114. src/htcli/utils/blockchain/formatting.py +368 -0
  115. src/htcli/utils/blockchain/patches.py +286 -0
  116. src/htcli/utils/blockchain/peer_id.py +186 -0
  117. src/htcli/utils/blockchain/staking.py +448 -0
  118. src/htcli/utils/blockchain/type_registry.py +1373 -0
  119. src/htcli/utils/blockchain/validation.py +179 -0
  120. src/htcli/utils/cache.py +613 -0
  121. src/htcli/utils/constants.py +38 -0
  122. src/htcli/utils/legacy/__init__.py +12 -0
  123. src/htcli/utils/legacy/colors.py +311 -0
  124. src/htcli/utils/legacy/crypto.py +1176 -0
  125. src/htcli/utils/legacy/formatting.py +452 -0
  126. src/htcli/utils/legacy/interactive.py +306 -0
  127. src/htcli/utils/legacy/subnet_manifest.py +265 -0
  128. src/htcli/utils/legacy/validation.py +488 -0
  129. src/htcli/utils/logging.py +183 -0
  130. src/htcli/utils/network/__init__.py +20 -0
  131. src/htcli/utils/network/subnet.py +344 -0
  132. src/htcli/utils/prompts.py +27 -0
  133. src/htcli/utils/scale_codec.py +155 -0
  134. src/htcli/utils/validation/__init__.py +57 -0
  135. src/htcli/utils/validation/prompt_validators.py +267 -0
  136. src/htcli/utils/wallet/__init__.py +65 -0
  137. src/htcli/utils/wallet/auth.py +151 -0
  138. src/htcli/utils/wallet/core.py +1069 -0
  139. src/htcli/utils/wallet/crypto.py +1615 -0
  140. src/htcli/utils/wallet/migration.py +159 -0
@@ -0,0 +1,488 @@
1
+ """
2
+ Validation helpers for user input in the Hypertensor CLI.
3
+ """
4
+
5
+ import re
6
+ from pathlib import Path
7
+ from typing import Optional, Union
8
+
9
+
10
+ def validate_address(address: str) -> bool:
11
+ """Validate SS58 address format."""
12
+ # Basic SS58 validation - should be 42-48 characters and start with a number
13
+ if not address or len(address) < 42 or len(address) > 48:
14
+ return False
15
+
16
+ # Should start with a number (5 for mainnet, 0 for testnet)
17
+ if not address[0].isdigit():
18
+ return False
19
+
20
+ # Should contain only alphanumeric characters
21
+ if not re.match(r"^[1-9A-HJ-NP-Za-km-z]+$", address):
22
+ return False
23
+
24
+ return True
25
+
26
+
27
+ def validate_ethereum_address(address: str) -> bool:
28
+ """Validate Ethereum-style address format (Bytes20: 0x + 40 hex chars = 20 bytes)."""
29
+ if not address:
30
+ return False
31
+
32
+ # Must start with 0x
33
+ if not address.startswith("0x"):
34
+ return False
35
+
36
+ # Must be exactly 42 characters (0x + 40 hex chars = 20 bytes)
37
+ if len(address) != 42:
38
+ return False
39
+
40
+ # Must contain only valid hex characters after 0x
41
+ hex_part = address[2:]
42
+ if not re.match(r"^[a-fA-F0-9]{40}$", hex_part):
43
+ return False
44
+
45
+ return True
46
+
47
+
48
+ def validate_hotkey_address(address: str) -> bool:
49
+ """Validate hotkey address - should be Bytes20 format for EVM compatibility."""
50
+ return validate_ethereum_address(address)
51
+
52
+
53
+ def validate_amount(amount: Union[str, float, int]) -> bool:
54
+ """Validate amount format for TENSOR token (18 decimals)."""
55
+ try:
56
+ if isinstance(amount, str):
57
+ amount = float(amount)
58
+ elif isinstance(amount, int):
59
+ amount = float(amount)
60
+
61
+ if amount <= 0:
62
+ return False
63
+
64
+ # Check for TENSOR precision (18 decimals)
65
+ str_amount = f"{amount:.18f}"
66
+ if len(str_amount.split(".")[-1]) > 18:
67
+ return False
68
+
69
+ return True
70
+ except (ValueError, TypeError):
71
+ return False
72
+
73
+
74
+ def validate_subnet_id(subnet_id: Union[str, int]) -> bool:
75
+ """Validate subnet ID format."""
76
+ try:
77
+ if isinstance(subnet_id, str):
78
+ subnet_id = int(subnet_id)
79
+
80
+ if subnet_id <= 0:
81
+ return False
82
+
83
+ return True
84
+ except (ValueError, TypeError):
85
+ return False
86
+
87
+
88
+ def validate_node_id(node_id: Union[str, int]) -> bool:
89
+ """Validate node ID format."""
90
+ try:
91
+ if isinstance(node_id, str):
92
+ node_id = int(node_id)
93
+
94
+ if node_id <= 0:
95
+ return False
96
+
97
+ return True
98
+ except (ValueError, TypeError):
99
+ return False
100
+
101
+
102
+ def validate_peer_id(peer_id: str) -> bool:
103
+ """Validate peer ID format (MultiHash)."""
104
+ # Basic MultiHash validation
105
+ if not peer_id or len(peer_id) < 10:
106
+ return False
107
+
108
+ # Should start with Qm (base58btc multihash)
109
+ if not peer_id.startswith("Qm"):
110
+ return False
111
+
112
+ # Should contain only base58 characters
113
+ if not re.match(r"^[1-9A-HJ-NP-Za-km-z]+$", peer_id):
114
+ return False
115
+
116
+ return True
117
+
118
+
119
+ def validate_key_type(key_type: str) -> bool:
120
+ """Validate key type."""
121
+ valid_types = ["sr25519", "ed25519", "ecdsa"]
122
+ return key_type.lower() in valid_types
123
+
124
+
125
+ def validate_password(password: Optional[str]) -> bool:
126
+ """Validate password strength."""
127
+ if password is None:
128
+ return True # Allow empty passwords
129
+
130
+ if len(password) < 8:
131
+ return False
132
+
133
+ # Should contain at least one letter and one number
134
+ if not re.search(r"[a-zA-Z]", password) or not re.search(r"\d", password):
135
+ return False
136
+
137
+ return True
138
+
139
+
140
+ def validate_file_path(file_path: str) -> bool:
141
+ """Validate file path."""
142
+ try:
143
+ Path(file_path)
144
+ # Check if parent directory exists or can be created
145
+ return True
146
+ except Exception:
147
+ return False
148
+
149
+
150
+ def validate_rpc_url(url: str) -> bool:
151
+ """Validate RPC URL format."""
152
+ # Basic URL validation
153
+ if not url:
154
+ return False
155
+
156
+ # Should start with ws:// or wss:// or http:// or https://
157
+ if not re.match(r"^(ws|wss|http|https)://", url):
158
+ return False
159
+
160
+ return True
161
+
162
+
163
+ def validate_vote_type(vote: str) -> bool:
164
+ """Validate vote type."""
165
+ valid_votes = ["yay", "nay"]
166
+ return vote.lower() in valid_votes
167
+
168
+
169
+ def validate_proposal_data(data: str) -> bool:
170
+ """Validate proposal data."""
171
+ if not data or len(data.strip()) == 0:
172
+ return False
173
+
174
+ # Basic length validation
175
+ if len(data) > 10000: # 10KB limit
176
+ return False
177
+
178
+ return True
179
+
180
+
181
+ def validate_validation_data(data: str) -> bool:
182
+ """Validate validation data."""
183
+ if not data or len(data.strip()) == 0:
184
+ return False
185
+
186
+ # Basic length validation
187
+ if len(data) > 100000: # 100KB limit
188
+ return False
189
+
190
+ return True
191
+
192
+
193
+ def validate_memory_mb(memory_mb: int) -> bool:
194
+ """Validate memory requirement in MB."""
195
+ if memory_mb <= 0 or memory_mb > 100000: # 100GB limit
196
+ return False
197
+
198
+ return True
199
+
200
+
201
+ def validate_registration_blocks(blocks: int) -> bool:
202
+ """Validate registration period in blocks."""
203
+ if blocks <= 0 or blocks > 1000000: # 1M blocks limit
204
+ return False
205
+
206
+ return True
207
+
208
+
209
+ def validate_entry_interval(interval: int) -> bool:
210
+ """Validate entry interval in blocks."""
211
+ if interval <= 0 or interval > 100000: # 100K blocks limit
212
+ return False
213
+
214
+ return True
215
+
216
+
217
+ def validate_subnet_path(path: str) -> bool:
218
+ """Validate subnet path/name."""
219
+ if not path or len(path.strip()) == 0:
220
+ return False
221
+
222
+ # Should be alphanumeric with hyphens and underscores
223
+ if not re.match(r"^[a-zA-Z0-9_-]+$", path):
224
+ return False
225
+
226
+ # Length validation
227
+ if len(path) > 100:
228
+ return False
229
+
230
+ return True
231
+
232
+
233
+ def validate_wallet_name(name: str) -> bool:
234
+ """Validate wallet name."""
235
+ if not name or len(name.strip()) == 0:
236
+ return False
237
+
238
+ # Should be alphanumeric with hyphens and underscores
239
+ if not re.match(r"^[a-zA-Z0-9_-]+$", name):
240
+ return False
241
+
242
+ # Length validation
243
+ if len(name) > 50:
244
+ return False
245
+
246
+ return True
247
+
248
+
249
+ def validate_private_key(private_key: str) -> bool:
250
+ """Validate private key format."""
251
+ if not private_key or len(private_key.strip()) == 0:
252
+ return False
253
+
254
+ # Should be hex string
255
+ if not re.match(r"^[0-9a-fA-F]+$", private_key):
256
+ return False
257
+
258
+ # Length validation (32 bytes = 64 hex characters)
259
+ if len(private_key) != 64:
260
+ return False
261
+
262
+ return True
263
+
264
+
265
+ def validate_mnemonic(mnemonic: str) -> bool:
266
+ """Validate mnemonic phrase."""
267
+ if not mnemonic or len(mnemonic.strip()) == 0:
268
+ return False
269
+
270
+ # Split into words
271
+ words = mnemonic.strip().split()
272
+
273
+ # Should have 12, 15, 18, 21, or 24 words
274
+ if len(words) not in [12, 15, 18, 21, 24]:
275
+ return False
276
+
277
+ # All words should be lowercase
278
+ for word in words:
279
+ if not word.islower() or not word.isalpha():
280
+ return False
281
+
282
+ return True
283
+
284
+
285
+ def validate_block_number(block_number: Optional[Union[str, int]]) -> bool:
286
+ """Validate block number."""
287
+ if block_number is None:
288
+ return True # Allow None for latest block
289
+
290
+ try:
291
+ if isinstance(block_number, str):
292
+ block_number = int(block_number)
293
+
294
+ if block_number < 0:
295
+ return False
296
+
297
+ return True
298
+ except (ValueError, TypeError):
299
+ return False
300
+
301
+
302
+ def validate_limit(limit: Union[str, int]) -> bool:
303
+ """Validate limit parameter."""
304
+ try:
305
+ if isinstance(limit, str):
306
+ limit = int(limit)
307
+
308
+ if limit <= 0 or limit > 1000:
309
+ return False
310
+
311
+ return True
312
+ except (ValueError, TypeError):
313
+ return False
314
+
315
+
316
+ def validate_tensor_stake_amount(amount: Union[str, float, int]) -> bool:
317
+ """Validate TENSOR stake amount with 18 decimal precision."""
318
+ try:
319
+ if isinstance(amount, str):
320
+ amount = float(amount)
321
+ elif isinstance(amount, int):
322
+ amount = float(amount)
323
+
324
+ if amount <= 0:
325
+ return False
326
+
327
+ # Check for TENSOR precision (18 decimals)
328
+ str_amount = f"{amount:.18f}"
329
+ if len(str_amount.split(".")[-1]) > 18:
330
+ return False
331
+
332
+ return True
333
+ except (ValueError, TypeError):
334
+ return False
335
+
336
+
337
+ def validate_tensor_balance(amount: Union[str, float, int]) -> bool:
338
+ """Validate TENSOR balance amount with 18 decimal precision."""
339
+ try:
340
+ if isinstance(amount, str):
341
+ amount = float(amount)
342
+ elif isinstance(amount, int):
343
+ amount = float(amount)
344
+
345
+ if amount < 0: # Allow zero balance
346
+ return False
347
+
348
+ # Check for TENSOR precision (18 decimals)
349
+ str_amount = f"{amount:.18f}"
350
+ if len(str_amount.split(".")[-1]) > 18:
351
+ return False
352
+
353
+ return True
354
+ except (ValueError, TypeError):
355
+ return False
356
+
357
+
358
+ def validate_url(url: str) -> bool:
359
+ """Validate URL format for configuration."""
360
+ if not url:
361
+ return False
362
+
363
+ # Should start with ws:// or wss:// or http:// or https://
364
+ if not re.match(r"^(ws|wss|http|https)://", url):
365
+ return False
366
+
367
+ # Basic domain validation
368
+ if len(url) < 10:
369
+ return False
370
+
371
+ return True
372
+
373
+
374
+ def validate_path(path: str) -> bool:
375
+ """Validate file/directory path for configuration."""
376
+ try:
377
+ Path(path).expanduser()
378
+ # Check if parent directory exists or can be created
379
+ return True
380
+ except Exception:
381
+ return False
382
+
383
+
384
+ def validate_subnet_name(name: str) -> bool:
385
+ """Validate subnet name."""
386
+ if not name or len(name) < 1 or len(name) > 100:
387
+ return False
388
+ # Allow alphanumeric, hyphens, underscores, spaces
389
+ import re
390
+
391
+ return bool(re.match(r"^[a-zA-Z0-9\-\_\s]+$", name))
392
+
393
+
394
+ def validate_repo_url(repo: str) -> bool:
395
+ """Validate repository URL."""
396
+ if not repo or len(repo) < 10 or len(repo) > 500:
397
+ return False
398
+ # Basic URL validation
399
+ import re
400
+
401
+ return bool(re.match(r"^https?://[^\s/$.?#].[^\s]*$", repo))
402
+
403
+
404
+ def validate_subnet_description(description: str) -> bool:
405
+ """Validate subnet description."""
406
+ if not description or len(description) < 10 or len(description) > 1000:
407
+ return False
408
+ return True
409
+
410
+
411
+ def validate_stake_amount(amount: int) -> bool:
412
+ """Validate stake amount."""
413
+ return amount > 0 and amount <= 10**18 # Max 1 billion TENSOR
414
+
415
+
416
+ def validate_delegate_percentage(percentage: int) -> bool:
417
+ """Validate delegate stake percentage."""
418
+ return 0 <= percentage <= 100
419
+
420
+
421
+ def validate_epoch_value(epochs: int) -> bool:
422
+ """Validate epoch-based values."""
423
+ return 0 <= epochs <= 1000000
424
+
425
+
426
+ def validate_churn_limit(churn_limit: int) -> bool:
427
+ """Validate churn limit."""
428
+ return 1 <= churn_limit <= 1000
429
+
430
+
431
+ def validate_max_nodes(max_nodes: int) -> bool:
432
+ """Validate maximum registered nodes."""
433
+ return 1 <= max_nodes <= 10000
434
+
435
+
436
+ def validate_max_penalties(penalties: int) -> bool:
437
+ """Validate maximum node penalties."""
438
+ return 1 <= penalties <= 100
439
+
440
+
441
+ def validate_key_types(key_types: list) -> bool:
442
+ """Validate key types (matching Rust enum names)."""
443
+ valid_types = ["Rsa", "Ed25519", "Secp256k1", "Ecdsa"]
444
+ return all(kt in valid_types for kt in key_types)
445
+
446
+
447
+ def validate_node_removal_system(system: str) -> bool:
448
+ """Validate node removal system (matching Rust enum)."""
449
+ valid_systems = ["Consensus", "Stake", "Reputation"]
450
+ return system in valid_systems
451
+
452
+
453
+ def validate_coldkey_addresses(addresses: list[str]) -> bool:
454
+ """Validate a list of coldkey addresses (supports both SS58 and Ethereum formats)."""
455
+ if not addresses:
456
+ return True # Empty list is valid for subnet registration
457
+
458
+ for address in addresses:
459
+ # Accept either SS58 or Ethereum format
460
+ if not (validate_ss58_address(address) or validate_ethereum_address(address)):
461
+ return False
462
+
463
+ return True
464
+
465
+
466
+ def validate_ss58_address(address: str) -> bool:
467
+ """Validate SS58 address format."""
468
+ if not address or len(address) < 10 or len(address) > 100:
469
+ return False
470
+ # Basic SS58 format validation (starts with number and contains alphanumeric)
471
+ import re
472
+
473
+ return bool(re.match(r"^[1-9][a-zA-Z0-9]+$", address))
474
+
475
+
476
+ def validate_delegate_reward_rate(rate: int) -> bool:
477
+ """Validate delegate reward rate."""
478
+ if not isinstance(rate, int):
479
+ return False
480
+
481
+ if rate < 0:
482
+ return False
483
+
484
+ # Rate should be reasonable (not excessively high)
485
+ if rate > 1000000000000000000: # 1 TENSOR in smallest units
486
+ return False
487
+
488
+ return True
@@ -0,0 +1,183 @@
1
+ """
2
+ Centralized logging configuration for HTCLI.
3
+
4
+ This module provides a standardized logging setup with proper formatting,
5
+ log levels, and file rotation for the HTCLI application.
6
+ """
7
+
8
+ import logging
9
+ import logging.handlers
10
+ from pathlib import Path
11
+ from typing import Optional
12
+
13
+ from rich.console import Console
14
+ from rich.logging import RichHandler
15
+
16
+
17
+ class HTCLILogger:
18
+ """Centralized logger configuration for HTCLI."""
19
+
20
+ _initialized = False
21
+ _console = Console()
22
+
23
+ @classmethod
24
+ def setup_logging(
25
+ cls,
26
+ level: str = "INFO",
27
+ log_file: Optional[Path] = None,
28
+ enable_file_logging: bool = True,
29
+ enable_console_logging: bool = False,
30
+ ) -> None:
31
+ """
32
+ Set up centralized logging configuration.
33
+
34
+ Args:
35
+ level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
36
+ log_file: Path to log file (defaults to ~/.htcli/logs/htcli.log)
37
+ enable_file_logging: Whether to enable file logging
38
+ enable_console_logging: Whether to enable console logging
39
+ """
40
+ if cls._initialized:
41
+ return
42
+
43
+ # Create log directory if it doesn't exist
44
+ if log_file is None:
45
+ log_dir = Path.home() / ".htcli" / "logs"
46
+ try:
47
+ log_dir.mkdir(parents=True, exist_ok=True)
48
+ except OSError:
49
+ enable_file_logging = False
50
+ log_file = log_dir / "htcli.log"
51
+
52
+ # Configure root logger
53
+ root_logger = logging.getLogger()
54
+ root_logger.setLevel(getattr(logging, level.upper()))
55
+
56
+ # Clear existing handlers
57
+ root_logger.handlers.clear()
58
+
59
+ # Console handler with Rich formatting (only if explicitly enabled)
60
+ # If console logging is disabled, do NOT add any console handlers
61
+ # All output should go to the log file
62
+ if enable_console_logging:
63
+ console_handler = RichHandler(
64
+ console=cls._console,
65
+ rich_tracebacks=True,
66
+ show_path=False,
67
+ show_time=True,
68
+ )
69
+ console_handler.setLevel(getattr(logging, level.upper()))
70
+
71
+ # Only show DEBUG/INFO on console if level is DEBUG
72
+ if level.upper() != "DEBUG":
73
+ console_handler.setLevel(logging.WARNING)
74
+
75
+ root_logger.addHandler(console_handler)
76
+
77
+ # File handler with rotation
78
+ if enable_file_logging:
79
+ try:
80
+ file_handler = logging.handlers.RotatingFileHandler(
81
+ log_file,
82
+ maxBytes=10 * 1024 * 1024, # 10MB
83
+ backupCount=5,
84
+ encoding="utf-8",
85
+ )
86
+ file_handler.setLevel(logging.DEBUG)
87
+
88
+ # File formatter with more detail
89
+ file_formatter = logging.Formatter(
90
+ fmt="%(asctime)s | %(levelname)-8s | %(name)s:%(lineno)d | %(message)s",
91
+ datefmt="%Y-%m-%d %H:%M:%S",
92
+ )
93
+ file_handler.setFormatter(file_formatter)
94
+
95
+ root_logger.addHandler(file_handler)
96
+ except OSError:
97
+ enable_file_logging = False
98
+
99
+ # Set specific logger levels
100
+ cls._configure_logger_levels()
101
+
102
+ cls._initialized = True
103
+
104
+ # Log initialization
105
+ logger = logging.getLogger(__name__)
106
+ logger.info(f"HTCLI logging initialized - Level: {level}")
107
+ if enable_file_logging:
108
+ logger.info(f"Log file: {log_file}")
109
+
110
+ @classmethod
111
+ def _configure_logger_levels(cls) -> None:
112
+ """Configure specific logger levels for different modules."""
113
+ # Reduce noise from external libraries - set to ERROR to suppress warnings
114
+ substrate_logger = logging.getLogger("substrateinterface")
115
+ substrate_logger.setLevel(logging.ERROR)
116
+ substrate_logger.handlers.clear() # Clear any handlers from substrateinterface
117
+
118
+ logging.getLogger("websockets").setLevel(logging.ERROR)
119
+ logging.getLogger("websockets").handlers.clear()
120
+
121
+ logging.getLogger("urllib3").setLevel(logging.ERROR)
122
+ logging.getLogger("requests").setLevel(logging.ERROR)
123
+
124
+ # Enable debug for HTCLI modules if needed
125
+ logging.getLogger("htcli").setLevel(logging.DEBUG)
126
+
127
+ @classmethod
128
+ def get_logger(cls, name: str) -> logging.Logger:
129
+ """
130
+ Get a logger instance with proper configuration.
131
+
132
+ Args:
133
+ name: Logger name (usually __name__)
134
+
135
+ Returns:
136
+ Configured logger instance
137
+ """
138
+ if not cls._initialized:
139
+ # Initialize with console logging disabled by default to keep terminal clean
140
+ cls.setup_logging(enable_console_logging=False)
141
+
142
+ return logging.getLogger(name)
143
+
144
+
145
+ def get_logger(name: str) -> logging.Logger:
146
+ """
147
+ Convenience function to get a configured logger.
148
+
149
+ Args:
150
+ name: Logger name (usually __name__)
151
+
152
+ Returns:
153
+ Configured logger instance
154
+ """
155
+ return HTCLILogger.get_logger(name)
156
+
157
+
158
+ def setup_logging(
159
+ level: str = "INFO",
160
+ log_file: Optional[Path] = None,
161
+ enable_file_logging: bool = True,
162
+ enable_console_logging: bool = False,
163
+ ) -> None:
164
+ """
165
+ Convenience function to set up logging.
166
+
167
+ Args:
168
+ level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
169
+ log_file: Path to log file
170
+ enable_file_logging: Whether to enable file logging
171
+ enable_console_logging: Whether to enable console logging
172
+ """
173
+ HTCLILogger.setup_logging(
174
+ level=level,
175
+ log_file=log_file,
176
+ enable_file_logging=enable_file_logging,
177
+ enable_console_logging=enable_console_logging,
178
+ )
179
+
180
+
181
+ # Do NOT initialize logging on module import
182
+ # Let the application entry point (main.py) control initialization
183
+ # setup_logging()
@@ -0,0 +1,20 @@
1
+ """
2
+ Network utilities for the Hypertensor CLI.
3
+ Handles subnet management and network-specific operations.
4
+ """
5
+
6
+ from .subnet import (
7
+ add_subnet_to_registry,
8
+ get_subnet_from_registry,
9
+ list_registered_subnets,
10
+ remove_subnet_from_registry,
11
+ validate_subnet_manifest,
12
+ )
13
+
14
+ __all__ = [
15
+ "add_subnet_to_registry",
16
+ "get_subnet_from_registry",
17
+ "remove_subnet_from_registry",
18
+ "list_registered_subnets",
19
+ "validate_subnet_manifest",
20
+ ]