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
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Off-chain configuration management operations.
|
|
3
|
+
Handles local configuration files, network settings, and CLI preferences.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Optional
|
|
9
|
+
|
|
10
|
+
import yaml
|
|
11
|
+
|
|
12
|
+
from ...models.requests import (
|
|
13
|
+
ConfigGetRequest,
|
|
14
|
+
ConfigInitRequest,
|
|
15
|
+
ConfigSetRequest,
|
|
16
|
+
ConfigShowRequest,
|
|
17
|
+
ConfigValidateRequest,
|
|
18
|
+
)
|
|
19
|
+
from ...utils.logging import get_logger
|
|
20
|
+
|
|
21
|
+
logger = get_logger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ConfigManager:
|
|
25
|
+
"""Manager for off-chain configuration operations."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, config_dir: Optional[str] = None):
|
|
28
|
+
self.config_dir = Path(config_dir) if config_dir else Path.home() / ".htcli"
|
|
29
|
+
self.config_dir.mkdir(parents=True, exist_ok=True)
|
|
30
|
+
self.config_file = self.config_dir / "config.yaml"
|
|
31
|
+
self.default_config = {
|
|
32
|
+
"network": {
|
|
33
|
+
"endpoint": "wss://rpc.htcli.io/",
|
|
34
|
+
"ws_endpoint": "wss://rpc.htcli.io/",
|
|
35
|
+
"timeout": 30,
|
|
36
|
+
"retry_attempts": 3,
|
|
37
|
+
"chain": "hypertensor", # Chain name (optional, can be auto-detected)
|
|
38
|
+
},
|
|
39
|
+
"wallet": {
|
|
40
|
+
"path": "~/.htcli/wallets",
|
|
41
|
+
"default_name": "default",
|
|
42
|
+
"encryption_enabled": True,
|
|
43
|
+
},
|
|
44
|
+
"output": {
|
|
45
|
+
"format": "table",
|
|
46
|
+
"verbose": False,
|
|
47
|
+
"color": True,
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
def load_config(self) -> dict:
|
|
52
|
+
"""Load configuration from file or create default if not exists."""
|
|
53
|
+
try:
|
|
54
|
+
if self.config_file.exists():
|
|
55
|
+
with open(self.config_file) as f:
|
|
56
|
+
if self.config_file.suffix.lower() == ".json":
|
|
57
|
+
config = json.load(f)
|
|
58
|
+
else:
|
|
59
|
+
config = yaml.safe_load(f)
|
|
60
|
+
|
|
61
|
+
# Validate and clean config to only include fields from default_config
|
|
62
|
+
validated_config = self._validate_config(config)
|
|
63
|
+
return {
|
|
64
|
+
"success": True,
|
|
65
|
+
"message": "Configuration loaded successfully",
|
|
66
|
+
"data": validated_config,
|
|
67
|
+
}
|
|
68
|
+
else:
|
|
69
|
+
# Create default config
|
|
70
|
+
return self.save_config(self.default_config)
|
|
71
|
+
except Exception as e:
|
|
72
|
+
logger.error(f"Failed to load config: {str(e)}")
|
|
73
|
+
raise
|
|
74
|
+
|
|
75
|
+
def save_config(self, config: dict) -> dict:
|
|
76
|
+
"""Save configuration to file."""
|
|
77
|
+
try:
|
|
78
|
+
# Validate config structure
|
|
79
|
+
validated_config = self._validate_config(config)
|
|
80
|
+
|
|
81
|
+
with open(self.config_file, "w") as f:
|
|
82
|
+
if self.config_file.suffix.lower() == ".json":
|
|
83
|
+
json.dump(validated_config, f, indent=2)
|
|
84
|
+
else:
|
|
85
|
+
yaml.dump(
|
|
86
|
+
validated_config,
|
|
87
|
+
f,
|
|
88
|
+
indent=2,
|
|
89
|
+
default_flow_style=False,
|
|
90
|
+
sort_keys=False,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
"success": True,
|
|
95
|
+
"message": "Configuration saved successfully",
|
|
96
|
+
"data": validated_config,
|
|
97
|
+
}
|
|
98
|
+
except Exception as e:
|
|
99
|
+
logger.error(f"Failed to save config: {str(e)}")
|
|
100
|
+
raise
|
|
101
|
+
|
|
102
|
+
def get_setting(self, key_path: str) -> Any:
|
|
103
|
+
"""Get a specific setting using dot notation (e.g., 'network.endpoint')."""
|
|
104
|
+
try:
|
|
105
|
+
config_result = self.load_config()
|
|
106
|
+
config = config_result["data"]
|
|
107
|
+
|
|
108
|
+
keys = key_path.split(".")
|
|
109
|
+
value = config
|
|
110
|
+
|
|
111
|
+
for key in keys:
|
|
112
|
+
if isinstance(value, dict) and key in value:
|
|
113
|
+
value = value[key]
|
|
114
|
+
else:
|
|
115
|
+
raise KeyError(f"Setting '{key_path}' not found")
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
"success": True,
|
|
119
|
+
"message": f"Setting '{key_path}' retrieved successfully",
|
|
120
|
+
"data": value,
|
|
121
|
+
}
|
|
122
|
+
except Exception as e:
|
|
123
|
+
logger.error(f"Failed to get setting: {str(e)}")
|
|
124
|
+
raise
|
|
125
|
+
|
|
126
|
+
def set_setting(self, key_path: str, value: Any) -> dict:
|
|
127
|
+
"""Set a specific setting using dot notation.
|
|
128
|
+
|
|
129
|
+
Only allows setting fields that are in default_config.
|
|
130
|
+
"""
|
|
131
|
+
try:
|
|
132
|
+
# Validate that the key path is in default_config
|
|
133
|
+
keys = key_path.split(".")
|
|
134
|
+
if len(keys) < 2:
|
|
135
|
+
raise ValueError(
|
|
136
|
+
f"Invalid key path: {key_path}. Must be in format 'section.field'"
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
section_name = keys[0]
|
|
140
|
+
field_name = keys[-1]
|
|
141
|
+
|
|
142
|
+
# Check if section exists in default_config
|
|
143
|
+
if section_name not in self.default_config:
|
|
144
|
+
raise ValueError(
|
|
145
|
+
f"Invalid section '{section_name}'. "
|
|
146
|
+
f"Available sections: {', '.join(self.default_config.keys())}"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Check if field exists in default_config section
|
|
150
|
+
if field_name not in self.default_config[section_name]:
|
|
151
|
+
raise ValueError(
|
|
152
|
+
f"Invalid field '{field_name}' in section '{section_name}'. "
|
|
153
|
+
f"Available fields: {', '.join(self.default_config[section_name].keys())}"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# Load current config
|
|
157
|
+
config_result = self.load_config()
|
|
158
|
+
config = config_result["data"]
|
|
159
|
+
|
|
160
|
+
# Navigate to the parent of the target key
|
|
161
|
+
current = config
|
|
162
|
+
for key in keys[:-1]:
|
|
163
|
+
if key not in current:
|
|
164
|
+
current[key] = {}
|
|
165
|
+
current = current[key]
|
|
166
|
+
|
|
167
|
+
# Set the value
|
|
168
|
+
current[keys[-1]] = value
|
|
169
|
+
|
|
170
|
+
# Save updated config (validation will ensure only valid fields are saved)
|
|
171
|
+
self.save_config(config)
|
|
172
|
+
return {
|
|
173
|
+
"success": True,
|
|
174
|
+
"message": f"Setting '{key_path}' updated successfully",
|
|
175
|
+
"data": {"key": key_path, "value": value},
|
|
176
|
+
}
|
|
177
|
+
except Exception as e:
|
|
178
|
+
logger.error(f"Failed to set setting: {str(e)}")
|
|
179
|
+
raise
|
|
180
|
+
|
|
181
|
+
def reset_config(self, confirm: bool = False) -> dict:
|
|
182
|
+
"""Reset configuration to defaults."""
|
|
183
|
+
try:
|
|
184
|
+
if not confirm:
|
|
185
|
+
raise ValueError("Config reset requires explicit confirmation")
|
|
186
|
+
|
|
187
|
+
self.save_config(self.default_config)
|
|
188
|
+
return {
|
|
189
|
+
"success": True,
|
|
190
|
+
"message": "Configuration reset to defaults successfully",
|
|
191
|
+
"data": self.default_config,
|
|
192
|
+
}
|
|
193
|
+
except Exception as e:
|
|
194
|
+
logger.error(f"Failed to reset config: {str(e)}")
|
|
195
|
+
raise
|
|
196
|
+
|
|
197
|
+
def backup_config(self, backup_path: Optional[str] = None) -> dict:
|
|
198
|
+
"""Create a backup of current configuration."""
|
|
199
|
+
try:
|
|
200
|
+
if backup_path is None:
|
|
201
|
+
backup_dir = self.config_dir / "backups"
|
|
202
|
+
backup_dir.mkdir(exist_ok=True)
|
|
203
|
+
backup_path = (
|
|
204
|
+
backup_dir / f"config_backup_{int(Path().stat().st_ctime)}.json"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
backup_file = Path(backup_path)
|
|
208
|
+
backup_file.parent.mkdir(parents=True, exist_ok=True)
|
|
209
|
+
|
|
210
|
+
if self.config_file.exists():
|
|
211
|
+
with (
|
|
212
|
+
open(self.config_file) as src,
|
|
213
|
+
open(backup_file, "w") as dst,
|
|
214
|
+
):
|
|
215
|
+
dst.write(src.read())
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
"success": True,
|
|
219
|
+
"message": "Configuration backed up successfully",
|
|
220
|
+
"data": {
|
|
221
|
+
"backup_path": str(backup_file),
|
|
222
|
+
"original_path": str(self.config_file),
|
|
223
|
+
},
|
|
224
|
+
}
|
|
225
|
+
else:
|
|
226
|
+
raise ValueError("No configuration file to backup")
|
|
227
|
+
except Exception as e:
|
|
228
|
+
logger.error(f"Failed to backup config: {str(e)}")
|
|
229
|
+
raise
|
|
230
|
+
|
|
231
|
+
def restore_config(self, backup_path: str) -> dict:
|
|
232
|
+
"""Restore configuration from backup."""
|
|
233
|
+
try:
|
|
234
|
+
backup_file = Path(backup_path)
|
|
235
|
+
|
|
236
|
+
if not backup_file.exists():
|
|
237
|
+
raise ValueError(f"Backup file '{backup_path}' not found")
|
|
238
|
+
|
|
239
|
+
# Try YAML first, then JSON for backward compatibility
|
|
240
|
+
with open(backup_file) as f:
|
|
241
|
+
if backup_path.endswith('.yaml') or backup_path.endswith('.yml'):
|
|
242
|
+
config = yaml.safe_load(f)
|
|
243
|
+
else:
|
|
244
|
+
# Try YAML first, fall back to JSON if it fails
|
|
245
|
+
try:
|
|
246
|
+
f.seek(0)
|
|
247
|
+
config = yaml.safe_load(f)
|
|
248
|
+
except Exception:
|
|
249
|
+
import json
|
|
250
|
+
f.seek(0)
|
|
251
|
+
config = json.load(f)
|
|
252
|
+
|
|
253
|
+
self.save_config(config)
|
|
254
|
+
return {
|
|
255
|
+
"success": True,
|
|
256
|
+
"message": "Configuration restored from backup successfully",
|
|
257
|
+
"data": {"backup_path": backup_path, "config": config},
|
|
258
|
+
}
|
|
259
|
+
except Exception as e:
|
|
260
|
+
logger.error(f"Failed to restore config: {str(e)}")
|
|
261
|
+
raise
|
|
262
|
+
|
|
263
|
+
def list_network_profiles(self) -> dict:
|
|
264
|
+
"""List available network profiles."""
|
|
265
|
+
try:
|
|
266
|
+
profiles = {
|
|
267
|
+
"mainnet": {
|
|
268
|
+
"endpoint": "wss://rpc.htcli.io/",
|
|
269
|
+
"ws_endpoint": "wss://rpc.htcli.io/",
|
|
270
|
+
"chain": "hypertensor",
|
|
271
|
+
"ss58_format": 0,
|
|
272
|
+
},
|
|
273
|
+
"testnet": {
|
|
274
|
+
"endpoint": "wss://testnet-rpc.hypertensor.io",
|
|
275
|
+
"ws_endpoint": "wss://testnet-ws.hypertensor.io",
|
|
276
|
+
"chain": "hypertensor-testnet",
|
|
277
|
+
"ss58_format": 0,
|
|
278
|
+
},
|
|
279
|
+
"local": {
|
|
280
|
+
"endpoint": "ws://127.0.0.1:9944",
|
|
281
|
+
"ws_endpoint": "ws://127.0.0.1:9944",
|
|
282
|
+
"chain": "hypertensor-local",
|
|
283
|
+
"ss58_format": 0,
|
|
284
|
+
},
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
"success": True,
|
|
289
|
+
"message": "Network profiles retrieved successfully",
|
|
290
|
+
"data": profiles,
|
|
291
|
+
}
|
|
292
|
+
except Exception as e:
|
|
293
|
+
logger.error(f"Failed to list network profiles: {str(e)}")
|
|
294
|
+
raise
|
|
295
|
+
|
|
296
|
+
def switch_network(self, profile: str) -> dict:
|
|
297
|
+
"""Switch to a different network profile."""
|
|
298
|
+
try:
|
|
299
|
+
profiles_result = self.list_network_profiles()
|
|
300
|
+
profiles = profiles_result["data"]
|
|
301
|
+
|
|
302
|
+
if profile not in profiles:
|
|
303
|
+
raise ValueError(f"Unknown network profile: {profile}")
|
|
304
|
+
|
|
305
|
+
network_config = profiles[profile]
|
|
306
|
+
|
|
307
|
+
# Update network settings
|
|
308
|
+
for key, value in network_config.items():
|
|
309
|
+
self.set_setting(f"network.{key}", value)
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
"success": True,
|
|
313
|
+
"message": f"Switched to {profile} network successfully",
|
|
314
|
+
"data": {"profile": profile, "network_config": network_config},
|
|
315
|
+
}
|
|
316
|
+
except Exception as e:
|
|
317
|
+
logger.error(f"Failed to switch network: {str(e)}")
|
|
318
|
+
raise
|
|
319
|
+
|
|
320
|
+
def _merge_configs(self, default: dict, user: dict) -> dict:
|
|
321
|
+
"""Recursively merge user config with defaults."""
|
|
322
|
+
result = default.copy()
|
|
323
|
+
for key, value in user.items():
|
|
324
|
+
if (
|
|
325
|
+
key in result
|
|
326
|
+
and isinstance(result[key], dict)
|
|
327
|
+
and isinstance(value, dict)
|
|
328
|
+
):
|
|
329
|
+
result[key] = self._merge_configs(result[key], value)
|
|
330
|
+
else:
|
|
331
|
+
result[key] = value
|
|
332
|
+
return result
|
|
333
|
+
|
|
334
|
+
def _validate_config(self, config: dict) -> dict:
|
|
335
|
+
"""Validate configuration structure and values.
|
|
336
|
+
|
|
337
|
+
Only keeps fields that are in default_config, removing any extra fields.
|
|
338
|
+
"""
|
|
339
|
+
# Start with default config to ensure only valid fields are present
|
|
340
|
+
validated_config = {}
|
|
341
|
+
|
|
342
|
+
# Only include sections that are in default_config
|
|
343
|
+
for section_name, default_section in self.default_config.items():
|
|
344
|
+
if section_name in config:
|
|
345
|
+
# Only include fields that are in default_config
|
|
346
|
+
validated_section = {}
|
|
347
|
+
section_data = config[section_name]
|
|
348
|
+
for field_name in default_section.keys():
|
|
349
|
+
if field_name in section_data:
|
|
350
|
+
validated_section[field_name] = section_data[field_name]
|
|
351
|
+
else:
|
|
352
|
+
# Use default value if field is missing
|
|
353
|
+
validated_section[field_name] = default_section[field_name]
|
|
354
|
+
validated_config[section_name] = validated_section
|
|
355
|
+
else:
|
|
356
|
+
# Use default section if section is missing
|
|
357
|
+
validated_config[section_name] = default_section.copy()
|
|
358
|
+
|
|
359
|
+
# Validate network endpoint format
|
|
360
|
+
endpoint = validated_config.get("network", {}).get("endpoint", "")
|
|
361
|
+
if endpoint and not (
|
|
362
|
+
endpoint.startswith("ws://") or endpoint.startswith("wss://")
|
|
363
|
+
):
|
|
364
|
+
raise ValueError("Network endpoint must start with ws:// or wss://")
|
|
365
|
+
|
|
366
|
+
return validated_config
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
# --- Client Functions ---
|
|
370
|
+
|
|
371
|
+
# Default instance for client functions
|
|
372
|
+
_manager = ConfigManager()
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def get_config_path(config_file: Optional[str] = None) -> Path:
|
|
376
|
+
"""Gets the active configuration file path."""
|
|
377
|
+
if config_file:
|
|
378
|
+
return Path(config_file).expanduser()
|
|
379
|
+
return _manager.config_file
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def initialize_config(request: ConfigInitRequest) -> dict:
|
|
383
|
+
"""Initializes and saves a new configuration."""
|
|
384
|
+
path = get_config_path(request.config_file)
|
|
385
|
+
# Use a temporary manager for non-default paths
|
|
386
|
+
manager = ConfigManager(path.parent) if request.config_file else _manager
|
|
387
|
+
# Ensure we write to the exact requested file path (not just default name)
|
|
388
|
+
if request.config_file:
|
|
389
|
+
manager.config_file = path
|
|
390
|
+
|
|
391
|
+
# Map request fields to the nested config structure
|
|
392
|
+
new_config = manager.default_config.copy()
|
|
393
|
+
new_config["network"]["endpoint"] = request.endpoint
|
|
394
|
+
new_config["network"]["ws_endpoint"] = request.ws_endpoint or request.endpoint
|
|
395
|
+
new_config["network"]["timeout"] = request.timeout
|
|
396
|
+
new_config["network"]["retry_attempts"] = request.retry_attempts
|
|
397
|
+
new_config["wallet"]["path"] = request.wallet_path
|
|
398
|
+
new_config["wallet"]["default_name"] = request.default_wallet or "default"
|
|
399
|
+
new_config["wallet"]["encryption_enabled"] = request.encryption_enabled
|
|
400
|
+
new_config["output"]["format"] = request.output_format
|
|
401
|
+
new_config["output"]["color"] = request.color
|
|
402
|
+
new_config["output"]["verbose"] = request.verbose
|
|
403
|
+
|
|
404
|
+
result = manager.save_config(new_config)
|
|
405
|
+
|
|
406
|
+
return {
|
|
407
|
+
"config_path": manager.config_file,
|
|
408
|
+
"created": True,
|
|
409
|
+
"message": result["message"],
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def load_config(request: ConfigShowRequest) -> dict:
|
|
414
|
+
"""Loads configuration from disk."""
|
|
415
|
+
cfg_path = get_config_path(request.config_file)
|
|
416
|
+
manager = ConfigManager(cfg_path.parent) if request.config_file else _manager
|
|
417
|
+
if request.config_file:
|
|
418
|
+
manager.config_file = cfg_path
|
|
419
|
+
result = manager.load_config()
|
|
420
|
+
return {
|
|
421
|
+
"config_path": manager.config_file,
|
|
422
|
+
"config_data": result["data"],
|
|
423
|
+
"exists": result["success"],
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def save_config(config: dict, config_file: Optional[str] = None) -> dict:
|
|
428
|
+
"""Saves configuration to disk."""
|
|
429
|
+
cfg_path = get_config_path(config_file)
|
|
430
|
+
manager = ConfigManager(cfg_path.parent) if config_file else _manager
|
|
431
|
+
if config_file:
|
|
432
|
+
manager.config_file = cfg_path
|
|
433
|
+
result = manager.save_config(config)
|
|
434
|
+
return {
|
|
435
|
+
"config_path": manager.config_file,
|
|
436
|
+
"message": result["message"],
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def set_config_value(request: ConfigSetRequest) -> dict:
|
|
441
|
+
"""Sets a specific configuration value."""
|
|
442
|
+
cfg_path = get_config_path(request.config_file)
|
|
443
|
+
manager = ConfigManager(cfg_path.parent) if request.config_file else _manager
|
|
444
|
+
if request.config_file:
|
|
445
|
+
manager.config_file = cfg_path
|
|
446
|
+
|
|
447
|
+
try:
|
|
448
|
+
old_value_result = manager.get_setting(request.key)
|
|
449
|
+
old_value = old_value_result["data"]
|
|
450
|
+
except KeyError:
|
|
451
|
+
old_value = None
|
|
452
|
+
|
|
453
|
+
# Normalize values for comparison (convert to string, handle None, case-insensitive)
|
|
454
|
+
def normalize_value(val: Any) -> str:
|
|
455
|
+
if val is None:
|
|
456
|
+
return ""
|
|
457
|
+
# Convert to string and strip whitespace
|
|
458
|
+
val_str = str(val).strip()
|
|
459
|
+
# Handle boolean values (both actual bools and string representations)
|
|
460
|
+
if isinstance(val, bool):
|
|
461
|
+
return str(val).lower()
|
|
462
|
+
val_str_lower = val_str.lower()
|
|
463
|
+
if val_str_lower in ("true", "false"):
|
|
464
|
+
return val_str_lower
|
|
465
|
+
# Handle numeric values - normalize to avoid "30" vs "30.0" vs 30 mismatches
|
|
466
|
+
try:
|
|
467
|
+
# Try to convert to float first to handle decimals
|
|
468
|
+
num_val = float(val_str)
|
|
469
|
+
# If it's a whole number, normalize to int string representation
|
|
470
|
+
# This handles: 30 == "30" == "30.0" == 30.0
|
|
471
|
+
if num_val == int(num_val):
|
|
472
|
+
return str(int(num_val))
|
|
473
|
+
# For decimals, return the normalized float string
|
|
474
|
+
return str(num_val)
|
|
475
|
+
except (ValueError, TypeError):
|
|
476
|
+
# Not a number, return as lowercase string for case-insensitive comparison
|
|
477
|
+
return val_str_lower
|
|
478
|
+
|
|
479
|
+
old_value_normalized = normalize_value(old_value)
|
|
480
|
+
new_value_normalized = normalize_value(request.value)
|
|
481
|
+
|
|
482
|
+
# Early exit if values are the same
|
|
483
|
+
if old_value_normalized == new_value_normalized:
|
|
484
|
+
return {
|
|
485
|
+
"config_path": manager.config_file,
|
|
486
|
+
"key": request.key,
|
|
487
|
+
"old_value": old_value,
|
|
488
|
+
"new_value": request.value,
|
|
489
|
+
"message": "No change needed - value is already set to the same value",
|
|
490
|
+
"unchanged": True,
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
result = manager.set_setting(request.key, request.value)
|
|
494
|
+
|
|
495
|
+
return {
|
|
496
|
+
"config_path": manager.config_file,
|
|
497
|
+
"key": request.key,
|
|
498
|
+
"old_value": old_value,
|
|
499
|
+
"new_value": request.value,
|
|
500
|
+
"message": result["message"],
|
|
501
|
+
"unchanged": False,
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def get_config_value(request: ConfigGetRequest) -> dict:
|
|
506
|
+
"""Gets a specific configuration value."""
|
|
507
|
+
cfg_path = get_config_path(request.config_file)
|
|
508
|
+
manager = ConfigManager(cfg_path.parent) if request.config_file else _manager
|
|
509
|
+
if request.config_file:
|
|
510
|
+
manager.config_file = cfg_path
|
|
511
|
+
result = manager.get_setting(request.key)
|
|
512
|
+
value = result["data"]
|
|
513
|
+
|
|
514
|
+
return {
|
|
515
|
+
"key": request.key,
|
|
516
|
+
"value": value,
|
|
517
|
+
"value_type": str(type(value).__name__),
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def validate_config(request: ConfigValidateRequest) -> dict:
|
|
522
|
+
"""Validates the configuration file."""
|
|
523
|
+
cfg_path = get_config_path(request.config_file)
|
|
524
|
+
manager = ConfigManager(cfg_path.parent) if request.config_file else _manager
|
|
525
|
+
if request.config_file:
|
|
526
|
+
manager.config_file = cfg_path
|
|
527
|
+
try:
|
|
528
|
+
config_result = manager.load_config()
|
|
529
|
+
manager._validate_config(config_result["data"])
|
|
530
|
+
return {
|
|
531
|
+
"config_path": manager.config_file,
|
|
532
|
+
"valid": True,
|
|
533
|
+
"message": "Configuration is valid.",
|
|
534
|
+
}
|
|
535
|
+
except Exception as e:
|
|
536
|
+
return {
|
|
537
|
+
"config_path": manager.config_file,
|
|
538
|
+
"valid": False,
|
|
539
|
+
"errors": [str(e)],
|
|
540
|
+
"message": "Configuration validation failed.",
|
|
541
|
+
}
|