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,613 @@
1
+ """
2
+ Caching utilities for HTCLI.
3
+
4
+ This module provides efficient caching strategies for blockchain data
5
+ to improve performance and reduce redundant API calls.
6
+ """
7
+
8
+ import asyncio
9
+ import hashlib
10
+ import json
11
+ import time
12
+ from collections.abc import Awaitable, Callable
13
+ from functools import wraps
14
+ from pathlib import Path
15
+ from typing import Any, Optional
16
+
17
+ from ..utils.logging import get_logger
18
+
19
+ logger = get_logger(__name__)
20
+
21
+
22
+ class TTLCache:
23
+ """
24
+ Time-to-live cache implementation.
25
+
26
+ This cache automatically expires entries after a specified time period.
27
+ """
28
+
29
+ def __init__(self, default_ttl: float = 300.0, max_size: int = 1000):
30
+ """
31
+ Initialize TTL cache.
32
+
33
+ Args:
34
+ default_ttl: Default time-to-live in seconds
35
+ max_size: Maximum number of entries to store
36
+ """
37
+ self.default_ttl = default_ttl
38
+ self.max_size = max_size
39
+ self._cache: dict[str, dict[str, Any]] = {}
40
+ self._access_times: dict[str, float] = {}
41
+
42
+ def _is_expired(self, key: str) -> bool:
43
+ """Check if a cache entry is expired."""
44
+ if key not in self._cache:
45
+ return True
46
+
47
+ entry = self._cache[key]
48
+ if "expires_at" not in entry:
49
+ return True
50
+
51
+ return time.time() > entry["expires_at"]
52
+
53
+ def _cleanup_expired(self) -> None:
54
+ """Remove expired entries from cache."""
55
+ current_time = time.time()
56
+ expired_keys = []
57
+
58
+ for key, entry in self._cache.items():
59
+ if "expires_at" in entry and current_time > entry["expires_at"]:
60
+ expired_keys.append(key)
61
+
62
+ for key in expired_keys:
63
+ self._cache.pop(key, None)
64
+ self._access_times.pop(key, None)
65
+
66
+ def _evict_lru(self) -> None:
67
+ """Evict least recently used entry."""
68
+ if not self._access_times:
69
+ return
70
+
71
+ lru_key = min(self._access_times.keys(), key=lambda k: self._access_times[k])
72
+ self._cache.pop(lru_key, None)
73
+ self._access_times.pop(lru_key, None)
74
+
75
+ def get(self, key: str) -> Optional[Any]:
76
+ """
77
+ Get value from cache.
78
+
79
+ Args:
80
+ key: Cache key
81
+
82
+ Returns:
83
+ Cached value or None if not found/expired
84
+ """
85
+ if self._is_expired(key):
86
+ self._cache.pop(key, None)
87
+ self._access_times.pop(key, None)
88
+ return None
89
+
90
+ self._access_times[key] = time.time()
91
+ return self._cache[key]["value"]
92
+
93
+ def set(self, key: str, value: Any, ttl: Optional[float] = None) -> None:
94
+ """
95
+ Set value in cache.
96
+
97
+ Args:
98
+ key: Cache key
99
+ value: Value to cache
100
+ ttl: Time-to-live in seconds (uses default if None)
101
+ """
102
+ if ttl is None:
103
+ ttl = self.default_ttl
104
+
105
+ # Clean up expired entries
106
+ self._cleanup_expired()
107
+
108
+ # Evict LRU if at max size
109
+ if len(self._cache) >= self.max_size and key not in self._cache:
110
+ self._evict_lru()
111
+
112
+ expires_at = time.time() + ttl
113
+ self._cache[key] = {
114
+ "value": value,
115
+ "expires_at": expires_at,
116
+ "created_at": time.time(),
117
+ }
118
+ self._access_times[key] = time.time()
119
+
120
+ def delete(self, key: str) -> None:
121
+ """Delete entry from cache."""
122
+ self._cache.pop(key, None)
123
+ self._access_times.pop(key, None)
124
+
125
+ def clear(self) -> None:
126
+ """Clear all cache entries."""
127
+ self._cache.clear()
128
+ self._access_times.clear()
129
+
130
+ def size(self) -> int:
131
+ """Get current cache size."""
132
+ self._cleanup_expired()
133
+ return len(self._cache)
134
+
135
+ def stats(self) -> dict[str, Any]:
136
+ """Get cache statistics."""
137
+ self._cleanup_expired()
138
+
139
+ if not self._cache:
140
+ return {
141
+ "size": 0,
142
+ "max_size": self.max_size,
143
+ "hit_rate": 0.0,
144
+ "avg_ttl": 0.0,
145
+ }
146
+
147
+ total_ttl = sum(
148
+ entry["expires_at"] - entry["created_at"] for entry in self._cache.values()
149
+ )
150
+
151
+ return {
152
+ "size": len(self._cache),
153
+ "max_size": self.max_size,
154
+ "avg_ttl": total_ttl / len(self._cache),
155
+ "oldest_entry": min(entry["created_at"] for entry in self._cache.values()),
156
+ "newest_entry": max(entry["created_at"] for entry in self._cache.values()),
157
+ }
158
+
159
+ async def get_async(self, key: str) -> Optional[Any]:
160
+ """
161
+ Async version of get method.
162
+
163
+ Args:
164
+ key: Cache key
165
+
166
+ Returns:
167
+ Cached value or None if not found/expired
168
+ """
169
+ # Run the sync get in a thread pool to avoid blocking
170
+ loop = asyncio.get_event_loop()
171
+ return await loop.run_in_executor(None, self.get, key)
172
+
173
+ async def set_async(
174
+ self, key: str, value: Any, ttl: Optional[float] = None
175
+ ) -> None:
176
+ """
177
+ Async version of set method.
178
+
179
+ Args:
180
+ key: Cache key
181
+ value: Value to cache
182
+ ttl: Time-to-live in seconds (uses default if None)
183
+ """
184
+ # Run the sync set in a thread pool to avoid blocking
185
+ loop = asyncio.get_event_loop()
186
+ await loop.run_in_executor(None, self.set, key, value, ttl)
187
+
188
+ async def delete_async(self, key: str) -> None:
189
+ """
190
+ Async version of delete method.
191
+
192
+ Args:
193
+ key: Cache key to delete
194
+ """
195
+ # Run the sync delete in a thread pool to avoid blocking
196
+ loop = asyncio.get_event_loop()
197
+ await loop.run_in_executor(None, self.delete, key)
198
+
199
+
200
+ class PersistentCache:
201
+ """
202
+ Persistent cache that saves data to disk.
203
+
204
+ This cache persists data across application restarts.
205
+ """
206
+
207
+ def __init__(self, cache_dir: Path, default_ttl: float = 3600.0):
208
+ """
209
+ Initialize persistent cache.
210
+
211
+ Args:
212
+ cache_dir: Directory to store cache files
213
+ default_ttl: Default time-to-live in seconds
214
+ """
215
+ self.cache_dir = cache_dir
216
+ self.cache_dir.mkdir(parents=True, exist_ok=True)
217
+ self.default_ttl = default_ttl
218
+ self._memory_cache = TTLCache(default_ttl=default_ttl)
219
+
220
+ def _get_cache_file(self, key: str) -> Path:
221
+ """Get cache file path for a key."""
222
+ # Hash the key to create a safe filename
223
+ key_hash = hashlib.sha256(key.encode()).hexdigest()
224
+ return self.cache_dir / f"{key_hash}.json"
225
+
226
+ def get(self, key: str) -> Optional[Any]:
227
+ """
228
+ Get value from persistent cache.
229
+
230
+ Args:
231
+ key: Cache key
232
+
233
+ Returns:
234
+ Cached value or None if not found/expired
235
+ """
236
+ # Try memory cache first
237
+ value = self._memory_cache.get(key)
238
+ if value is not None:
239
+ return value
240
+
241
+ # Try disk cache
242
+ cache_file = self._get_cache_file(key)
243
+ if not cache_file.exists():
244
+ return None
245
+
246
+ try:
247
+ with open(cache_file) as f:
248
+ data = json.load(f)
249
+
250
+ # Check if expired
251
+ if time.time() > data["expires_at"]:
252
+ cache_file.unlink()
253
+ return None
254
+
255
+ # Load into memory cache
256
+ self._memory_cache.set(key, data["value"], data["expires_at"] - time.time())
257
+ return data["value"]
258
+
259
+ except Exception as e:
260
+ logger.warning(f"Error reading cache file {cache_file}: {e}")
261
+ return None
262
+
263
+ def set(self, key: str, value: Any, ttl: Optional[float] = None) -> None:
264
+ """
265
+ Set value in persistent cache.
266
+
267
+ Args:
268
+ key: Cache key
269
+ value: Value to cache
270
+ ttl: Time-to-live in seconds (uses default if None)
271
+ """
272
+ if ttl is None:
273
+ ttl = self.default_ttl
274
+
275
+ expires_at = time.time() + ttl
276
+
277
+ # Set in memory cache
278
+ self._memory_cache.set(key, value, ttl)
279
+
280
+ # Save to disk
281
+ cache_file = self._get_cache_file(key)
282
+ try:
283
+ data = {"value": value, "expires_at": expires_at, "created_at": time.time()}
284
+
285
+ with open(cache_file, "w") as f:
286
+ json.dump(data, f)
287
+
288
+ except Exception as e:
289
+ logger.warning(f"Error writing cache file {cache_file}: {e}")
290
+
291
+ def delete(self, key: str) -> None:
292
+ """Delete entry from persistent cache."""
293
+ self._memory_cache.delete(key)
294
+
295
+ cache_file = self._get_cache_file(key)
296
+ if cache_file.exists():
297
+ cache_file.unlink()
298
+
299
+ def clear(self) -> None:
300
+ """Clear all cache entries."""
301
+ self._memory_cache.clear()
302
+
303
+ # Remove all cache files
304
+ for cache_file in self.cache_dir.glob("*.json"):
305
+ cache_file.unlink()
306
+
307
+ def cleanup_expired(self) -> None:
308
+ """Clean up expired cache files."""
309
+ current_time = time.time()
310
+
311
+ for cache_file in self.cache_dir.glob("*.json"):
312
+ try:
313
+ with open(cache_file) as f:
314
+ data = json.load(f)
315
+
316
+ if current_time > data["expires_at"]:
317
+ cache_file.unlink()
318
+
319
+ except Exception as e:
320
+ logger.warning(f"Error cleaning up cache file {cache_file}: {e}")
321
+
322
+ async def get_async(self, key: str) -> Optional[Any]:
323
+ """
324
+ Async version of get method.
325
+
326
+ Args:
327
+ key: Cache key
328
+
329
+ Returns:
330
+ Cached value or None if not found/expired
331
+ """
332
+ # Run the sync get in a thread pool to avoid blocking
333
+ loop = asyncio.get_event_loop()
334
+ return await loop.run_in_executor(None, self.get, key)
335
+
336
+ async def set_async(
337
+ self, key: str, value: Any, ttl: Optional[float] = None
338
+ ) -> None:
339
+ """
340
+ Async version of set method.
341
+
342
+ Args:
343
+ key: Cache key
344
+ value: Value to cache
345
+ ttl: Time-to-live in seconds (uses default if None)
346
+ """
347
+ # Run the sync set in a thread pool to avoid blocking
348
+ loop = asyncio.get_event_loop()
349
+ await loop.run_in_executor(None, self.set, key, value, ttl)
350
+
351
+ async def delete_async(self, key: str) -> None:
352
+ """
353
+ Async version of delete method.
354
+
355
+ Args:
356
+ key: Cache key to delete
357
+ """
358
+ # Run the sync delete in a thread pool to avoid blocking
359
+ loop = asyncio.get_event_loop()
360
+ await loop.run_in_executor(None, self.delete, key)
361
+
362
+ async def cleanup_expired_async(self) -> None:
363
+ """Async version of cleanup_expired method."""
364
+ # Run the sync cleanup in a thread pool to avoid blocking
365
+ loop = asyncio.get_event_loop()
366
+ await loop.run_in_executor(None, self.cleanup_expired)
367
+
368
+
369
+ # Global cache instances
370
+ _blockchain_cache: Optional[TTLCache] = None
371
+ _persistent_cache: Optional[PersistentCache] = None
372
+
373
+
374
+ def get_blockchain_cache() -> TTLCache:
375
+ """Get the global blockchain cache instance."""
376
+ global _blockchain_cache
377
+
378
+ if _blockchain_cache is None:
379
+ _blockchain_cache = TTLCache(default_ttl=60.0, max_size=500) # 1 minute TTL
380
+
381
+ return _blockchain_cache
382
+
383
+
384
+ def get_persistent_cache() -> PersistentCache:
385
+ """Get the global persistent cache instance."""
386
+ global _persistent_cache
387
+
388
+ if _persistent_cache is None:
389
+ cache_dir = Path.home() / ".htcli" / "cache"
390
+ _persistent_cache = PersistentCache(cache_dir, default_ttl=3600.0) # 1 hour TTL
391
+
392
+ return _persistent_cache
393
+
394
+
395
+ def cached(ttl: float = 300.0, cache_type: str = "memory", key_prefix: str = ""):
396
+ """
397
+ Decorator to cache function results.
398
+
399
+ Args:
400
+ ttl: Time-to-live in seconds
401
+ cache_type: Type of cache to use ("memory" or "persistent")
402
+ key_prefix: Prefix for cache keys
403
+
404
+ Returns:
405
+ Decorated function
406
+ """
407
+
408
+ def decorator(func: Callable) -> Callable:
409
+ @wraps(func)
410
+ def wrapper(*args, **kwargs):
411
+ # Generate cache key
412
+ cache_key = f"{key_prefix}{func.__name__}:{hash(str(args) + str(kwargs))}"
413
+
414
+ # Get appropriate cache
415
+ if cache_type == "persistent":
416
+ cache = get_persistent_cache()
417
+ else:
418
+ cache = get_blockchain_cache()
419
+
420
+ # Try to get from cache
421
+ result = cache.get(cache_key)
422
+ if result is not None:
423
+ logger.debug(f"Cache hit for {cache_key}")
424
+ return result
425
+
426
+ # Execute function and cache result
427
+ result = func(*args, **kwargs)
428
+ cache.set(cache_key, result, ttl)
429
+ logger.debug(f"Cached result for {cache_key}")
430
+
431
+ return result
432
+
433
+ return wrapper
434
+
435
+ return decorator
436
+
437
+
438
+ def cache_blockchain_data(key: str, data: Any, ttl: float = 60.0) -> None:
439
+ """
440
+ Cache blockchain data.
441
+
442
+ Args:
443
+ key: Cache key
444
+ data: Data to cache
445
+ ttl: Time-to-live in seconds
446
+ """
447
+ cache = get_blockchain_cache()
448
+ cache.set(key, data, ttl)
449
+
450
+
451
+ def get_cached_blockchain_data(key: str) -> Optional[Any]:
452
+ """
453
+ Get cached blockchain data.
454
+
455
+ Args:
456
+ key: Cache key
457
+
458
+ Returns:
459
+ Cached data or None if not found/expired
460
+ """
461
+ cache = get_blockchain_cache()
462
+ return cache.get(key)
463
+
464
+
465
+ def cache_persistent_data(key: str, data: Any, ttl: float = 3600.0) -> None:
466
+ """
467
+ Cache persistent data.
468
+
469
+ Args:
470
+ key: Cache key
471
+ data: Data to cache
472
+ ttl: Time-to-live in seconds
473
+ """
474
+ cache = get_persistent_cache()
475
+ cache.set(key, data, ttl)
476
+
477
+
478
+ def get_cached_persistent_data(key: str) -> Optional[Any]:
479
+ """
480
+ Get cached persistent data.
481
+
482
+ Args:
483
+ key: Cache key
484
+
485
+ Returns:
486
+ Cached data or None if not found/expired
487
+ """
488
+ cache = get_persistent_cache()
489
+ return cache.get(key)
490
+
491
+
492
+ def clear_all_caches() -> None:
493
+ """Clear all caches."""
494
+ global _blockchain_cache, _persistent_cache
495
+
496
+ if _blockchain_cache:
497
+ _blockchain_cache.clear()
498
+
499
+ if _persistent_cache:
500
+ _persistent_cache.clear()
501
+
502
+ logger.info("All caches cleared")
503
+
504
+
505
+ def get_cache_stats() -> dict[str, Any]:
506
+ """Get cache statistics."""
507
+ stats = {}
508
+
509
+ if _blockchain_cache:
510
+ stats["blockchain_cache"] = _blockchain_cache.stats()
511
+
512
+ if _persistent_cache:
513
+ stats["persistent_cache"] = _persistent_cache.stats()
514
+
515
+ return stats
516
+
517
+
518
+ # Async utility functions
519
+ async def cache_blockchain_data_async(key: str, data: Any, ttl: float = 60.0) -> None:
520
+ """
521
+ Async version of cache_blockchain_data.
522
+
523
+ Args:
524
+ key: Cache key
525
+ data: Data to cache
526
+ ttl: Time-to-live in seconds
527
+ """
528
+ cache = get_blockchain_cache()
529
+ await cache.set_async(key, data, ttl)
530
+
531
+
532
+ async def get_cached_blockchain_data_async(key: str) -> Optional[Any]:
533
+ """
534
+ Async version of get_cached_blockchain_data.
535
+
536
+ Args:
537
+ key: Cache key
538
+
539
+ Returns:
540
+ Cached data or None if not found/expired
541
+ """
542
+ cache = get_blockchain_cache()
543
+ return await cache.get_async(key)
544
+
545
+
546
+ async def cache_persistent_data_async(key: str, data: Any, ttl: float = 3600.0) -> None:
547
+ """
548
+ Async version of cache_persistent_data.
549
+
550
+ Args:
551
+ key: Cache key
552
+ data: Data to cache
553
+ ttl: Time-to-live in seconds
554
+ """
555
+ cache = get_persistent_cache()
556
+ await cache.set_async(key, data, ttl)
557
+
558
+
559
+ async def get_cached_persistent_data_async(key: str) -> Optional[Any]:
560
+ """
561
+ Async version of get_cached_persistent_data.
562
+
563
+ Args:
564
+ key: Cache key
565
+
566
+ Returns:
567
+ Cached data or None if not found/expired
568
+ """
569
+ cache = get_persistent_cache()
570
+ return await cache.get_async(key)
571
+
572
+
573
+ def async_cached(ttl: float = 300.0, cache_type: str = "memory", key_prefix: str = ""):
574
+ """
575
+ Async decorator to cache async function results.
576
+
577
+ Args:
578
+ ttl: Time-to-live in seconds
579
+ cache_type: Type of cache to use ("memory" or "persistent")
580
+ key_prefix: Prefix for cache keys
581
+
582
+ Returns:
583
+ Decorated async function
584
+ """
585
+
586
+ def decorator(func: Callable[..., Awaitable[Any]]) -> Callable[..., Awaitable[Any]]:
587
+ @wraps(func)
588
+ async def wrapper(*args, **kwargs):
589
+ # Generate cache key
590
+ cache_key = f"{key_prefix}{func.__name__}:{hash(str(args) + str(kwargs))}"
591
+
592
+ # Get appropriate cache
593
+ if cache_type == "persistent":
594
+ cache = get_persistent_cache()
595
+ else:
596
+ cache = get_blockchain_cache()
597
+
598
+ # Try to get from cache
599
+ result = await cache.get_async(cache_key)
600
+ if result is not None:
601
+ logger.debug(f"Async cache hit for {cache_key}")
602
+ return result
603
+
604
+ # Execute function and cache result
605
+ result = await func(*args, **kwargs)
606
+ await cache.set_async(cache_key, result, ttl)
607
+ logger.debug(f"Async cached result for {cache_key}")
608
+
609
+ return result
610
+
611
+ return wrapper
612
+
613
+ return decorator
@@ -0,0 +1,38 @@
1
+ """
2
+ Centralized runtime constants
3
+
4
+ These values originate from `hypertensor-evm/pallets/network/src/lib.rs`.
5
+ Keeping them in one place makes it straightforward to update the CLI whenever
6
+ the chain defaults change.
7
+ """
8
+
9
+ from decimal import Decimal
10
+
11
+ # Percentages in the runtime are expressed as 1e18 = 100%.
12
+ PERCENTAGE_FACTOR: Decimal = Decimal("1e18")
13
+
14
+ # --- Subnet activation requirements ---------------------------------------------------------
15
+
16
+ # Minimum number of active/electable nodes a subnet must maintain.
17
+ MIN_SUBNET_NODES: int = 3
18
+
19
+ # Maximum penalty count before a subnet is considered unhealthy.
20
+ MAX_SUBNET_PENALTY_COUNT: int = 16
21
+
22
+ # Minimum delegate stake factor (0.1% of total issuance, scaled by node multiplier).
23
+ MIN_SUBNET_DELEGATE_STAKE_FACTOR_RAW: int = 1_000_000_000_000_000
24
+ MIN_SUBNET_DELEGATE_STAKE_FACTOR_PERCENT: Decimal = (
25
+ Decimal(MIN_SUBNET_DELEGATE_STAKE_FACTOR_RAW) / PERCENTAGE_FACTOR * Decimal(100)
26
+ )
27
+
28
+
29
+ def format_percentage(value: Decimal, precision: int = 3) -> str:
30
+ """
31
+ Format a decimal percentage (e.g., Decimal('0.1')) into a human readable string.
32
+ """
33
+
34
+ formatted = f"{value:.{precision}f}"
35
+ if "." in formatted:
36
+ formatted = formatted.rstrip("0").rstrip(".")
37
+ return f"{formatted}%"
38
+
@@ -0,0 +1,12 @@
1
+ """
2
+ Legacy utilities - deprecated in favor of the new UI system.
3
+ These modules are kept for backwards compatibility during migration.
4
+
5
+ Use the new UI system instead:
6
+ - colors.py -> use ui.colors
7
+ - formatting.py -> use ui.display
8
+ - interactive.py -> use ui.prompts
9
+ """
10
+
11
+ # No exports - encourage use of new UI system
12
+ __all__ = []