tetra-rp 0.6.0__py3-none-any.whl → 0.24.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.
Files changed (97) hide show
  1. tetra_rp/__init__.py +109 -19
  2. tetra_rp/cli/commands/__init__.py +1 -0
  3. tetra_rp/cli/commands/apps.py +143 -0
  4. tetra_rp/cli/commands/build.py +1082 -0
  5. tetra_rp/cli/commands/build_utils/__init__.py +1 -0
  6. tetra_rp/cli/commands/build_utils/handler_generator.py +176 -0
  7. tetra_rp/cli/commands/build_utils/lb_handler_generator.py +309 -0
  8. tetra_rp/cli/commands/build_utils/manifest.py +430 -0
  9. tetra_rp/cli/commands/build_utils/mothership_handler_generator.py +75 -0
  10. tetra_rp/cli/commands/build_utils/scanner.py +596 -0
  11. tetra_rp/cli/commands/deploy.py +580 -0
  12. tetra_rp/cli/commands/init.py +123 -0
  13. tetra_rp/cli/commands/resource.py +108 -0
  14. tetra_rp/cli/commands/run.py +296 -0
  15. tetra_rp/cli/commands/test_mothership.py +458 -0
  16. tetra_rp/cli/commands/undeploy.py +533 -0
  17. tetra_rp/cli/main.py +97 -0
  18. tetra_rp/cli/utils/__init__.py +1 -0
  19. tetra_rp/cli/utils/app.py +15 -0
  20. tetra_rp/cli/utils/conda.py +127 -0
  21. tetra_rp/cli/utils/deployment.py +530 -0
  22. tetra_rp/cli/utils/ignore.py +143 -0
  23. tetra_rp/cli/utils/skeleton.py +184 -0
  24. tetra_rp/cli/utils/skeleton_template/.env.example +4 -0
  25. tetra_rp/cli/utils/skeleton_template/.flashignore +40 -0
  26. tetra_rp/cli/utils/skeleton_template/.gitignore +44 -0
  27. tetra_rp/cli/utils/skeleton_template/README.md +263 -0
  28. tetra_rp/cli/utils/skeleton_template/main.py +44 -0
  29. tetra_rp/cli/utils/skeleton_template/mothership.py +55 -0
  30. tetra_rp/cli/utils/skeleton_template/pyproject.toml +58 -0
  31. tetra_rp/cli/utils/skeleton_template/requirements.txt +1 -0
  32. tetra_rp/cli/utils/skeleton_template/workers/__init__.py +0 -0
  33. tetra_rp/cli/utils/skeleton_template/workers/cpu/__init__.py +19 -0
  34. tetra_rp/cli/utils/skeleton_template/workers/cpu/endpoint.py +36 -0
  35. tetra_rp/cli/utils/skeleton_template/workers/gpu/__init__.py +19 -0
  36. tetra_rp/cli/utils/skeleton_template/workers/gpu/endpoint.py +61 -0
  37. tetra_rp/client.py +136 -33
  38. tetra_rp/config.py +29 -0
  39. tetra_rp/core/api/runpod.py +591 -39
  40. tetra_rp/core/deployment.py +232 -0
  41. tetra_rp/core/discovery.py +425 -0
  42. tetra_rp/core/exceptions.py +50 -0
  43. tetra_rp/core/resources/__init__.py +27 -9
  44. tetra_rp/core/resources/app.py +738 -0
  45. tetra_rp/core/resources/base.py +139 -4
  46. tetra_rp/core/resources/constants.py +21 -0
  47. tetra_rp/core/resources/cpu.py +115 -13
  48. tetra_rp/core/resources/gpu.py +182 -16
  49. tetra_rp/core/resources/live_serverless.py +153 -16
  50. tetra_rp/core/resources/load_balancer_sls_resource.py +440 -0
  51. tetra_rp/core/resources/network_volume.py +126 -31
  52. tetra_rp/core/resources/resource_manager.py +436 -35
  53. tetra_rp/core/resources/serverless.py +537 -120
  54. tetra_rp/core/resources/serverless_cpu.py +201 -0
  55. tetra_rp/core/resources/template.py +1 -59
  56. tetra_rp/core/utils/constants.py +10 -0
  57. tetra_rp/core/utils/file_lock.py +260 -0
  58. tetra_rp/core/utils/http.py +67 -0
  59. tetra_rp/core/utils/lru_cache.py +75 -0
  60. tetra_rp/core/utils/singleton.py +36 -1
  61. tetra_rp/core/validation.py +44 -0
  62. tetra_rp/execute_class.py +301 -0
  63. tetra_rp/protos/remote_execution.py +98 -9
  64. tetra_rp/runtime/__init__.py +1 -0
  65. tetra_rp/runtime/circuit_breaker.py +274 -0
  66. tetra_rp/runtime/config.py +12 -0
  67. tetra_rp/runtime/exceptions.py +49 -0
  68. tetra_rp/runtime/generic_handler.py +206 -0
  69. tetra_rp/runtime/lb_handler.py +189 -0
  70. tetra_rp/runtime/load_balancer.py +160 -0
  71. tetra_rp/runtime/manifest_fetcher.py +192 -0
  72. tetra_rp/runtime/metrics.py +325 -0
  73. tetra_rp/runtime/models.py +73 -0
  74. tetra_rp/runtime/mothership_provisioner.py +512 -0
  75. tetra_rp/runtime/production_wrapper.py +266 -0
  76. tetra_rp/runtime/reliability_config.py +149 -0
  77. tetra_rp/runtime/retry_manager.py +118 -0
  78. tetra_rp/runtime/serialization.py +124 -0
  79. tetra_rp/runtime/service_registry.py +346 -0
  80. tetra_rp/runtime/state_manager_client.py +248 -0
  81. tetra_rp/stubs/live_serverless.py +35 -17
  82. tetra_rp/stubs/load_balancer_sls.py +357 -0
  83. tetra_rp/stubs/registry.py +145 -19
  84. {tetra_rp-0.6.0.dist-info → tetra_rp-0.24.0.dist-info}/METADATA +398 -60
  85. tetra_rp-0.24.0.dist-info/RECORD +99 -0
  86. {tetra_rp-0.6.0.dist-info → tetra_rp-0.24.0.dist-info}/WHEEL +1 -1
  87. tetra_rp-0.24.0.dist-info/entry_points.txt +2 -0
  88. tetra_rp/core/pool/cluster_manager.py +0 -177
  89. tetra_rp/core/pool/dataclass.py +0 -18
  90. tetra_rp/core/pool/ex.py +0 -38
  91. tetra_rp/core/pool/job.py +0 -22
  92. tetra_rp/core/pool/worker.py +0 -19
  93. tetra_rp/core/resources/utils.py +0 -50
  94. tetra_rp/core/utils/json.py +0 -33
  95. tetra_rp-0.6.0.dist-info/RECORD +0 -39
  96. /tetra_rp/{core/pool → cli}/__init__.py +0 -0
  97. {tetra_rp-0.6.0.dist-info → tetra_rp-0.24.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,533 @@
1
+ """Undeploy command for managing RunPod serverless endpoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from typing import TYPE_CHECKING, Dict, Optional, Tuple
7
+ import typer
8
+ from rich.console import Console
9
+ from rich.table import Table
10
+ from rich.panel import Panel
11
+ from rich.prompt import Confirm
12
+ import questionary
13
+
14
+ if TYPE_CHECKING:
15
+ from ...core.resources.base import DeployableResource
16
+ from ...core.resources.resource_manager import ResourceManager
17
+
18
+ console = Console()
19
+
20
+
21
+ def _get_resource_manager():
22
+ """Get ResourceManager instance with lazy loading.
23
+
24
+ Imports are deferred to avoid loading heavy dependencies (runpod, aiohttp, etc)
25
+ at CLI startup time. This allows fast commands like 'flash init' to run without
26
+ loading unnecessary dependencies.
27
+
28
+ Can be mocked in tests: @patch('tetra_rp.cli.commands.undeploy._get_resource_manager')
29
+ """
30
+ from ...core.resources.resource_manager import ResourceManager
31
+
32
+ return ResourceManager()
33
+
34
+
35
+ def _get_resource_status(resource) -> Tuple[str, str]:
36
+ """Get resource status with icon and text.
37
+
38
+ Args:
39
+ resource: DeployableResource to check
40
+
41
+ Returns:
42
+ Tuple of (status_icon, status_text)
43
+ """
44
+ try:
45
+ if resource.is_deployed():
46
+ return "🟢", "Active"
47
+ return "🔴", "Inactive"
48
+ except Exception:
49
+ return "❓", "Unknown"
50
+
51
+
52
+ def _get_resource_type(resource) -> str:
53
+ """Get human-readable resource type.
54
+
55
+ Args:
56
+ resource: DeployableResource to check
57
+
58
+ Returns:
59
+ Resource type string
60
+ """
61
+ class_name = resource.__class__.__name__
62
+ return class_name.replace("Serverless", " Serverless").replace(
63
+ "Endpoint", " Endpoint"
64
+ )
65
+
66
+
67
+ def list_command():
68
+ """List all deployed endpoints tracked in .tetra_resources.pkl."""
69
+ manager = _get_resource_manager()
70
+ resources = manager.list_all_resources()
71
+
72
+ if not resources:
73
+ console.print(
74
+ Panel(
75
+ "No endpoints found\n\n"
76
+ "Endpoints are automatically tracked when you use @remote decorator.",
77
+ title="Tracked Endpoints",
78
+ expand=False,
79
+ )
80
+ )
81
+ return
82
+
83
+ table = Table(title="Tracked RunPod Serverless Endpoints")
84
+ table.add_column("Name", style="cyan", no_wrap=True)
85
+ table.add_column("Endpoint ID", style="magenta")
86
+ table.add_column("Status", justify="center")
87
+ table.add_column("Type", style="yellow")
88
+ table.add_column("Resource ID", style="dim", no_wrap=True)
89
+
90
+ active_count = 0
91
+ inactive_count = 0
92
+
93
+ for resource_id, resource in resources.items():
94
+ status_icon, status_text = _get_resource_status(resource)
95
+ if status_text == "Active":
96
+ active_count += 1
97
+ elif status_text == "Inactive":
98
+ inactive_count += 1
99
+
100
+ # Get name if available
101
+ name = getattr(resource, "name", "N/A")
102
+ endpoint_id = getattr(resource, "id", "N/A")
103
+ resource_type = _get_resource_type(resource)
104
+
105
+ # Truncate resource_id for display
106
+ display_resource_id = (
107
+ resource_id[:12] + "..." if len(resource_id) > 12 else resource_id
108
+ )
109
+
110
+ table.add_row(
111
+ name,
112
+ endpoint_id,
113
+ f"{status_icon} {status_text}",
114
+ resource_type,
115
+ display_resource_id,
116
+ )
117
+
118
+ console.print(table)
119
+
120
+ # Summary
121
+ total = len(resources)
122
+ unknown_count = total - active_count - inactive_count
123
+ summary = f"Total: {total} endpoint{'s' if total != 1 else ''}"
124
+ if active_count > 0:
125
+ summary += f" ({active_count} active"
126
+ if inactive_count > 0:
127
+ summary += (
128
+ f", {inactive_count} inactive"
129
+ if active_count > 0
130
+ else f" ({inactive_count} inactive"
131
+ )
132
+ if unknown_count > 0:
133
+ summary += (
134
+ f", {unknown_count} unknown"
135
+ if (active_count > 0 or inactive_count > 0)
136
+ else f" ({unknown_count} unknown"
137
+ )
138
+ if active_count > 0 or inactive_count > 0 or unknown_count > 0:
139
+ summary += ")"
140
+
141
+ console.print(f"\n{summary}\n")
142
+ console.print("💡 Use [bold]flash undeploy <name>[/bold] to remove an endpoint")
143
+ console.print("💡 Use [bold]flash undeploy --all[/bold] to remove all endpoints")
144
+ console.print(
145
+ "💡 Use [bold]flash undeploy --interactive[/bold] for checkbox selection"
146
+ )
147
+
148
+
149
+ def _cleanup_stale_endpoints(
150
+ resources: Dict[str, DeployableResource], manager: ResourceManager
151
+ ) -> None:
152
+ """Remove inactive endpoints from tracking (already deleted externally).
153
+
154
+ Args:
155
+ resources: Dictionary of resource_id -> DeployableResource
156
+ manager: ResourceManager instance for removing resources
157
+ """
158
+ console.print(
159
+ Panel(
160
+ "Checking for inactive endpoints...\n\n"
161
+ "This will remove endpoints from tracking that are no longer active\n"
162
+ "(already deleted via RunPod UI or API).",
163
+ title="Cleanup Stale Endpoints",
164
+ expand=False,
165
+ )
166
+ )
167
+
168
+ # Find inactive endpoints
169
+ inactive = []
170
+ with console.status("Checking endpoint status..."):
171
+ for resource_id, resource in resources.items():
172
+ status_icon, status_text = _get_resource_status(resource)
173
+ if status_text == "Inactive":
174
+ inactive.append((resource_id, resource))
175
+
176
+ if not inactive:
177
+ console.print("\n[green]✓[/green] No inactive endpoints found")
178
+ return
179
+
180
+ # Show what will be removed
181
+ console.print(f"\nFound [yellow]{len(inactive)}[/yellow] inactive endpoint(s):")
182
+ for resource_id, resource in inactive:
183
+ console.print(f" • {resource.name} ({getattr(resource, 'id', 'N/A')})")
184
+
185
+ # Confirm removal
186
+ if not Confirm.ask(
187
+ "\n[yellow]⚠️ Remove these from tracking?[/yellow]",
188
+ default=False,
189
+ ):
190
+ console.print("[yellow]Cancelled[/yellow]")
191
+ return
192
+
193
+ # Undeploy inactive endpoints (force remove from tracking even if already deleted remotely)
194
+ removed_count = 0
195
+ for resource_id, resource in inactive:
196
+ result = asyncio.run(
197
+ manager.undeploy_resource(resource_id, resource.name, force_remove=True)
198
+ )
199
+
200
+ if result["success"]:
201
+ removed_count += 1
202
+ console.print(
203
+ f"[green]✓[/green] Removed [cyan]{resource.name}[/cyan] from tracking"
204
+ )
205
+ else:
206
+ # Resource already deleted remotely, but force_remove cleaned up tracking
207
+ removed_count += 1
208
+ console.print(
209
+ f"[yellow]⚠[/yellow] {resource.name}: Already deleted remotely, removed from tracking"
210
+ )
211
+
212
+ console.print(f"\n[green]✓[/green] Cleaned up {removed_count} inactive endpoint(s)")
213
+
214
+
215
+ def undeploy_command(
216
+ name: Optional[str] = typer.Argument(
217
+ None, help="Name of the endpoint to undeploy (or 'list' to show all)"
218
+ ),
219
+ all: bool = typer.Option(False, "--all", help="Undeploy all endpoints"),
220
+ interactive: bool = typer.Option(
221
+ False, "--interactive", "-i", help="Interactive selection with checkboxes"
222
+ ),
223
+ cleanup_stale: bool = typer.Option(
224
+ False,
225
+ "--cleanup-stale",
226
+ help="Remove inactive endpoints from tracking (already deleted externally)",
227
+ ),
228
+ force: bool = typer.Option(
229
+ False, "--force", "-f", help="Force operation without confirmation prompts"
230
+ ),
231
+ ):
232
+ """Undeploy (delete) RunPod serverless endpoints.
233
+
234
+ Examples:
235
+
236
+ # List all endpoints
237
+ flash undeploy list
238
+
239
+ # Undeploy specific endpoint by name
240
+ flash undeploy my-api
241
+
242
+ # Undeploy all endpoints (with confirmation)
243
+ flash undeploy --all
244
+
245
+ # Undeploy all endpoints without confirmation
246
+ flash undeploy --all --force
247
+
248
+ # Interactive selection
249
+ flash undeploy --interactive
250
+
251
+ # Remove stale endpoint tracking (already deleted externally)
252
+ flash undeploy --cleanup-stale
253
+ """
254
+ # Handle "list" as a special case
255
+ if name == "list":
256
+ list_command()
257
+ return
258
+
259
+ manager = _get_resource_manager()
260
+ resources = manager.list_all_resources()
261
+
262
+ if not resources:
263
+ console.print(
264
+ Panel(
265
+ "No endpoints found to undeploy\n\n"
266
+ "Use @remote decorator to deploy endpoints.",
267
+ title="No Endpoints",
268
+ expand=False,
269
+ )
270
+ )
271
+ return
272
+
273
+ # Handle cleanup-stale mode
274
+ if cleanup_stale:
275
+ _cleanup_stale_endpoints(resources, manager)
276
+ return
277
+
278
+ # Handle different modes
279
+ if interactive:
280
+ _interactive_undeploy(resources, skip_confirm=force)
281
+ elif all:
282
+ _undeploy_all(resources, skip_confirm=force)
283
+ elif name:
284
+ _undeploy_by_name(name, resources, skip_confirm=force)
285
+ else:
286
+ console.print(
287
+ Panel(
288
+ "Usage: flash undeploy [name | list | --all | --interactive | --cleanup-stale]",
289
+ title="Undeploy Help",
290
+ expand=False,
291
+ )
292
+ )
293
+ console.print(
294
+ "[red]Error:[/red] Please specify a name, use --all/--interactive, or run `flash undeploy list`"
295
+ )
296
+ # Exit 0: Treat usage help display as successful operation for better UX
297
+ raise typer.Exit(0)
298
+
299
+
300
+ def _undeploy_by_name(name: str, resources: dict, skip_confirm: bool = False):
301
+ """Undeploy endpoints matching the given name.
302
+
303
+ Args:
304
+ name: Name to search for
305
+ resources: Dict of all resources
306
+ skip_confirm: Skip confirmation prompts
307
+ """
308
+ # Find matching resources
309
+ matches = []
310
+ for resource_id, resource in resources.items():
311
+ if hasattr(resource, "name") and resource.name == name:
312
+ matches.append((resource_id, resource))
313
+
314
+ if not matches:
315
+ console.print(f"[red]Error:[/red] No endpoint found with name '{name}'")
316
+ console.print(
317
+ "\n💡 Use [bold]flash undeploy list[/bold] to see available endpoints"
318
+ )
319
+ raise typer.Exit(1)
320
+
321
+ # Show what will be deleted
322
+ console.print(
323
+ Panel(
324
+ "[yellow]⚠️ The following endpoint(s) will be deleted:[/yellow]\n",
325
+ title="Undeploy Confirmation",
326
+ expand=False,
327
+ )
328
+ )
329
+
330
+ for resource_id, resource in matches:
331
+ endpoint_id = getattr(resource, "id", "N/A")
332
+ resource_type = _get_resource_type(resource)
333
+ status_icon, status_text = _get_resource_status(resource)
334
+
335
+ console.print(f" • [bold]{resource.name}[/bold]")
336
+ console.print(f" Endpoint ID: {endpoint_id}")
337
+ console.print(f" Type: {resource_type}")
338
+ console.print(f" Status: {status_icon} {status_text}")
339
+ console.print()
340
+
341
+ console.print("[red]🚨 This action cannot be undone![/red]\n")
342
+
343
+ if not skip_confirm:
344
+ try:
345
+ confirmed = questionary.confirm(
346
+ f"Are you sure you want to delete {len(matches)} endpoint(s)?"
347
+ ).ask()
348
+
349
+ if not confirmed:
350
+ console.print("Undeploy cancelled")
351
+ raise typer.Exit(0)
352
+ except KeyboardInterrupt:
353
+ console.print("\nUndeploy cancelled")
354
+ raise typer.Exit(0)
355
+
356
+ # Delete endpoints
357
+ manager = _get_resource_manager()
358
+ with console.status("Deleting endpoint(s)..."):
359
+ results = []
360
+ for resource_id, resource in matches:
361
+ result = asyncio.run(manager.undeploy_resource(resource_id, resource.name))
362
+ results.append(result)
363
+
364
+ # Show results
365
+ success_count = sum(1 for r in results if r["success"])
366
+ fail_count = len(results) - success_count
367
+
368
+ if success_count > 0:
369
+ console.print(
370
+ f"\n[green]✓[/green] Successfully deleted {success_count} endpoint(s)"
371
+ )
372
+ if fail_count > 0:
373
+ console.print(f"[red]✗[/red] Failed to delete {fail_count} endpoint(s)")
374
+ console.print("\nErrors:")
375
+ for result in results:
376
+ if not result["success"]:
377
+ console.print(f" • {result['message']}")
378
+
379
+
380
+ def _undeploy_all(resources: dict, skip_confirm: bool = False):
381
+ """Undeploy all endpoints with confirmation.
382
+
383
+ Args:
384
+ resources: Dict of all resources
385
+ skip_confirm: Skip confirmation prompts
386
+ """
387
+ # Show what will be deleted
388
+ console.print(
389
+ Panel(
390
+ f"[yellow]⚠️ ALL {len(resources)} endpoint(s) will be deleted![/yellow]\n",
391
+ title="Undeploy All Confirmation",
392
+ expand=False,
393
+ )
394
+ )
395
+
396
+ for resource_id, resource in resources.items():
397
+ name = getattr(resource, "name", "N/A")
398
+ endpoint_id = getattr(resource, "id", "N/A")
399
+ console.print(f" • {name} ({endpoint_id})")
400
+
401
+ console.print("\n[red]🚨 This action cannot be undone![/red]\n")
402
+
403
+ if not skip_confirm:
404
+ try:
405
+ confirmed = questionary.confirm(
406
+ f"Are you sure you want to delete ALL {len(resources)} endpoints?"
407
+ ).ask()
408
+
409
+ if not confirmed:
410
+ console.print("Undeploy cancelled")
411
+ raise typer.Exit(0)
412
+
413
+ # Double confirmation for --all
414
+ typed_confirm = questionary.text("Type 'DELETE ALL' to confirm:").ask()
415
+
416
+ if typed_confirm != "DELETE ALL":
417
+ console.print("Confirmation failed - text does not match")
418
+ raise typer.Exit(1)
419
+ except KeyboardInterrupt:
420
+ console.print("\nUndeploy cancelled")
421
+ raise typer.Exit(0)
422
+
423
+ # Delete all endpoints
424
+ manager = _get_resource_manager()
425
+ with console.status(f"Deleting {len(resources)} endpoint(s)..."):
426
+ results = []
427
+ for resource_id, resource in resources.items():
428
+ name = getattr(resource, "name", "N/A")
429
+ result = asyncio.run(manager.undeploy_resource(resource_id, name))
430
+ results.append(result)
431
+
432
+ # Show results
433
+ success_count = sum(1 for r in results if r["success"])
434
+ fail_count = len(results) - success_count
435
+
436
+ console.print("\n" + "=" * 50)
437
+ if success_count > 0:
438
+ console.print(
439
+ f"[green]✓[/green] Successfully deleted {success_count} endpoint(s)"
440
+ )
441
+ if fail_count > 0:
442
+ console.print(f"[red]✗[/red] Failed to delete {fail_count} endpoint(s)")
443
+ console.print("\nErrors:")
444
+ for result in results:
445
+ if not result["success"]:
446
+ console.print(f" • {result['message']}")
447
+
448
+
449
+ def _interactive_undeploy(resources: dict, skip_confirm: bool = False):
450
+ """Interactive checkbox selection for undeploying endpoints.
451
+
452
+ Args:
453
+ resources: Dict of all resources
454
+ skip_confirm: Skip confirmation prompts
455
+ """
456
+ # Create choices for questionary
457
+ choices = []
458
+ resource_map = {}
459
+
460
+ for resource_id, resource in resources.items():
461
+ name = getattr(resource, "name", "N/A")
462
+ endpoint_id = getattr(resource, "id", "N/A")
463
+ status_icon, status_text = _get_resource_status(resource)
464
+
465
+ choice_text = f"{name} ({endpoint_id}) - {status_icon} {status_text}"
466
+ choices.append(choice_text)
467
+ resource_map[choice_text] = (resource_id, resource)
468
+
469
+ try:
470
+ selected = questionary.checkbox(
471
+ "Select endpoints to undeploy (Space to select, Enter to confirm):",
472
+ choices=choices,
473
+ ).ask()
474
+
475
+ if not selected:
476
+ console.print("No endpoints selected")
477
+ raise typer.Exit(0)
478
+
479
+ # Show confirmation
480
+ console.print(
481
+ Panel(
482
+ f"[yellow]⚠️ {len(selected)} endpoint(s) will be deleted:[/yellow]\n",
483
+ title="Undeploy Confirmation",
484
+ expand=False,
485
+ )
486
+ )
487
+
488
+ selected_resources = []
489
+ for choice in selected:
490
+ resource_id, resource = resource_map[choice]
491
+ selected_resources.append((resource_id, resource))
492
+ name = getattr(resource, "name", "N/A")
493
+ endpoint_id = getattr(resource, "id", "N/A")
494
+ console.print(f" • {name} ({endpoint_id})")
495
+
496
+ console.print("\n[red]🚨 This action cannot be undone![/red]\n")
497
+
498
+ if not skip_confirm:
499
+ confirmed = questionary.confirm(
500
+ f"Are you sure you want to delete {len(selected)} endpoint(s)?"
501
+ ).ask()
502
+
503
+ if not confirmed:
504
+ console.print("Undeploy cancelled")
505
+ raise typer.Exit(0)
506
+ except KeyboardInterrupt:
507
+ console.print("\nUndeploy cancelled")
508
+ raise typer.Exit(0)
509
+
510
+ # Delete selected endpoints
511
+ manager = _get_resource_manager()
512
+ with console.status(f"Deleting {len(selected_resources)} endpoint(s)..."):
513
+ results = []
514
+ for resource_id, resource in selected_resources:
515
+ name = getattr(resource, "name", "N/A")
516
+ result = asyncio.run(manager.undeploy_resource(resource_id, name))
517
+ results.append(result)
518
+
519
+ # Show results
520
+ success_count = sum(1 for r in results if r["success"])
521
+ fail_count = len(results) - success_count
522
+
523
+ console.print("\n" + "=" * 50)
524
+ if success_count > 0:
525
+ console.print(
526
+ f"[green]✓[/green] Successfully deleted {success_count} endpoint(s)"
527
+ )
528
+ if fail_count > 0:
529
+ console.print(f"[red]✗[/red] Failed to delete {fail_count} endpoint(s)")
530
+ console.print("\nErrors:")
531
+ for result in results:
532
+ if not result["success"]:
533
+ console.print(f" • {result['message']}")
tetra_rp/cli/main.py ADDED
@@ -0,0 +1,97 @@
1
+ """Main CLI entry point for Flash CLI."""
2
+
3
+ import typer
4
+ from importlib import metadata
5
+ from rich.console import Console
6
+ from rich.panel import Panel
7
+
8
+ from .commands import (
9
+ init,
10
+ run,
11
+ build,
12
+ test_mothership,
13
+ # resource,
14
+ deploy,
15
+ apps,
16
+ undeploy,
17
+ )
18
+
19
+
20
+ def get_version() -> str:
21
+ """Get the package version from metadata."""
22
+ try:
23
+ return metadata.version("tetra_rp")
24
+ except metadata.PackageNotFoundError:
25
+ return "unknown"
26
+
27
+
28
+ console = Console()
29
+
30
+ # command: flash
31
+ app = typer.Typer(
32
+ name="flash",
33
+ help="Runpod Flash CLI - Distributed inference and serving framework",
34
+ no_args_is_help=True,
35
+ rich_markup_mode="rich",
36
+ )
37
+
38
+ # command: flash <command>
39
+ app.command("init")(init.init_command)
40
+ app.command("run")(run.run_command)
41
+ app.command("build")(build.build_command)
42
+ app.command("test-mothership")(test_mothership.test_mothership_command)
43
+ # app.command("report")(resource.report_command)
44
+
45
+
46
+ # command: flash deploy
47
+ deploy_app = typer.Typer(
48
+ name="deploy",
49
+ help="Deployment environment management commands",
50
+ no_args_is_help=True,
51
+ )
52
+
53
+ deploy_app.command("list")(deploy.list_command)
54
+ deploy_app.command("new")(deploy.new_command)
55
+ deploy_app.command("send")(deploy.send_command)
56
+ deploy_app.command("info")(deploy.info_command)
57
+ deploy_app.command("delete")(deploy.delete_command)
58
+ # deploy_app.command("report")(deploy.report_command)
59
+ # deploy_app.command("rollback")(deploy.rollback_command)
60
+ # deploy_app.command("remove")(deploy.remove_command)
61
+
62
+
63
+ # command: flash deploy *
64
+ app.add_typer(deploy_app, name="deploy")
65
+ app.add_typer(apps.apps_app)
66
+
67
+
68
+ # command: flash undeploy
69
+ # Note: Using a simple command instead of sub-app to allow both "undeploy list" and "undeploy <name>"
70
+ # The undeploy_command internally handles the "list" case
71
+ app.command("undeploy")(undeploy.undeploy_command)
72
+
73
+
74
+ @app.callback(invoke_without_command=True)
75
+ def main(
76
+ ctx: typer.Context,
77
+ version: bool = typer.Option(False, "--version", "-v", help="Show version"),
78
+ ):
79
+ """Runpod Flash CLI - Distributed inference and serving framework."""
80
+ if version:
81
+ console.print(f"Runpod Flash CLI v{get_version()}")
82
+ raise typer.Exit()
83
+
84
+ if ctx.invoked_subcommand is None:
85
+ console.print(
86
+ Panel(
87
+ "[bold blue]Runpod Flash CLI[/bold blue]\n\n"
88
+ "A framework for distributed inference and serving of ML models.\n\n"
89
+ "Use [bold]flash --help[/bold] to see available commands.",
90
+ title="Welcome",
91
+ expand=False,
92
+ )
93
+ )
94
+
95
+
96
+ if __name__ == "__main__":
97
+ app()
@@ -0,0 +1 @@
1
+ """CLI utility modules."""
@@ -0,0 +1,15 @@
1
+ from pathlib import Path
2
+
3
+
4
+ def discover_flash_project() -> tuple[Path, str]:
5
+ """
6
+ Discover Flash project directory and app name.
7
+ Returns:
8
+ Tuple of (project_dir, app_name)
9
+ Raises:
10
+ typer.Exit: If not in a Flash project directory
11
+ """
12
+ project_dir = Path.cwd()
13
+ app_name = project_dir.name
14
+
15
+ return project_dir, app_name