cartha-cli 1.0.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.
@@ -0,0 +1,463 @@
1
+ """Health check command - verify CLI connectivity and configuration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import time
7
+ from typing import Any
8
+
9
+ import bittensor as bt
10
+ import typer
11
+ from rich.console import Console
12
+ from rich.table import Table
13
+
14
+ from ..config import Settings, settings
15
+ from ..verifier import VerifierError, _build_url, _request
16
+ from .common import console
17
+
18
+ import requests # type: ignore[import-untyped]
19
+
20
+
21
+ def health_check(
22
+ verbose: bool = typer.Option(
23
+ False, "--verbose", "-v", help="Show detailed information for each check."
24
+ ),
25
+ ) -> None:
26
+ """Check CLI health: verifier connectivity, Bittensor network, and configuration.
27
+
28
+ USAGE:
29
+ ------
30
+ cartha utils health (or: cartha u health)
31
+ cartha utils health --verbose (or: -v)
32
+
33
+ CHECKS:
34
+ -------
35
+ 1. Verifier connectivity and response time
36
+ 2. Bittensor network connectivity
37
+ 3. Configuration validation
38
+ 4. Subnet metadata (slots, tempo, block)
39
+ 5. Environment variables status
40
+
41
+ Use this to diagnose issues before running other commands.
42
+ """
43
+ checks_passed = 0
44
+ checks_failed = 0
45
+ checks_warning = 0
46
+
47
+ results: list[dict[str, Any]] = []
48
+
49
+ # Track subtensor connections for cleanup
50
+ subtensor_connections: list[Any] = []
51
+
52
+ try:
53
+ # Check 1: Verifier connectivity
54
+ console.print("\n[bold cyan]━━━ Health Check ━━━[/]")
55
+ console.print()
56
+
57
+ verifier_url = settings.verifier_url
58
+ console.print(f"[bold]Checking verifier connectivity...[/]")
59
+ console.print(f"[dim]URL: {verifier_url}[/]")
60
+
61
+ verifier_ok = False
62
+ verifier_status = "Unknown"
63
+ verifier_latency_ms = None
64
+
65
+ try:
66
+ start_time = time.time()
67
+ # Try to hit a simple endpoint (health or root)
68
+ try:
69
+ # Try /health endpoint first
70
+ health_url = _build_url("/health")
71
+ response = requests.get(health_url, timeout=(5, 10))
72
+ if response.status_code == 200:
73
+ verifier_ok = True
74
+ verifier_status = "Healthy"
75
+ elif response.status_code == 404:
76
+ # 404 is OK - means verifier is up but endpoint doesn't exist
77
+ # Try a simple GET request to verify connectivity
78
+ try:
79
+ _request("GET", "/v1/miner/status", params={"hotkey": "test", "slot": "0"}, retry=False)
80
+ verifier_ok = True
81
+ verifier_status = "Reachable (no /health endpoint)"
82
+ except VerifierError as exc:
83
+ # 404 or 400 means verifier is reachable, just wrong params
84
+ if exc.status_code in (400, 404):
85
+ verifier_ok = True
86
+ verifier_status = "Reachable"
87
+ else:
88
+ verifier_status = f"Error: {exc}"
89
+ else:
90
+ verifier_status = f"HTTP {response.status_code}"
91
+ except requests.RequestException as exc:
92
+ # Connection error - verifier is not reachable
93
+ verifier_status = f"Connection error: {exc}"
94
+ except VerifierError as exc:
95
+ # This shouldn't happen for /health, but handle it anyway
96
+ if exc.status_code == 404:
97
+ verifier_ok = True
98
+ verifier_status = "Reachable (no /health endpoint)"
99
+ else:
100
+ verifier_status = f"Error: {exc}"
101
+ except Exception as exc:
102
+ verifier_status = f"Error: {exc}"
103
+
104
+ if verifier_ok:
105
+ latency_ms = int((time.time() - start_time) * 1000)
106
+ verifier_latency_ms = latency_ms
107
+ console.print(f"[bold green]✓ Verifier is reachable[/] ({latency_ms}ms)")
108
+ checks_passed += 1
109
+ else:
110
+ console.print(f"[bold red]✗ Verifier check failed[/]: {verifier_status}")
111
+ checks_failed += 1
112
+
113
+ results.append({
114
+ "name": "Verifier Connectivity",
115
+ "status": "pass" if verifier_ok else "fail",
116
+ "details": verifier_status,
117
+ "latency_ms": verifier_latency_ms,
118
+ })
119
+ except Exception as exc:
120
+ console.print(f"[bold red]✗ Verifier check error[/]: {exc}")
121
+ checks_failed += 1
122
+ results.append({
123
+ "name": "Verifier Connectivity",
124
+ "status": "fail",
125
+ "details": str(exc),
126
+ })
127
+
128
+ # Check 2: Bittensor network connectivity
129
+ console.print()
130
+ console.print(f"[bold]Checking Bittensor network...[/]")
131
+ console.print(f"[dim]Network: {settings.network}, NetUID: {settings.netuid}[/]")
132
+
133
+ bt_ok = False
134
+ bt_status = "Unknown"
135
+ bt_latency_ms = None
136
+ bt_is_dns_error = False
137
+
138
+ try:
139
+ start_time = time.time()
140
+ subtensor = bt.subtensor(network=settings.network)
141
+ subtensor_connections.append(subtensor)
142
+ current_block = subtensor.get_current_block()
143
+ latency_ms = int((time.time() - start_time) * 1000)
144
+ bt_latency_ms = latency_ms
145
+
146
+ if current_block and current_block > 0:
147
+ bt_ok = True
148
+ bt_status = f"Connected (block: {current_block})"
149
+ console.print(f"[bold green]✓ Bittensor network is reachable[/] ({latency_ms}ms, block: {current_block})")
150
+ checks_passed += 1
151
+ else:
152
+ bt_status = "Connected but invalid block number"
153
+ console.print(f"[bold yellow]⚠ Bittensor network check warning[/]: {bt_status}")
154
+ checks_warning += 1
155
+ except OSError as exc:
156
+ # DNS resolution errors (Errno 8)
157
+ error_str = str(exc)
158
+ if "nodename nor servname provided" in error_str or "Errno 8" in error_str:
159
+ bt_is_dns_error = True
160
+ bt_status = "DNS resolution failed - cannot resolve Bittensor network endpoints"
161
+ console.print(f"[bold yellow]⚠ Bittensor network check failed[/]: DNS resolution error")
162
+ console.print("[dim]This usually indicates a network connectivity or DNS configuration issue.[/]")
163
+ checks_warning += 1 # Make it a warning since verifier is working
164
+ else:
165
+ bt_status = f"Network error: {exc}"
166
+ console.print(f"[bold red]✗ Bittensor network check failed[/]: {exc}")
167
+ checks_failed += 1
168
+ except Exception as exc:
169
+ error_str = str(exc)
170
+ # Check if it's a DNS/network related error
171
+ if any(keyword in error_str.lower() for keyword in ["dns", "resolve", "nodename", "servname", "network", "connection"]):
172
+ bt_is_dns_error = True
173
+ bt_status = f"Network connectivity issue: {exc}"
174
+ console.print(f"[bold yellow]⚠ Bittensor network check failed[/]: {exc}")
175
+ console.print("[dim]This may be a temporary network issue. The CLI can still work if the verifier is accessible.[/]")
176
+ checks_warning += 1
177
+ else:
178
+ bt_status = f"Error: {exc}"
179
+ console.print(f"[bold red]✗ Bittensor network check failed[/]: {exc}")
180
+ checks_failed += 1
181
+
182
+ results.append({
183
+ "name": "Bittensor Network",
184
+ "status": "pass" if bt_ok else ("warning" if checks_warning > 0 else "fail"),
185
+ "details": bt_status,
186
+ "latency_ms": bt_latency_ms,
187
+ "is_dns_error": bt_is_dns_error,
188
+ })
189
+
190
+ # Check 3: Configuration validation
191
+ console.print()
192
+ console.print(f"[bold]Checking configuration...[/]")
193
+
194
+ config_issues: list[str] = []
195
+
196
+ if not settings.verifier_url:
197
+ config_issues.append("Verifier URL is not set")
198
+ elif not settings.verifier_url.startswith(("http://", "https://")):
199
+ config_issues.append("Verifier URL must start with http:// or https://")
200
+
201
+ if not settings.network:
202
+ config_issues.append("Network is not set")
203
+
204
+ if settings.netuid <= 0:
205
+ config_issues.append(f"Invalid netuid: {settings.netuid}")
206
+
207
+ if config_issues:
208
+ console.print(f"[bold yellow]⚠ Configuration issues found[/]:")
209
+ for issue in config_issues:
210
+ console.print(f" • {issue}")
211
+ checks_warning += 1
212
+ config_status = "Issues found"
213
+ else:
214
+ console.print("[bold green]✓ Configuration is valid[/]")
215
+ checks_passed += 1
216
+ config_status = "Valid"
217
+
218
+ results.append({
219
+ "name": "Configuration",
220
+ "status": "pass" if not config_issues else "warning",
221
+ "details": config_status,
222
+ "issues": config_issues if config_issues else None,
223
+ })
224
+
225
+ # Check 4: Subnet metadata
226
+ console.print()
227
+ console.print(f"[bold]Checking subnet metadata...[/]")
228
+ console.print(f"[dim]NetUID: {settings.netuid}[/]")
229
+
230
+ subnet_ok = False
231
+ subnet_status = "Unknown"
232
+ subnet_latency_ms = None
233
+ subnet_info: dict[str, Any] = {}
234
+ subnet_is_dns_error = False
235
+
236
+ try:
237
+ start_time = time.time()
238
+ subtensor = bt.subtensor(network=settings.network)
239
+ subtensor_connections.append(subtensor)
240
+ metagraph = subtensor.metagraph(netuid=settings.netuid)
241
+ metagraph.sync(subtensor=subtensor)
242
+ latency_ms = int((time.time() - start_time) * 1000)
243
+ subnet_latency_ms = latency_ms
244
+
245
+ # Get subnet metadata
246
+ total_miners = len(metagraph.neurons) if hasattr(metagraph, "neurons") else 0
247
+ tempo = getattr(metagraph, "tempo", None)
248
+ block = getattr(metagraph, "block", None)
249
+
250
+ # Convert block to int if it's an array
251
+ if hasattr(block, "__iter__") and not isinstance(block, (str, int)):
252
+ try:
253
+ block = int(block[0]) if len(block) > 0 else None
254
+ except (ValueError, TypeError, IndexError):
255
+ block = None
256
+
257
+ subnet_info = {
258
+ "total_slots": total_miners,
259
+ "tempo": tempo,
260
+ "block": block,
261
+ }
262
+
263
+ subnet_status_parts = []
264
+ if total_miners > 0:
265
+ subnet_status_parts.append(f"{total_miners} registered slots")
266
+ if tempo is not None:
267
+ subnet_status_parts.append(f"tempo: {tempo}")
268
+ if block is not None:
269
+ subnet_status_parts.append(f"block: {block}")
270
+
271
+ subnet_status = ", ".join(subnet_status_parts) if subnet_status_parts else "Connected"
272
+ subnet_ok = True
273
+
274
+ console.print(f"[bold green]✓ Subnet metadata retrieved[/] ({latency_ms}ms)")
275
+ if verbose:
276
+ console.print(f" • Registered slots: {total_miners}")
277
+ if tempo is not None:
278
+ console.print(f" • Tempo: {tempo}")
279
+ if block is not None:
280
+ console.print(f" • Block: {block}")
281
+ checks_passed += 1
282
+ except OSError as exc:
283
+ # DNS resolution errors (Errno 8)
284
+ error_str = str(exc)
285
+ if "nodename nor servname provided" in error_str or "Errno 8" in error_str:
286
+ subnet_is_dns_error = True
287
+ subnet_status = "DNS resolution failed - cannot resolve Bittensor network endpoints"
288
+ console.print(f"[bold yellow]⚠ Subnet metadata check failed[/]: DNS resolution error")
289
+ checks_warning += 1 # Make it a warning since verifier is working
290
+ else:
291
+ subnet_status = f"Network error: {exc}"
292
+ console.print(f"[bold red]✗ Subnet metadata check failed[/]: {exc}")
293
+ checks_failed += 1
294
+ except Exception as exc:
295
+ error_str = str(exc)
296
+ # Check if it's a DNS/network related error
297
+ if any(keyword in error_str.lower() for keyword in ["dns", "resolve", "nodename", "servname", "network", "connection"]):
298
+ subnet_is_dns_error = True
299
+ subnet_status = f"Network connectivity issue: {exc}"
300
+ console.print(f"[bold yellow]⚠ Subnet metadata check failed[/]: {exc}")
301
+ checks_warning += 1
302
+ else:
303
+ subnet_status = f"Error: {exc}"
304
+ console.print(f"[bold red]✗ Subnet metadata check failed[/]: {exc}")
305
+ checks_failed += 1
306
+
307
+ results.append({
308
+ "name": "Subnet Metadata",
309
+ "status": "pass" if subnet_ok else ("warning" if checks_warning > 0 else "fail"),
310
+ "details": subnet_status,
311
+ "latency_ms": subnet_latency_ms,
312
+ "info": subnet_info if subnet_ok else None,
313
+ "is_dns_error": subnet_is_dns_error,
314
+ })
315
+
316
+ # Check 5: Environment Variables
317
+ console.print()
318
+ console.print(f"[bold]Checking Environment Variables...[/]")
319
+
320
+ env_vars_ok = True
321
+ env_status = "OK"
322
+ env_details: dict[str, dict[str, Any]] = {}
323
+
324
+ # Get default values from Settings model
325
+ default_settings = Settings()
326
+
327
+ # Check each configurable env var
328
+ env_var_mapping = {
329
+ "CARTHA_VERIFIER_URL": ("verifier_url", default_settings.verifier_url),
330
+ "CARTHA_NETWORK": ("network", default_settings.network),
331
+ "CARTHA_NETUID": ("netuid", str(default_settings.netuid)),
332
+ "CARTHA_EVM_PK": ("evm_private_key", None), # Sensitive, don't show value
333
+ "CARTHA_RETRY_MAX_ATTEMPTS": ("retry_max_attempts", str(default_settings.retry_max_attempts)),
334
+ "CARTHA_RETRY_BACKOFF_FACTOR": ("retry_backoff_factor", str(default_settings.retry_backoff_factor)),
335
+ }
336
+
337
+ for env_var, (setting_key, default_value) in env_var_mapping.items():
338
+ env_value = os.getenv(env_var)
339
+ is_set = env_value is not None
340
+
341
+ # For sensitive values, don't show the actual value
342
+ display_value = "[REDACTED]" if env_var == "CARTHA_EVM_PK" and is_set else env_value
343
+
344
+ env_details[env_var] = {
345
+ "is_set": is_set,
346
+ "value": display_value,
347
+ "default": default_value,
348
+ "source": "environment" if is_set else "default",
349
+ }
350
+
351
+ # Count how many are set vs defaults
352
+ set_count = sum(1 for details in env_details.values() if details["is_set"])
353
+ total_count = len(env_details)
354
+
355
+ env_status = f"{set_count}/{total_count} Environment Variables set"
356
+
357
+ console.print(f"[bold green]✓ Environment Variables checked[/] ({set_count}/{total_count} set)")
358
+ if verbose:
359
+ for env_var, details in env_details.items():
360
+ source_indicator = "[green]●[/]" if details["is_set"] else "[dim]○[/]"
361
+ console.print(f" {source_indicator} {env_var}: {details['value'] or details['default']} ({details['source']})")
362
+
363
+ checks_passed += 1
364
+
365
+ results.append({
366
+ "name": "Environment Variables",
367
+ "status": "pass",
368
+ "details": env_status,
369
+ "info": env_details if verbose else None,
370
+ })
371
+
372
+ # Summary
373
+ console.print()
374
+ console.print("[bold cyan]━━━ Summary ━━━[/]")
375
+
376
+ summary_table = Table(show_header=True, header_style="bold cyan")
377
+ summary_table.add_column("Check", style="cyan")
378
+ summary_table.add_column("Status", justify="center")
379
+ summary_table.add_column("Details", style="dim")
380
+ summary_table.add_column("Latency", justify="right", style="dim")
381
+
382
+ for result in results:
383
+ status_icon = {
384
+ "pass": "[bold green]✓[/]",
385
+ "warning": "[bold yellow]⚠[/]",
386
+ "fail": "[bold red]✗[/]",
387
+ }.get(result["status"], "?")
388
+
389
+ latency_str = f"{result['latency_ms']}ms" if result.get("latency_ms") else "-"
390
+ details = result["details"]
391
+ if result.get("issues"):
392
+ details += f" ({len(result['issues'])} issue(s))"
393
+
394
+ summary_table.add_row(
395
+ result["name"],
396
+ status_icon,
397
+ details,
398
+ latency_str,
399
+ )
400
+
401
+ console.print(summary_table)
402
+ console.print()
403
+
404
+ # Overall status
405
+ total_checks = checks_passed + checks_failed + checks_warning
406
+ if checks_failed == 0 and checks_warning == 0:
407
+ console.print("[bold green]✓ All checks passed![/] CLI is ready to use.")
408
+ raise typer.Exit(code=0)
409
+ elif checks_failed == 0:
410
+ # Check if warnings are DNS-related
411
+ dns_warnings = [r for r in results if r.get("is_dns_error") and r.get("status") == "warning"]
412
+ if dns_warnings:
413
+ console.print(
414
+ f"[bold yellow]⚠ {checks_warning} warning(s) found[/] (including DNS resolution issues). "
415
+ "CLI should work for most commands, but Bittensor network features may be limited."
416
+ )
417
+ console.print("[dim]If you need to register or check miner status, ensure Bittensor network connectivity.[/]")
418
+ else:
419
+ console.print(
420
+ f"[bold yellow]⚠ {checks_warning} warning(s) found[/], but CLI should work. "
421
+ "Review configuration if needed."
422
+ )
423
+ raise typer.Exit(code=0)
424
+ else:
425
+ console.print(
426
+ f"[bold red]✗ {checks_failed} check(s) failed[/], {checks_warning} warning(s). "
427
+ "Please fix issues before using the CLI."
428
+ )
429
+ if verbose:
430
+ console.print("\n[bold]Troubleshooting:[/]")
431
+ console.print("• Check your network connectivity")
432
+ console.print(f"• Verify verifier URL: {settings.verifier_url}")
433
+ console.print(f"• Verify Bittensor network: {settings.network}")
434
+ console.print("• Check environment variables: CARTHA_VERIFIER_URL, CARTHA_NETWORK, CARTHA_NETUID")
435
+
436
+ # Check if DNS errors occurred
437
+ dns_errors = [r for r in results if r.get("is_dns_error")]
438
+ if dns_errors:
439
+ console.print("\n[bold yellow]DNS Resolution Issues Detected:[/]")
440
+ console.print("The following checks failed due to DNS resolution errors:")
441
+ for result in dns_errors:
442
+ console.print(f" • {result['name']}: {result['details']}")
443
+ console.print("\n[bold]DNS Troubleshooting Steps:[/]")
444
+ console.print(" 1. Check your internet connection")
445
+ console.print(" 2. Try flushing DNS cache:")
446
+ console.print(" • macOS: sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder")
447
+ console.print(" • Linux: sudo systemd-resolve --flush-caches (or sudo resolvectl flush-caches)")
448
+ console.print(" • Windows: ipconfig /flushdns")
449
+ console.print(" 3. Check if you're behind a firewall or proxy")
450
+ console.print(" 4. Try using a different network (e.g., mobile hotspot)")
451
+ console.print(" 5. Check if DNS servers are reachable: nslookup <domain>")
452
+ console.print("\n[dim]Note: If the verifier check passed, you can still use most CLI commands.[/]")
453
+ console.print("[dim]Bittensor network connectivity is only needed for registration and status checks.[/]")
454
+ raise typer.Exit(code=1)
455
+ finally:
456
+ # Clean up subtensor connections
457
+ for subtensor in subtensor_connections:
458
+ try:
459
+ if hasattr(subtensor, "close"):
460
+ subtensor.close()
461
+ except Exception:
462
+ pass
463
+
@@ -0,0 +1,49 @@
1
+ """Help and root command utilities."""
2
+
3
+ from rich import box
4
+ from rich.console import Console
5
+ from rich.rule import Rule
6
+ from rich.table import Table
7
+
8
+ from ..display import get_clock_table
9
+ from .common import console
10
+
11
+
12
+ def print_root_help() -> None:
13
+ """Print the root help message."""
14
+ console.print(Rule("[bold cyan]Cartha CLI[/]"))
15
+ console.print(
16
+ "Miner-facing command line tool for Cartha subnet miners.\n"
17
+ "Cartha is the Liquidity Provider for 0xMarkets DEX.\n"
18
+ "Register on the subnet, manage lock positions, and track your mining status."
19
+ )
20
+ console.print()
21
+ console.print("[bold]Usage[/]: cartha [OPTIONS] COMMAND [ARGS]...")
22
+ console.print()
23
+
24
+ options = Table(title="Options", box=box.SQUARE_DOUBLE_HEAD, show_header=False)
25
+ options.add_row("[cyan]-h[/], [cyan]--help[/]", "Show this message and exit.")
26
+ console.print(options)
27
+ console.print()
28
+
29
+ commands = Table(title="Commands", box=box.SQUARE_DOUBLE_HEAD, show_header=False)
30
+ commands.add_row("[green]help[/]", "Show this help message.")
31
+ commands.add_row("[green]version[/]", "Show CLI version.")
32
+ commands.add_row(
33
+ "[green]miner[/] [dim](or [green]m[/])[/]", "Miner management commands."
34
+ )
35
+ commands.add_row(
36
+ "[green]vault[/] [dim](or [green]v[/])[/]", "Vault management commands."
37
+ )
38
+ commands.add_row(
39
+ "[green]utils[/] [dim](or [green]u[/])[/]", "Utility commands: health checks and configuration."
40
+ )
41
+ console.print(commands)
42
+ console.print()
43
+
44
+ # Display clock and countdown in a separate table
45
+ clock_table = get_clock_table()
46
+ console.print(clock_table)
47
+ console.print()
48
+
49
+ console.print("[dim]Made with ❤ by GTV[/]")