blockmachine 0.3.0__tar.gz → 0.3.2__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: blockmachine
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: Blockmachine CLI - Miner and validator operator interface
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/taostat/blockmachine
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: blockmachine
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: Blockmachine CLI - Miner and validator operator interface
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/taostat/blockmachine
@@ -8,7 +8,7 @@ import logging
8
8
  import socket
9
9
  import ssl
10
10
  import time
11
- from typing import Optional
11
+ from typing import Callable, Optional
12
12
  from urllib.parse import urlparse
13
13
 
14
14
  import httpx
@@ -116,6 +116,17 @@ def _test_tls(hostname: str, port: int) -> tuple[bool, Optional[str], str]:
116
116
  return False, None, str(e)
117
117
 
118
118
 
119
+ def _bracket_ipv6(hostname: str) -> str:
120
+ """Wrap bare IPv6 addresses in brackets for use in URLs."""
121
+ try:
122
+ addr = ipaddress.ip_address(hostname)
123
+ if addr.version == 6:
124
+ return f"[{hostname}]"
125
+ except ValueError:
126
+ pass
127
+ return hostname
128
+
129
+
119
130
  def _is_ip_endpoint(endpoint: str) -> bool:
120
131
  """Return True if the endpoint hostname is an IP address (not a domain)."""
121
132
  hostname = urlparse(endpoint).hostname
@@ -344,23 +355,100 @@ def show(
344
355
  node_id = _resolve_node(client, alias)
345
356
  response = client.get(f"/nodes/{node_id}")
346
357
 
347
- if response.status_code == 404:
348
- console.print(f"[red]Node not found:[/red] {alias}")
349
- raise typer.Exit(1)
350
- if response.status_code != 200:
351
- console.print(f"[red]Error:[/red] {response.status_code} - {response.text}")
352
- raise typer.Exit(1)
358
+ if response.status_code == 404:
359
+ console.print(f"[red]Node not found:[/red] {alias}")
360
+ raise typer.Exit(1)
361
+ if response.status_code != 200:
362
+ console.print(f"[red]Error:[/red] {response.status_code} - {response.text}")
363
+ raise typer.Exit(1)
353
364
 
354
- node = response.json()
355
- console.print(f"[bold]ID:[/bold] {node['id']}")
356
- console.print(f"[bold]Alias:[/bold] {node.get('alias') or '-'}")
357
- console.print(f"[bold]Chain:[/bold] {node['chain']}")
358
- console.print(f"[bold]Status:[/bold] {node['status']}")
359
- console.print(f"[bold]Endpoint:[/bold] {node['endpoint']}")
360
- console.print(f"[bold]Created:[/bold] {format_timestamp(node['created_at'])}")
361
- console.print(
362
- f"[bold]Last Seen:[/bold] {format_timestamp(node.get('last_seen_at'))}"
363
- )
365
+ node = response.json()
366
+ console.print(f"[bold]ID:[/bold] {node['id']}")
367
+ console.print(f"[bold]Alias:[/bold] {node.get('alias') or '-'}")
368
+ console.print(f"[bold]Chain:[/bold] {node['chain']}")
369
+ console.print(f"[bold]Status:[/bold] {node['status']}")
370
+ console.print(f"[bold]Endpoint:[/bold] {node['endpoint']}")
371
+ console.print(f"[bold]Created:[/bold] {format_timestamp(node['created_at'])}")
372
+ console.print(
373
+ f"[bold]Last Seen:[/bold] {format_timestamp(node.get('last_seen_at'))}"
374
+ )
375
+
376
+ m_resp = client.get(f"/nodes/{node_id}/metrics")
377
+ if m_resp.status_code == 200:
378
+ m = m_resp.json()
379
+ if m.get("available"):
380
+ console.print()
381
+ console.print("[bold]Routing Metrics[/bold]")
382
+ _print_metrics(m)
383
+
384
+
385
+ def _fmt_success_rate(v: float) -> str:
386
+ pct = v * 100
387
+ color = "green" if pct >= 99 else "yellow" if pct >= 95 else "red"
388
+ return f"[{color}]{pct:.1f}%[/{color}]"
389
+
390
+
391
+ _METRIC_FIELDS: list[tuple[str, str, Callable]] = [
392
+ ("request_count", "Requests", lambda v: f"{v:,}"),
393
+ ("success_rate", "Success Rate", _fmt_success_rate),
394
+ ("p95_ms", "P95 Latency", lambda v: f"{v:.0f} ms"),
395
+ ("latency_score", "Latency Score", lambda v: f"{v:.4f}"),
396
+ ("routing_score", "Routing Score", lambda v: f"{v:.4f}"),
397
+ ("traffic_pct", "Traffic Share", lambda v: f"{v:.1f}%"),
398
+ ("reliability", "Reliability", lambda v: f"{v:.4f}"),
399
+ ("epochs_good", "Epochs Good", str),
400
+ ("bid", "Bid", lambda v: f"${v:.6f}"),
401
+ ("cohort_median", "Cohort Median", lambda v: f"${v:.6f}"),
402
+ ("price_factor", "Price Factor", lambda v: f"{v:.4f}"),
403
+ ]
404
+
405
+
406
+ def _print_metrics(m: dict) -> None:
407
+ """Print routing metrics from a metrics API response."""
408
+ table = Table(show_header=False, box=None, padding=(0, 2))
409
+ table.add_column("Key", style="bold")
410
+ table.add_column("Value")
411
+
412
+ for key, label, fmt in _METRIC_FIELDS:
413
+ val = m.get(key)
414
+ if val is not None:
415
+ table.add_row(label, fmt(val))
416
+
417
+ console.print(table)
418
+
419
+
420
+ @app.command("metrics")
421
+ def metrics(
422
+ alias: str = typer.Argument(None, help="Node alias or ID"),
423
+ ) -> None:
424
+ """Show routing metrics for a miner node."""
425
+ alias = _get_alias(alias)
426
+ config = load_config()
427
+
428
+ with RegistryClient(config) as client:
429
+ node_id = _resolve_node(client, alias)
430
+ response = client.get(f"/nodes/{node_id}/metrics")
431
+
432
+ if response.status_code == 503:
433
+ console.print(
434
+ "[yellow]Metrics not available (scoring not enabled)[/yellow]"
435
+ )
436
+ raise typer.Exit(1)
437
+ if response.status_code != 200:
438
+ console.print(f"[red]Error:[/red] {response.status_code} - {response.text}")
439
+ raise typer.Exit(1)
440
+
441
+ m = response.json()
442
+ if not m.get("available"):
443
+ console.print(
444
+ f"[yellow]No metrics available for {alias}[/yellow]\n"
445
+ "[dim]The gateway may not have routed traffic to this"
446
+ " node yet.[/dim]"
447
+ )
448
+ return
449
+
450
+ console.print(f"[bold]Routing Metrics: {alias}[/bold]\n")
451
+ _print_metrics(m)
364
452
 
365
453
 
366
454
  @app.command("rm")
@@ -443,7 +531,7 @@ def _test_health(hostname: str) -> tuple[bool, str]:
443
531
  """Test HTTP health endpoint on port 80."""
444
532
  try:
445
533
  r = httpx.get(
446
- f"http://{hostname}/health",
534
+ f"http://{_bracket_ipv6(hostname)}/health",
447
535
  timeout=10,
448
536
  follow_redirects=True,
449
537
  )
@@ -458,7 +546,7 @@ def _test_rpc(
458
546
  hostname: str, port: int, secret: str
459
547
  ) -> tuple[bool, Optional[float], str]:
460
548
  """Send system_health RPC over HTTPS. Return (ok, latency_ms, message)."""
461
- url = f"https://{hostname}:{port}"
549
+ url = f"https://{_bracket_ipv6(hostname)}:{port}"
462
550
  payload = {
463
551
  "jsonrpc": "2.0",
464
552
  "method": "system_health",
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "blockmachine"
7
- version = "0.3.0"
7
+ version = "0.3.2"
8
8
  description = "Blockmachine CLI - Miner and validator operator interface"
9
9
  requires-python = ">=3.10"
10
10
  license = "MIT"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes