voltagegpu-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.
volt/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ """VoltageGPU - GPU Cloud Computing CLI and SDK."""
2
+
3
+ from volt.sdk import VoltageGPUClient, Config
4
+
5
+ __all__ = ["VoltageGPUClient", "Config"]
6
+ __version__ = "1.0.0"
volt/cli/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """VoltageGPU CLI - Command line interface for VoltageGPU."""
2
+
3
+ __all__ = []
volt/cli/cli.py ADDED
@@ -0,0 +1,417 @@
1
+ """VoltageGPU CLI - Command-line interface for managing GPU cloud resources."""
2
+
3
+ import click
4
+ import json
5
+ import sys
6
+ from typing import Optional
7
+
8
+
9
+ def get_client():
10
+ """Get an authenticated VoltageGPU client."""
11
+ try:
12
+ from volt.sdk import VoltageGPUClient
13
+ return VoltageGPUClient()
14
+ except ValueError as e:
15
+ click.echo(f"Error: {e}", err=True)
16
+ click.echo("Set your API key: export VOLT_API_KEY=your_key", err=True)
17
+ click.echo("Or create ~/.volt/config.ini with [api] api_key=your_key", err=True)
18
+ sys.exit(1)
19
+ except Exception as e:
20
+ click.echo(f"Error initializing client: {e}", err=True)
21
+ sys.exit(1)
22
+
23
+
24
+ def format_table(headers, rows):
25
+ """Format data as a simple aligned table."""
26
+ if not rows:
27
+ click.echo("No results found.")
28
+ return
29
+ col_widths = [len(h) for h in headers]
30
+ for row in rows:
31
+ for i, cell in enumerate(row):
32
+ col_widths[i] = max(col_widths[i], len(str(cell)))
33
+ header_line = " ".join(h.ljust(col_widths[i]) for i, h in enumerate(headers))
34
+ separator = " ".join("-" * w for w in col_widths)
35
+ click.echo(header_line)
36
+ click.echo(separator)
37
+ for row in rows:
38
+ click.echo(" ".join(str(cell).ljust(col_widths[i]) for i, cell in enumerate(row)))
39
+
40
+
41
+ @click.group(invoke_without_command=True)
42
+ @click.version_option(version="1.0.0", prog_name="volt")
43
+ @click.pass_context
44
+ def cli(ctx):
45
+ """VoltageGPU CLI - Manage GPU cloud resources from your terminal.
46
+
47
+ Deploy, manage, and scale GPU pods with ease.
48
+ Get started: https://docs.voltagegpu.com/cli
49
+ """
50
+ if ctx.invoked_subcommand is None:
51
+ click.echo(ctx.get_help())
52
+
53
+
54
+ # ==================== PODS ====================
55
+
56
+ @cli.group()
57
+ def pods():
58
+ """Manage GPU pods."""
59
+ pass
60
+
61
+
62
+ @pods.command("list")
63
+ @click.option("--json", "as_json", is_flag=True, help="Output as JSON")
64
+ def pods_list(as_json):
65
+ """List all your pods."""
66
+ client = get_client()
67
+ try:
68
+ pods_data = client.list_pods()
69
+ if as_json:
70
+ click.echo(json.dumps([{
71
+ "id": p.id, "name": p.name, "status": p.status,
72
+ "gpu_type": p.gpu_type, "gpu_count": p.gpu_count,
73
+ "hourly_price": p.hourly_price
74
+ } for p in pods_data], indent=2))
75
+ else:
76
+ rows = [(p.id[:12] + "...", p.name, p.status, p.gpu_type,
77
+ str(p.gpu_count), f"${p.hourly_price:.2f}/hr") for p in pods_data]
78
+ format_table(["ID", "NAME", "STATUS", "GPU", "COUNT", "PRICE"], rows)
79
+ except Exception as e:
80
+ click.echo(f"Error: {e}", err=True)
81
+ sys.exit(1)
82
+ finally:
83
+ client.close()
84
+
85
+
86
+ @pods.command("get")
87
+ @click.argument("pod_id")
88
+ @click.option("--json", "as_json", is_flag=True, help="Output as JSON")
89
+ def pods_get(pod_id, as_json):
90
+ """Get details of a specific pod."""
91
+ client = get_client()
92
+ try:
93
+ pod = client.get_pod(pod_id)
94
+ if as_json:
95
+ click.echo(json.dumps({
96
+ "id": pod.id, "name": pod.name, "status": pod.status,
97
+ "gpu_type": pod.gpu_type, "gpu_count": pod.gpu_count,
98
+ "hourly_price": pod.hourly_price, "ssh_host": pod.ssh_host,
99
+ "ssh_port": pod.ssh_port, "template_id": pod.template_id,
100
+ "created_at": pod.created_at
101
+ }, indent=2))
102
+ else:
103
+ click.echo(f"Pod: {pod.name}")
104
+ click.echo(f" ID: {pod.id}")
105
+ click.echo(f" Status: {pod.status}")
106
+ click.echo(f" GPU: {pod.gpu_count}x {pod.gpu_type}")
107
+ click.echo(f" Price: ${pod.hourly_price:.2f}/hr")
108
+ if pod.ssh_host:
109
+ click.echo(f" SSH: ssh root@{pod.ssh_host} -p {pod.ssh_port or 22}")
110
+ if pod.template_id:
111
+ click.echo(f" Template: {pod.template_id}")
112
+ if pod.created_at:
113
+ click.echo(f" Created: {pod.created_at}")
114
+ except Exception as e:
115
+ click.echo(f"Error: {e}", err=True)
116
+ sys.exit(1)
117
+ finally:
118
+ client.close()
119
+
120
+
121
+ @pods.command("create")
122
+ @click.option("--template", "template_id", required=True, help="Template ID to use")
123
+ @click.option("--name", required=True, help="Name for the pod")
124
+ @click.option("--gpu-count", default=1, type=int, help="Number of GPUs (default: 1)")
125
+ @click.option("--ssh-key", "ssh_key_ids", multiple=True, help="SSH key ID(s) to attach")
126
+ def pods_create(template_id, name, gpu_count, ssh_key_ids):
127
+ """Deploy a new GPU pod."""
128
+ client = get_client()
129
+ try:
130
+ pod = client.create_pod(
131
+ template_id=template_id,
132
+ name=name,
133
+ gpu_count=gpu_count,
134
+ ssh_key_ids=list(ssh_key_ids) if ssh_key_ids else None
135
+ )
136
+ click.echo(f"Pod created successfully!")
137
+ click.echo(f" ID: {pod.id}")
138
+ click.echo(f" Name: {pod.name}")
139
+ click.echo(f" Status: {pod.status}")
140
+ click.echo(f" GPU: {pod.gpu_count}x {pod.gpu_type}")
141
+ click.echo(f" Price: ${pod.hourly_price:.2f}/hr")
142
+ except Exception as e:
143
+ click.echo(f"Error: {e}", err=True)
144
+ sys.exit(1)
145
+ finally:
146
+ client.close()
147
+
148
+
149
+ @pods.command("start")
150
+ @click.argument("pod_id")
151
+ def pods_start(pod_id):
152
+ """Start a stopped pod."""
153
+ client = get_client()
154
+ try:
155
+ pod = client.start_pod(pod_id)
156
+ click.echo(f"Pod {pod.name} started. Status: {pod.status}")
157
+ except Exception as e:
158
+ click.echo(f"Error: {e}", err=True)
159
+ sys.exit(1)
160
+ finally:
161
+ client.close()
162
+
163
+
164
+ @pods.command("stop")
165
+ @click.argument("pod_id")
166
+ def pods_stop(pod_id):
167
+ """Stop a running pod."""
168
+ client = get_client()
169
+ try:
170
+ pod = client.stop_pod(pod_id)
171
+ click.echo(f"Pod {pod.name} stopped. Status: {pod.status}")
172
+ except Exception as e:
173
+ click.echo(f"Error: {e}", err=True)
174
+ sys.exit(1)
175
+ finally:
176
+ client.close()
177
+
178
+
179
+ @pods.command("delete")
180
+ @click.argument("pod_id")
181
+ @click.option("--yes", is_flag=True, help="Skip confirmation")
182
+ def pods_delete(pod_id, yes):
183
+ """Terminate and delete a pod permanently."""
184
+ if not yes:
185
+ click.confirm(f"Are you sure you want to delete pod {pod_id}?", abort=True)
186
+ client = get_client()
187
+ try:
188
+ client.delete_pod(pod_id)
189
+ click.echo(f"Pod {pod_id} deleted.")
190
+ except Exception as e:
191
+ click.echo(f"Error: {e}", err=True)
192
+ sys.exit(1)
193
+ finally:
194
+ client.close()
195
+
196
+
197
+ @pods.command("ssh")
198
+ @click.argument("pod_id")
199
+ def pods_ssh(pod_id):
200
+ """Show SSH command to connect to a pod."""
201
+ client = get_client()
202
+ try:
203
+ pod = client.get_pod(pod_id)
204
+ if pod.ssh_host:
205
+ port = pod.ssh_port or 22
206
+ cmd = f"ssh root@{pod.ssh_host} -p {port}"
207
+ click.echo(cmd)
208
+ else:
209
+ click.echo("SSH not available for this pod.", err=True)
210
+ sys.exit(1)
211
+ except Exception as e:
212
+ click.echo(f"Error: {e}", err=True)
213
+ sys.exit(1)
214
+ finally:
215
+ client.close()
216
+
217
+
218
+ # ==================== TEMPLATES ====================
219
+
220
+ @cli.group()
221
+ def templates():
222
+ """Browse available pod templates."""
223
+ pass
224
+
225
+
226
+ @templates.command("list")
227
+ @click.option("--category", help="Filter by category (e.g., llm, ml, diffusion)")
228
+ @click.option("--json", "as_json", is_flag=True, help="Output as JSON")
229
+ def templates_list(category, as_json):
230
+ """List available templates."""
231
+ client = get_client()
232
+ try:
233
+ templates_data = client.list_templates(category=category)
234
+ if as_json:
235
+ click.echo(json.dumps([{
236
+ "id": t.id, "name": t.name, "category": t.category,
237
+ "docker_image": t.docker_image
238
+ } for t in templates_data], indent=2))
239
+ else:
240
+ rows = [(t.id[:12] + "...", t.name, t.category or "-",
241
+ t.docker_image[:40] + "..." if len(t.docker_image) > 40 else t.docker_image)
242
+ for t in templates_data]
243
+ format_table(["ID", "NAME", "CATEGORY", "DOCKER IMAGE"], rows)
244
+ except Exception as e:
245
+ click.echo(f"Error: {e}", err=True)
246
+ sys.exit(1)
247
+ finally:
248
+ client.close()
249
+
250
+
251
+ @templates.command("get")
252
+ @click.argument("template_id")
253
+ def templates_get(template_id):
254
+ """Get details of a specific template."""
255
+ client = get_client()
256
+ try:
257
+ t = client.get_template(template_id)
258
+ click.echo(f"Template: {t.name}")
259
+ click.echo(f" ID: {t.id}")
260
+ click.echo(f" Description: {t.description}")
261
+ click.echo(f" Docker Image: {t.docker_image}")
262
+ click.echo(f" Category: {t.category or '-'}")
263
+ except Exception as e:
264
+ click.echo(f"Error: {e}", err=True)
265
+ sys.exit(1)
266
+ finally:
267
+ client.close()
268
+
269
+
270
+ # ==================== SSH KEYS ====================
271
+
272
+ @cli.group("ssh-keys")
273
+ def ssh_keys():
274
+ """Manage SSH keys."""
275
+ pass
276
+
277
+
278
+ @ssh_keys.command("list")
279
+ @click.option("--json", "as_json", is_flag=True, help="Output as JSON")
280
+ def ssh_keys_list(as_json):
281
+ """List your SSH keys."""
282
+ client = get_client()
283
+ try:
284
+ keys = client.list_ssh_keys()
285
+ if as_json:
286
+ click.echo(json.dumps([{
287
+ "id": k.id, "name": k.name, "fingerprint": k.fingerprint,
288
+ "created_at": k.created_at
289
+ } for k in keys], indent=2))
290
+ else:
291
+ rows = [(k.id[:12] + "...", k.name, k.fingerprint or "-",
292
+ k.created_at or "-") for k in keys]
293
+ format_table(["ID", "NAME", "FINGERPRINT", "CREATED"], rows)
294
+ except Exception as e:
295
+ click.echo(f"Error: {e}", err=True)
296
+ sys.exit(1)
297
+ finally:
298
+ client.close()
299
+
300
+
301
+ @ssh_keys.command("add")
302
+ @click.option("--name", required=True, help="Label for this key")
303
+ @click.option("--file", "key_file", type=click.Path(exists=True), help="Path to public key file")
304
+ @click.option("--key", "key_str", help="Public key string directly")
305
+ def ssh_keys_add(name, key_file, key_str):
306
+ """Add a new SSH key."""
307
+ if not key_file and not key_str:
308
+ click.echo("Error: provide --file or --key", err=True)
309
+ sys.exit(1)
310
+ if key_file:
311
+ with open(key_file) as f:
312
+ public_key = f.read().strip()
313
+ else:
314
+ public_key = key_str
315
+ client = get_client()
316
+ try:
317
+ key = client.add_ssh_key(name=name, public_key=public_key)
318
+ click.echo(f"SSH key added: {key.name} ({key.id})")
319
+ except Exception as e:
320
+ click.echo(f"Error: {e}", err=True)
321
+ sys.exit(1)
322
+ finally:
323
+ client.close()
324
+
325
+
326
+ @ssh_keys.command("delete")
327
+ @click.argument("key_id")
328
+ def ssh_keys_delete(key_id):
329
+ """Remove an SSH key."""
330
+ client = get_client()
331
+ try:
332
+ client.delete_ssh_key(key_id)
333
+ click.echo(f"SSH key {key_id} deleted.")
334
+ except Exception as e:
335
+ click.echo(f"Error: {e}", err=True)
336
+ sys.exit(1)
337
+ finally:
338
+ client.close()
339
+
340
+
341
+ # ==================== MACHINES ====================
342
+
343
+ @cli.group()
344
+ def machines():
345
+ """View available GPU hardware."""
346
+ pass
347
+
348
+
349
+ @machines.command("list")
350
+ @click.option("--gpu", help="Filter by GPU type (e.g., RTX4090, A100, H100)")
351
+ @click.option("--json", "as_json", is_flag=True, help="Output as JSON")
352
+ def machines_list(gpu, as_json):
353
+ """List available GPU machines with pricing."""
354
+ client = get_client()
355
+ try:
356
+ machines_data = client.list_machines(gpu_type=gpu)
357
+ if as_json:
358
+ click.echo(json.dumps([{
359
+ "name": m.name, "hourly_price": m.hourly_price,
360
+ "total_gpu_count": m.total_gpu_count, "available": m.available
361
+ } for m in machines_data], indent=2))
362
+ else:
363
+ rows = [(m.name, str(m.total_gpu_count),
364
+ f"${m.hourly_price:.2f}/hr",
365
+ "Yes" if m.available else "No") for m in machines_data]
366
+ format_table(["GPU", "AVAILABLE COUNT", "PRICE", "IN STOCK"], rows)
367
+ except Exception as e:
368
+ click.echo(f"Error: {e}", err=True)
369
+ sys.exit(1)
370
+ finally:
371
+ client.close()
372
+
373
+
374
+ # ==================== ACCOUNT ====================
375
+
376
+ @cli.group()
377
+ def account():
378
+ """View account details and balance."""
379
+ pass
380
+
381
+
382
+ @account.command("balance")
383
+ def account_balance():
384
+ """Check your account credit balance."""
385
+ client = get_client()
386
+ try:
387
+ balance = client.get_balance()
388
+ click.echo(f"Balance: ${balance:.2f}")
389
+ except Exception as e:
390
+ click.echo(f"Error: {e}", err=True)
391
+ sys.exit(1)
392
+ finally:
393
+ client.close()
394
+
395
+
396
+ @account.command("info")
397
+ def account_info():
398
+ """Show your account details."""
399
+ client = get_client()
400
+ try:
401
+ info = client.get_account_info()
402
+ for key, value in info.items():
403
+ click.echo(f" {key}: {value}")
404
+ except Exception as e:
405
+ click.echo(f"Error: {e}", err=True)
406
+ sys.exit(1)
407
+ finally:
408
+ client.close()
409
+
410
+
411
+ def main():
412
+ """Main entry point for the CLI."""
413
+ cli()
414
+
415
+
416
+ if __name__ == "__main__":
417
+ main()
volt/sdk/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ """VoltageGPU SDK - Python client for VoltageGPU API."""
2
+
3
+ from .config import Config
4
+ from .client import VoltageGPUClient
5
+
6
+ __all__ = ["Config", "VoltageGPUClient"]
7
+ __version__ = "0.1.0"