hypercli-cli 0.4.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.
c3cli/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
c3cli/billing.py ADDED
@@ -0,0 +1,60 @@
1
+ """c3 billing commands"""
2
+ import typer
3
+ from c3 import C3
4
+ from .output import output, console, spinner
5
+
6
+ app = typer.Typer(help="Billing and balance commands")
7
+
8
+
9
+ def get_client() -> C3:
10
+ return C3()
11
+
12
+
13
+ @app.command("balance")
14
+ def balance(
15
+ fmt: str = typer.Option("table", "--output", "-o", help="Output format: table|json"),
16
+ ):
17
+ """Get account balance"""
18
+ c3 = get_client()
19
+ with spinner("Fetching balance..."):
20
+ bal = c3.billing.balance()
21
+
22
+ if fmt == "table":
23
+ console.print()
24
+ console.print("[bold]Account Balance[/bold]")
25
+ console.print()
26
+ console.print(f" Balance: [bold green]${bal.total}[/bold green]")
27
+ console.print(f" Available: ${bal.available}")
28
+ if bal.rewards != "0.000000" and bal.rewards != "0":
29
+ console.print(f" [dim](Rewards: ${bal.rewards})[/dim]")
30
+ console.print()
31
+ else:
32
+ output(bal, fmt)
33
+
34
+
35
+ @app.command("transactions")
36
+ def transactions(
37
+ limit: int = typer.Option(20, "--limit", "-n", help="Number of transactions"),
38
+ page: int = typer.Option(1, "--page", "-p", help="Page number"),
39
+ fmt: str = typer.Option("table", "--output", "-o", help="Output format: table|json"),
40
+ ):
41
+ """List transactions"""
42
+ c3 = get_client()
43
+ with spinner("Fetching transactions..."):
44
+ txs = c3.billing.transactions(limit=limit, page=page)
45
+
46
+ if fmt == "json":
47
+ output(txs, fmt)
48
+ else:
49
+ output(txs, "table", ["id", "transaction_type", "amount_usd", "status", "created_at"])
50
+
51
+
52
+ @app.command("invoices")
53
+ def invoices(
54
+ limit: int = typer.Option(20, "--limit", "-n", help="Number of invoices"),
55
+ fmt: str = typer.Option("table", "--output", "-o", help="Output format: table|json"),
56
+ ):
57
+ """List invoices"""
58
+ c3 = get_client()
59
+ # TODO: Add invoices to SDK
60
+ console.print("[dim]Invoices endpoint not yet implemented in SDK[/dim]")
c3cli/cli.py ADDED
@@ -0,0 +1,183 @@
1
+ """C3 CLI - Main entry point"""
2
+ import sys
3
+ import typer
4
+ from rich.console import Console
5
+ from rich.prompt import Prompt
6
+
7
+ from c3 import C3, APIError, configure
8
+ from c3.config import CONFIG_FILE
9
+
10
+ from . import billing, comfyui, instances, jobs, llm, renders, user
11
+
12
+ console = Console()
13
+
14
+
15
+ def fuzzy_match(input_str: str, options: list[str], threshold: float = 0.5) -> list[str]:
16
+ """Find similar strings using multiple heuristics"""
17
+ def similarity(a: str, b: str) -> float:
18
+ a, b = a.lower(), b.lower()
19
+ if a == b:
20
+ return 1.0
21
+
22
+ # Exact substring match
23
+ if a in b or b in a:
24
+ return 0.9
25
+
26
+ # Same characters (handles transpositions like rtx6000pro vs rtxpro6000)
27
+ if sorted(a) == sorted(b):
28
+ return 0.95
29
+
30
+ # Character set overlap
31
+ set_a, set_b = set(a), set(b)
32
+ common = set_a & set_b
33
+ jaccard = len(common) / len(set_a | set_b) if set_a | set_b else 0
34
+
35
+ # Prefix match bonus
36
+ prefix_len = 0
37
+ for ca, cb in zip(a, b):
38
+ if ca == cb:
39
+ prefix_len += 1
40
+ else:
41
+ break
42
+ prefix_bonus = prefix_len / max(len(a), len(b)) * 0.3
43
+
44
+ return jaccard + prefix_bonus
45
+
46
+ matches = [(opt, similarity(input_str, opt)) for opt in options]
47
+ matches = [(opt, score) for opt, score in matches if score >= threshold]
48
+ matches.sort(key=lambda x: x[1], reverse=True)
49
+ return [opt for opt, _ in matches[:3]]
50
+
51
+ app = typer.Typer(
52
+ name="c3",
53
+ help="HyperCLI CLI - GPU orchestration and LLM API",
54
+ no_args_is_help=True,
55
+ rich_markup_mode="rich",
56
+ )
57
+
58
+ # Register subcommands
59
+ app.add_typer(billing.app, name="billing")
60
+ app.add_typer(comfyui.app, name="comfyui")
61
+ app.add_typer(instances.app, name="instances")
62
+ app.add_typer(jobs.app, name="jobs")
63
+ app.add_typer(llm.app, name="llm")
64
+ app.add_typer(renders.app, name="renders")
65
+ app.add_typer(user.app, name="user")
66
+
67
+
68
+ @app.command("configure")
69
+ def configure_cmd():
70
+ """Configure C3 CLI with your API key and API URL"""
71
+ import getpass
72
+ from c3.config import get_api_key, get_api_url, DEFAULT_API_URL
73
+
74
+ console.print("\n[bold cyan]C3 CLI Configuration[/bold cyan]\n")
75
+
76
+ # Show current config
77
+ current_key = get_api_key()
78
+ current_url = get_api_url()
79
+
80
+ if current_key:
81
+ key_preview = current_key[:4] + "..." + current_key[-4:] if len(current_key) > 8 else "****"
82
+ console.print(f"Current API key: [dim]{key_preview}[/dim]")
83
+ if current_url and current_url != DEFAULT_API_URL:
84
+ console.print(f"Current API URL: [dim]{current_url}[/dim]")
85
+
86
+ console.print()
87
+ console.print("Get your API key at [link=https://hypercli.com/dashboard]hypercli.com/dashboard[/link]\n")
88
+
89
+ # API Key
90
+ api_key = getpass.getpass("API key (enter to keep current): ") if current_key else getpass.getpass("API key: ")
91
+ api_key = api_key.strip() if api_key else None
92
+
93
+ if not api_key and not current_key:
94
+ console.print("[red]No API key provided[/red]")
95
+ raise typer.Exit(1)
96
+
97
+ # API URL
98
+ url_prompt = f"API URL (enter for default, current: {current_url}): " if current_url != DEFAULT_API_URL else "API URL (enter for default): "
99
+ api_url = Prompt.ask(url_prompt, default="")
100
+ api_url = api_url.strip() if api_url else None
101
+
102
+ # Only update what changed
103
+ final_key = api_key or current_key
104
+ final_url = api_url if api_url else (current_url if current_url != DEFAULT_API_URL else None)
105
+
106
+ configure(final_key, final_url)
107
+
108
+ console.print(f"\n[green]✓[/green] Config saved to {CONFIG_FILE}")
109
+ if api_key:
110
+ preview = api_key[:4] + "..." + api_key[-4:] if len(api_key) > 8 else "****"
111
+ console.print(f" API key: {preview}")
112
+ if final_url:
113
+ console.print(f" API URL: {final_url}")
114
+ console.print("\nTest your setup with: [cyan]c3 billing balance[/cyan]\n")
115
+
116
+
117
+ @app.callback()
118
+ def main(
119
+ version: bool = typer.Option(False, "--version", "-v", help="Show version"),
120
+ ):
121
+ """
122
+ [bold cyan]C3 CLI[/bold cyan] - HyperCLI GPU orchestration and LLM API
123
+
124
+ Set your API key: [green]c3 configure[/green]
125
+
126
+ Get started:
127
+ c3 instances list Browse available GPUs
128
+ c3 instances launch Launch a GPU instance
129
+ c3 jobs list View your running jobs
130
+ c3 llm chat -i Start a chat
131
+ """
132
+ if version:
133
+ from . import __version__
134
+ console.print(f"c3 version {__version__}")
135
+ raise typer.Exit()
136
+
137
+
138
+ def cli():
139
+ """Entry point with error handling"""
140
+ try:
141
+ app()
142
+ except APIError as e:
143
+ detail = e.detail or str(e)
144
+
145
+ # Check for GPU type errors and suggest corrections
146
+ if "GPU type" in detail and "not found" in detail and "Available:" in detail:
147
+ # Extract the invalid GPU type and available options
148
+ import re
149
+ match = re.search(r"GPU type '([^']+)' not found\. Available: \[([^\]]+)\]", detail)
150
+ if match:
151
+ invalid_type = match.group(1)
152
+ available_str = match.group(2)
153
+ available = [s.strip().strip("'") for s in available_str.split(",")]
154
+
155
+ console.print(f"[bold red]Error:[/bold red] Unknown GPU type '[yellow]{invalid_type}[/yellow]'")
156
+
157
+ # Find similar GPU types
158
+ suggestions = fuzzy_match(invalid_type, available)
159
+ if suggestions:
160
+ console.print(f"\n[dim]Did you mean:[/dim]")
161
+ for s in suggestions:
162
+ console.print(f" [green]{s}[/green]")
163
+
164
+ console.print(f"\n[dim]Available GPU types:[/dim] {', '.join(available)}")
165
+ sys.exit(1)
166
+
167
+ # Check for region errors
168
+ if "region" in detail.lower() and "not found" in detail.lower():
169
+ console.print(f"[bold red]Error:[/bold red] {detail}")
170
+ console.print("\n[dim]Tip: Use 'c3 jobs regions' to see available regions[/dim]")
171
+ sys.exit(1)
172
+
173
+ # Generic API error
174
+ console.print(f"[bold red]API Error ({e.status_code}):[/bold red] {detail}")
175
+ sys.exit(1)
176
+
177
+ except KeyboardInterrupt:
178
+ console.print("\n[dim]Interrupted[/dim]")
179
+ sys.exit(130)
180
+
181
+
182
+ if __name__ == "__main__":
183
+ cli()