nepher-cli 0.2.5__tar.gz → 0.2.6__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.
Files changed (27) hide show
  1. {nepher_cli-0.2.5 → nepher_cli-0.2.6}/PKG-INFO +1 -1
  2. {nepher_cli-0.2.5 → nepher_cli-0.2.6}/pyproject.toml +1 -1
  3. {nepher_cli-0.2.5 → nepher_cli-0.2.6}/src/nepher_cli/commands/account.py +73 -1
  4. {nepher_cli-0.2.5 → nepher_cli-0.2.6}/src/nepher_cli/commands/tournament.py +38 -9
  5. {nepher_cli-0.2.5 → nepher_cli-0.2.6}/src/nepher_cli/tournament/api.py +20 -3
  6. {nepher_cli-0.2.5 → nepher_cli-0.2.6}/.gitignore +0 -0
  7. {nepher_cli-0.2.5 → nepher_cli-0.2.6}/README.md +0 -0
  8. {nepher_cli-0.2.5 → nepher_cli-0.2.6}/src/nepher_cli/__init__.py +0 -0
  9. {nepher_cli-0.2.5 → nepher_cli-0.2.6}/src/nepher_cli/__main__.py +0 -0
  10. {nepher_cli-0.2.5 → nepher_cli-0.2.6}/src/nepher_cli/cli.py +0 -0
  11. {nepher_cli-0.2.5 → nepher_cli-0.2.6}/src/nepher_cli/commands/__init__.py +0 -0
  12. {nepher_cli-0.2.5 → nepher_cli-0.2.6}/src/nepher_cli/commands/envhub.py +0 -0
  13. {nepher_cli-0.2.5 → nepher_cli-0.2.6}/src/nepher_cli/commands/hackathon.py +0 -0
  14. {nepher_cli-0.2.5 → nepher_cli-0.2.6}/src/nepher_cli/commands/highlight.py +0 -0
  15. {nepher_cli-0.2.5 → nepher_cli-0.2.6}/src/nepher_cli/commands/simstore.py +0 -0
  16. {nepher_cli-0.2.5 → nepher_cli-0.2.6}/src/nepher_cli/config.py +0 -0
  17. {nepher_cli-0.2.5 → nepher_cli-0.2.6}/src/nepher_cli/core/__init__.py +0 -0
  18. {nepher_cli-0.2.5 → nepher_cli-0.2.6}/src/nepher_cli/core/credentials.py +0 -0
  19. {nepher_cli-0.2.5 → nepher_cli-0.2.6}/src/nepher_cli/core/http.py +0 -0
  20. {nepher_cli-0.2.5 → nepher_cli-0.2.6}/src/nepher_cli/envhub/__init__.py +0 -0
  21. {nepher_cli-0.2.5 → nepher_cli-0.2.6}/src/nepher_cli/envhub/cache.py +0 -0
  22. {nepher_cli-0.2.5 → nepher_cli-0.2.6}/src/nepher_cli/envhub/config.py +0 -0
  23. {nepher_cli-0.2.5 → nepher_cli-0.2.6}/src/nepher_cli/py.typed +0 -0
  24. {nepher_cli-0.2.5 → nepher_cli-0.2.6}/src/nepher_cli/tournament/__init__.py +0 -0
  25. {nepher_cli-0.2.5 → nepher_cli-0.2.6}/src/nepher_cli/tournament/agent_check.py +0 -0
  26. {nepher_cli-0.2.5 → nepher_cli-0.2.6}/src/nepher_cli/tournament/packer.py +0 -0
  27. {nepher_cli-0.2.5 → nepher_cli-0.2.6}/src/nepher_cli/tournament/wallet.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nepher-cli
3
- Version: 0.2.5
3
+ Version: 0.2.6
4
4
  Summary: npcli — unified Nepher command-line interface for accounts, hackathons, EnvHub, tournaments, and SimStore
5
5
  Project-URL: Homepage, https://github.com/nepher-ai/nepher-cli
6
6
  Author: Nepher
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "nepher-cli"
7
- version = "0.2.5"
7
+ version = "0.2.6"
8
8
  description = "npcli — unified Nepher command-line interface for accounts, hackathons, EnvHub, tournaments, and SimStore"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -396,7 +396,7 @@ def register_coldkey(wallet: str, api_key: str, base_url: str) -> int:
396
396
 
397
397
  @click.group("account")
398
398
  def account() -> None:
399
- """Manage your Nepher account — login, API keys, and coldkey registration."""
399
+ """Manage your Nepher account — login, API keys, coldkey, and platform balance."""
400
400
 
401
401
 
402
402
  # ── Auth ────────────────────────────────────────────────────────────────────
@@ -627,6 +627,78 @@ def api_keys_revoke(key_id: str, api_key: str | None) -> None:
627
627
  # ── Coldkey ──────────────────────────────────────────────────────────────────
628
628
 
629
629
 
630
+ @account.command("balance")
631
+ @click.option(
632
+ "--api-key", "--apikey", "api_key",
633
+ default=None, envvar="NEPHER_API_KEY", metavar="KEY",
634
+ help="Nepher API key (nepher_...). Falls back to stored credentials.",
635
+ )
636
+ def cmd_balance(api_key: str | None) -> None:
637
+ """Show platform balance used for submission fees and unlocks."""
638
+ headers = _require_auth(api_key)
639
+ url = f"{ACCOUNT_BACKEND.rstrip('/')}/api/v1/billing/balance"
640
+ try:
641
+ r = httpx.get(url, headers=headers, timeout=30.0)
642
+ except httpx.RequestError as e:
643
+ console.print(f"[red]Network error[/red]: {e}")
644
+ raise SystemExit(1) from e
645
+ if r.status_code >= 400:
646
+ console.print(f"[red]{parse_error_body(r.text) or r.text.strip() or f'HTTP {r.status_code}'}[/red]")
647
+ raise SystemExit(1)
648
+ body = r.json()
649
+ console.print(f" Balance : [bold]{body.get('balance_tao', 0)} TAO[/bold]")
650
+ if body.get("coldkey"):
651
+ console.print(f" Coldkey : {body['coldkey']}")
652
+ else:
653
+ console.print(" Coldkey : [yellow]not registered[/yellow] — run npcli account register-coldkey")
654
+ if body.get("network"):
655
+ console.print(f" Network : {body['network']}")
656
+ console.print(
657
+ " Deposit : "
658
+ + ("available via Account → Billing on the website" if body.get("deposit_available") else "unavailable")
659
+ )
660
+
661
+
662
+ @account.command("ledger")
663
+ @click.option("--limit", default=20, show_default=True, help="Number of ledger rows.")
664
+ @click.option(
665
+ "--api-key", "--apikey", "api_key",
666
+ default=None, envvar="NEPHER_API_KEY", metavar="KEY",
667
+ help="Nepher API key (nepher_...). Falls back to stored credentials.",
668
+ )
669
+ def cmd_ledger(limit: int, api_key: str | None) -> None:
670
+ """Show recent platform billing history (deposits, fees, unlocks)."""
671
+ headers = _require_auth(api_key)
672
+ url = f"{ACCOUNT_BACKEND.rstrip('/')}/api/v1/billing/ledger"
673
+ try:
674
+ r = httpx.get(url, headers=headers, params={"limit": limit}, timeout=30.0)
675
+ except httpx.RequestError as e:
676
+ console.print(f"[red]Network error[/red]: {e}")
677
+ raise SystemExit(1) from e
678
+ if r.status_code >= 400:
679
+ console.print(f"[red]{parse_error_body(r.text) or r.text.strip() or f'HTTP {r.status_code}'}[/red]")
680
+ raise SystemExit(1)
681
+ items = (r.json() or {}).get("items") or []
682
+ if not items:
683
+ console.print("[dim]No billing activity yet.[/dim]")
684
+ return
685
+ table = Table(show_header=True, header_style="bold")
686
+ table.add_column("When")
687
+ table.add_column("Type")
688
+ table.add_column("Amount (TAO)", justify="right")
689
+ table.add_column("Balance after", justify="right")
690
+ table.add_column("Details")
691
+ for row in items:
692
+ table.add_row(
693
+ str(row.get("created_at") or "—"),
694
+ str(row.get("entry_type") or "—"),
695
+ f"{float(row.get('amount_tao') or 0):.6f}",
696
+ f"{float(row.get('balance_after_tao') or 0):.6f}",
697
+ str(row.get("description") or "—"),
698
+ )
699
+ console.print(table)
700
+
701
+
630
702
  @account.command("register-coldkey")
631
703
  @click.option("--wallet", required=True, metavar="NAME", help="Bittensor wallet name. Must exist in your local btcli wallet.")
632
704
  @click.option(
@@ -582,16 +582,43 @@ def tournament_submit(
582
582
  resolved_tournament_id = token_data["tournament_id"]
583
583
  upload_token = token_data["upload_token"]
584
584
 
585
+ fee = token_data.get("submission_fee_tao")
586
+ if fee is not None:
587
+ console.print(
588
+ f"Submission fee: [bold]{fee} TAO[/bold] "
589
+ f"(debited from platform balance on upload)"
590
+ )
591
+ fee_min = token_data.get("submission_fee_min_tao")
592
+ fee_max = token_data.get("submission_fee_max_tao")
593
+ fee_dur = token_data.get("submission_fee_duration_hours")
594
+ if fee_min is not None and fee_max is not None:
595
+ console.print(
596
+ f" [dim]Fee curve: {fee_max} → {fee_min} TAO"
597
+ + (f" over {fee_dur}h" if fee_dur is not None else "")
598
+ + "[/dim]"
599
+ )
600
+
585
601
  console.print("Uploading agent...")
586
- agent_id = await tournament_api.upload_agent(
587
- api_key=resolved_key,
588
- api_url=resolved_url,
589
- tournament_id=resolved_tournament_id,
590
- upload_token=upload_token,
591
- miner_hotkey=miner_hotkey,
592
- content_hash=content_hash,
593
- file_path=archive,
594
- )
602
+ try:
603
+ upload_result = await tournament_api.upload_agent(
604
+ api_key=resolved_key,
605
+ api_url=resolved_url,
606
+ tournament_id=resolved_tournament_id,
607
+ upload_token=upload_token,
608
+ miner_hotkey=miner_hotkey,
609
+ content_hash=content_hash,
610
+ file_path=archive,
611
+ )
612
+ except tournament_api.UploadChargeError as e:
613
+ console.print(f"[red]Submission fee failed:[/red] {e}")
614
+ console.print(
615
+ " Check balance with [bold]npcli account balance[/bold], "
616
+ "then deposit TAO on Account → Billing."
617
+ )
618
+ return 1
619
+
620
+ agent_id = upload_result["agent_id"]
621
+ charged = upload_result.get("submission_fee_tao")
595
622
 
596
623
  t_title = _tournament_title(token_data)
597
624
  t_task = _tournament_task_name(token_data)
@@ -600,6 +627,8 @@ def tournament_submit(
600
627
  console.print("[green]Agent submitted successfully.[/green]")
601
628
  console.print(f" Agent ID : [bold]{agent_id}[/bold]")
602
629
  console.print(f" Tournament ID: [bold]{resolved_tournament_id}[/bold]")
630
+ if charged is not None:
631
+ console.print(f" Fee charged : [bold]{charged} TAO[/bold]")
603
632
  if t_title and t_title != "—":
604
633
  console.print(f" Title : {t_title}")
605
634
  if t_task and t_task != "—":
@@ -21,6 +21,14 @@ def _json_headers(api_key: str) -> dict[str, str]:
21
21
  }
22
22
 
23
23
 
24
+ class UploadChargeError(RuntimeError):
25
+ """Raised when upload fails due to payment / platform balance."""
26
+
27
+ def __init__(self, message: str, *, status_code: int = 402):
28
+ super().__init__(message)
29
+ self.status_code = status_code
30
+
31
+
24
32
  def _raise_for_error(response: httpx.Response) -> None:
25
33
  if response.status_code < 400:
26
34
  return
@@ -29,6 +37,8 @@ def _raise_for_error(response: httpx.Response) -> None:
29
37
  or response.text.strip()
30
38
  or f"HTTP {response.status_code}"
31
39
  )
40
+ if response.status_code == 402:
41
+ raise UploadChargeError(msg, status_code=402)
32
42
  raise RuntimeError(msg)
33
43
 
34
44
 
@@ -76,8 +86,11 @@ async def upload_agent(
76
86
  miner_hotkey: str,
77
87
  content_hash: str,
78
88
  file_path: Path,
79
- ) -> str:
80
- """POST /api/v1/agents/upload/{tournament_id} (multipart) → agent_id string."""
89
+ ) -> dict[str, Any]:
90
+ """POST /api/v1/agents/upload/{tournament_id} (multipart) → response dict.
91
+
92
+ Returns at least ``agent_id``; may include ``submission_fee_tao``.
93
+ """
81
94
  url = f"{api_url.rstrip('/')}/api/v1/agents/upload/{tournament_id}"
82
95
  headers = {
83
96
  "X-API-Key": api_key,
@@ -97,4 +110,8 @@ async def upload_agent(
97
110
  agent_id = data.get("id") or data.get("agent_id")
98
111
  if not agent_id:
99
112
  raise RuntimeError(f"No agent ID in upload response: {data}")
100
- return str(agent_id)
113
+ return {
114
+ "agent_id": str(agent_id),
115
+ "submission_fee_tao": data.get("submission_fee_tao"),
116
+ "raw": data,
117
+ }
File without changes
File without changes