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.
- htcli-1.1.0.dist-info/METADATA +509 -0
- htcli-1.1.0.dist-info/RECORD +140 -0
- htcli-1.1.0.dist-info/WHEEL +4 -0
- htcli-1.1.0.dist-info/entry_points.txt +2 -0
- htcli-1.1.0.dist-info/licenses/LICENSE +21 -0
- src/__init__.py +0 -0
- src/htcli/__init__.py +5 -0
- src/htcli/client/__init__.py +338 -0
- src/htcli/client/extrinsics/__init__.py +26 -0
- src/htcli/client/extrinsics/base.py +487 -0
- src/htcli/client/extrinsics/consensus.py +79 -0
- src/htcli/client/extrinsics/governance.py +714 -0
- src/htcli/client/extrinsics/identity.py +490 -0
- src/htcli/client/extrinsics/node.py +1054 -0
- src/htcli/client/extrinsics/overwatch.py +401 -0
- src/htcli/client/extrinsics/staking.py +1504 -0
- src/htcli/client/extrinsics/subnet.py +2218 -0
- src/htcli/client/extrinsics/validator.py +203 -0
- src/htcli/client/extrinsics/wallet.py +323 -0
- src/htcli/client/offchain/__init__.py +10 -0
- src/htcli/client/offchain/backup.py +385 -0
- src/htcli/client/offchain/config.py +541 -0
- src/htcli/client/offchain/wallet.py +839 -0
- src/htcli/client/rpc/__init__.py +20 -0
- src/htcli/client/rpc/chain.py +568 -0
- src/htcli/client/rpc/node.py +783 -0
- src/htcli/client/rpc/overwatch.py +680 -0
- src/htcli/client/rpc/staking.py +216 -0
- src/htcli/client/rpc/subnet.py +2104 -0
- src/htcli/client/rpc/wallet.py +912 -0
- src/htcli/commands/__init__.py +31 -0
- src/htcli/commands/chain/__init__.py +66 -0
- src/htcli/commands/chain/display.py +204 -0
- src/htcli/commands/chain/handlers.py +260 -0
- src/htcli/commands/config/__init__.py +158 -0
- src/htcli/commands/config/display.py +353 -0
- src/htcli/commands/config/handlers.py +347 -0
- src/htcli/commands/config/prompts.py +357 -0
- src/htcli/commands/consensus/__init__.py +61 -0
- src/htcli/commands/consensus/handlers.py +100 -0
- src/htcli/commands/governance/__init__.py +49 -0
- src/htcli/commands/governance/handlers.py +81 -0
- src/htcli/commands/node/__init__.py +304 -0
- src/htcli/commands/node/display.py +749 -0
- src/htcli/commands/node/error_handling.py +470 -0
- src/htcli/commands/node/handlers.py +844 -0
- src/htcli/commands/node/prompts.py +346 -0
- src/htcli/commands/overwatch/__init__.py +219 -0
- src/htcli/commands/overwatch/display.py +396 -0
- src/htcli/commands/overwatch/error_handling.py +276 -0
- src/htcli/commands/overwatch/handlers.py +443 -0
- src/htcli/commands/overwatch/prompts.py +359 -0
- src/htcli/commands/stake/__init__.py +736 -0
- src/htcli/commands/stake/display.py +1103 -0
- src/htcli/commands/stake/error_handling.py +425 -0
- src/htcli/commands/stake/handlers.py +1902 -0
- src/htcli/commands/stake/prompts.py +1080 -0
- src/htcli/commands/subnet/__init__.py +639 -0
- src/htcli/commands/subnet/display.py +801 -0
- src/htcli/commands/subnet/error_handling.py +524 -0
- src/htcli/commands/subnet/handlers.py +2855 -0
- src/htcli/commands/subnet/prompts.py +1225 -0
- src/htcli/commands/validator/__init__.py +192 -0
- src/htcli/commands/validator/display.py +54 -0
- src/htcli/commands/validator/handlers.py +340 -0
- src/htcli/commands/wallet/__init__.py +546 -0
- src/htcli/commands/wallet/display.py +806 -0
- src/htcli/commands/wallet/error_handling.py +210 -0
- src/htcli/commands/wallet/handlers.py +3040 -0
- src/htcli/commands/wallet/prompts.py +1518 -0
- src/htcli/config.py +184 -0
- src/htcli/dependencies.py +186 -0
- src/htcli/errors/__init__.py +63 -0
- src/htcli/errors/base.py +141 -0
- src/htcli/errors/display.py +20 -0
- src/htcli/errors/handlers.py +710 -0
- src/htcli/main.py +343 -0
- src/htcli/models/__init__.py +21 -0
- src/htcli/models/enums/enum_types.py +35 -0
- src/htcli/models/errors.py +103 -0
- src/htcli/models/requests/__init__.py +197 -0
- src/htcli/models/requests/config.py +70 -0
- src/htcli/models/requests/consensus.py +19 -0
- src/htcli/models/requests/governance.py +38 -0
- src/htcli/models/requests/identity.py +51 -0
- src/htcli/models/requests/key.py +22 -0
- src/htcli/models/requests/node.py +91 -0
- src/htcli/models/requests/overwatch.py +64 -0
- src/htcli/models/requests/staking.py +580 -0
- src/htcli/models/requests/subnet.py +195 -0
- src/htcli/models/requests/validator.py +139 -0
- src/htcli/models/requests/wallet.py +118 -0
- src/htcli/models/responses/__init__.py +147 -0
- src/htcli/models/responses/base.py +18 -0
- src/htcli/models/responses/chain.py +39 -0
- src/htcli/models/responses/config.py +58 -0
- src/htcli/models/responses/identity.py +102 -0
- src/htcli/models/responses/overwatch.py +51 -0
- src/htcli/models/responses/staking.py +502 -0
- src/htcli/models/responses/subnet.py +856 -0
- src/htcli/models/responses/wallet.py +185 -0
- src/htcli/ui/__init__.py +87 -0
- src/htcli/ui/colors.py +309 -0
- src/htcli/ui/components/__init__.py +60 -0
- src/htcli/ui/components/panels.py +174 -0
- src/htcli/ui/components/progress.py +166 -0
- src/htcli/ui/components/spinners.py +92 -0
- src/htcli/ui/components/tables.py +809 -0
- src/htcli/ui/components/trees.py +721 -0
- src/htcli/ui/display.py +336 -0
- src/htcli/ui/prompts.py +870 -0
- src/htcli/utils/__init__.py +76 -0
- src/htcli/utils/blockchain/__init__.py +75 -0
- src/htcli/utils/blockchain/formatting.py +368 -0
- src/htcli/utils/blockchain/patches.py +286 -0
- src/htcli/utils/blockchain/peer_id.py +186 -0
- src/htcli/utils/blockchain/staking.py +448 -0
- src/htcli/utils/blockchain/type_registry.py +1373 -0
- src/htcli/utils/blockchain/validation.py +179 -0
- src/htcli/utils/cache.py +613 -0
- src/htcli/utils/constants.py +38 -0
- src/htcli/utils/legacy/__init__.py +12 -0
- src/htcli/utils/legacy/colors.py +311 -0
- src/htcli/utils/legacy/crypto.py +1176 -0
- src/htcli/utils/legacy/formatting.py +452 -0
- src/htcli/utils/legacy/interactive.py +306 -0
- src/htcli/utils/legacy/subnet_manifest.py +265 -0
- src/htcli/utils/legacy/validation.py +488 -0
- src/htcli/utils/logging.py +183 -0
- src/htcli/utils/network/__init__.py +20 -0
- src/htcli/utils/network/subnet.py +344 -0
- src/htcli/utils/prompts.py +27 -0
- src/htcli/utils/scale_codec.py +155 -0
- src/htcli/utils/validation/__init__.py +57 -0
- src/htcli/utils/validation/prompt_validators.py +267 -0
- src/htcli/utils/wallet/__init__.py +65 -0
- src/htcli/utils/wallet/auth.py +151 -0
- src/htcli/utils/wallet/core.py +1069 -0
- src/htcli/utils/wallet/crypto.py +1615 -0
- src/htcli/utils/wallet/migration.py +159 -0
src/htcli/utils/cache.py
ADDED
|
@@ -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__ = []
|