tetra-rp 0.17.1__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.

Potentially problematic release.


This version of tetra-rp might be problematic. Click here for more details.

Files changed (66) hide show
  1. tetra_rp/__init__.py +43 -0
  2. tetra_rp/cli/__init__.py +0 -0
  3. tetra_rp/cli/commands/__init__.py +1 -0
  4. tetra_rp/cli/commands/build.py +534 -0
  5. tetra_rp/cli/commands/deploy.py +370 -0
  6. tetra_rp/cli/commands/init.py +119 -0
  7. tetra_rp/cli/commands/resource.py +191 -0
  8. tetra_rp/cli/commands/run.py +100 -0
  9. tetra_rp/cli/main.py +85 -0
  10. tetra_rp/cli/utils/__init__.py +1 -0
  11. tetra_rp/cli/utils/conda.py +127 -0
  12. tetra_rp/cli/utils/deployment.py +172 -0
  13. tetra_rp/cli/utils/ignore.py +139 -0
  14. tetra_rp/cli/utils/skeleton.py +184 -0
  15. tetra_rp/cli/utils/skeleton_template/.env.example +3 -0
  16. tetra_rp/cli/utils/skeleton_template/.flashignore +40 -0
  17. tetra_rp/cli/utils/skeleton_template/.gitignore +44 -0
  18. tetra_rp/cli/utils/skeleton_template/README.md +256 -0
  19. tetra_rp/cli/utils/skeleton_template/main.py +43 -0
  20. tetra_rp/cli/utils/skeleton_template/requirements.txt +1 -0
  21. tetra_rp/cli/utils/skeleton_template/workers/__init__.py +0 -0
  22. tetra_rp/cli/utils/skeleton_template/workers/cpu/__init__.py +20 -0
  23. tetra_rp/cli/utils/skeleton_template/workers/cpu/endpoint.py +38 -0
  24. tetra_rp/cli/utils/skeleton_template/workers/gpu/__init__.py +20 -0
  25. tetra_rp/cli/utils/skeleton_template/workers/gpu/endpoint.py +62 -0
  26. tetra_rp/client.py +128 -0
  27. tetra_rp/config.py +29 -0
  28. tetra_rp/core/__init__.py +0 -0
  29. tetra_rp/core/api/__init__.py +6 -0
  30. tetra_rp/core/api/runpod.py +319 -0
  31. tetra_rp/core/exceptions.py +50 -0
  32. tetra_rp/core/resources/__init__.py +37 -0
  33. tetra_rp/core/resources/base.py +47 -0
  34. tetra_rp/core/resources/cloud.py +4 -0
  35. tetra_rp/core/resources/constants.py +4 -0
  36. tetra_rp/core/resources/cpu.py +146 -0
  37. tetra_rp/core/resources/environment.py +41 -0
  38. tetra_rp/core/resources/gpu.py +68 -0
  39. tetra_rp/core/resources/live_serverless.py +62 -0
  40. tetra_rp/core/resources/network_volume.py +148 -0
  41. tetra_rp/core/resources/resource_manager.py +145 -0
  42. tetra_rp/core/resources/serverless.py +463 -0
  43. tetra_rp/core/resources/serverless_cpu.py +162 -0
  44. tetra_rp/core/resources/template.py +94 -0
  45. tetra_rp/core/resources/utils.py +50 -0
  46. tetra_rp/core/utils/__init__.py +0 -0
  47. tetra_rp/core/utils/backoff.py +43 -0
  48. tetra_rp/core/utils/constants.py +10 -0
  49. tetra_rp/core/utils/file_lock.py +260 -0
  50. tetra_rp/core/utils/json.py +33 -0
  51. tetra_rp/core/utils/lru_cache.py +75 -0
  52. tetra_rp/core/utils/singleton.py +21 -0
  53. tetra_rp/core/validation.py +44 -0
  54. tetra_rp/execute_class.py +319 -0
  55. tetra_rp/logger.py +34 -0
  56. tetra_rp/protos/__init__.py +0 -0
  57. tetra_rp/protos/remote_execution.py +148 -0
  58. tetra_rp/stubs/__init__.py +5 -0
  59. tetra_rp/stubs/live_serverless.py +155 -0
  60. tetra_rp/stubs/registry.py +117 -0
  61. tetra_rp/stubs/serverless.py +30 -0
  62. tetra_rp-0.17.1.dist-info/METADATA +976 -0
  63. tetra_rp-0.17.1.dist-info/RECORD +66 -0
  64. tetra_rp-0.17.1.dist-info/WHEEL +5 -0
  65. tetra_rp-0.17.1.dist-info/entry_points.txt +2 -0
  66. tetra_rp-0.17.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,370 @@
1
+ """Deployment environment management commands."""
2
+
3
+ import typer
4
+ from rich.console import Console
5
+ from rich.table import Table
6
+ from rich.panel import Panel
7
+ import questionary
8
+
9
+ from ..utils.deployment import (
10
+ get_deployment_environments,
11
+ create_deployment_environment,
12
+ remove_deployment_environment,
13
+ deploy_to_environment,
14
+ rollback_deployment,
15
+ get_environment_info,
16
+ )
17
+
18
+ console = Console()
19
+
20
+
21
+ def list_command():
22
+ """Show available deployment environments."""
23
+
24
+ console.print(
25
+ Panel(
26
+ "[yellow]The deploy command is coming soon.[/yellow]\n\n"
27
+ "This feature is under development and will be available in a future release.",
28
+ title="Coming Soon",
29
+ expand=False,
30
+ )
31
+ )
32
+ return
33
+
34
+ environments = get_deployment_environments()
35
+
36
+ if not environments:
37
+ console.print(
38
+ Panel(
39
+ "๐Ÿ“ฆ No deployment environments found\n\n"
40
+ "Create one with: [bold]runpod remote deploy new <name>[/bold]",
41
+ title="Deployment Environments",
42
+ expand=False,
43
+ )
44
+ )
45
+ return
46
+
47
+ table = Table(title="Deployment Environments")
48
+ table.add_column("Environment", style="cyan", no_wrap=True)
49
+ table.add_column("Status", justify="center")
50
+ table.add_column("Current Version", style="magenta")
51
+ table.add_column("Last Deployed", style="yellow")
52
+ table.add_column("URL", style="blue")
53
+
54
+ active_count = 0
55
+ idle_count = 0
56
+
57
+ for env_name, env_info in environments.items():
58
+ status = env_info.get("status", "Unknown")
59
+ if status == "active":
60
+ status_display = "๐ŸŸข Active"
61
+ active_count += 1
62
+ elif status == "idle":
63
+ status_display = "๐ŸŸก Idle"
64
+ idle_count += 1
65
+ else:
66
+ status_display = "๐Ÿ”ด Error"
67
+
68
+ table.add_row(
69
+ env_name,
70
+ status_display,
71
+ env_info.get("current_version", "N/A"),
72
+ env_info.get("last_deployed", "Never"),
73
+ env_info.get("url", "N/A"),
74
+ )
75
+
76
+ console.print(table)
77
+
78
+ # Summary
79
+ total = len(environments)
80
+ error_count = total - active_count - idle_count
81
+ summary = f"Total: {total} environments ({active_count} active"
82
+ if idle_count > 0:
83
+ summary += f", {idle_count} idle"
84
+ if error_count > 0:
85
+ summary += f", {error_count} error"
86
+ summary += ")"
87
+
88
+ console.print(f"\n{summary}")
89
+
90
+
91
+ def new_command(
92
+ name: str = typer.Argument(
93
+ ..., help="Name of the deployment environment to create"
94
+ ),
95
+ ):
96
+ """Create a new deployment environment."""
97
+
98
+ console.print(
99
+ Panel(
100
+ "[yellow]The deploy command is coming soon.[/yellow]\n\n"
101
+ "This feature is under development and will be available in a future release.",
102
+ title="Coming Soon",
103
+ expand=False,
104
+ )
105
+ )
106
+ return
107
+
108
+ environments = get_deployment_environments()
109
+
110
+ if name in environments:
111
+ console.print(f"Environment '{name}' already exists")
112
+ raise typer.Exit(1)
113
+
114
+ # Interactive configuration
115
+ config = {}
116
+
117
+ try:
118
+ config["region"] = questionary.select(
119
+ "Select region:",
120
+ choices=["us-east-1", "us-west-2", "eu-west-1", "ap-southeast-1"],
121
+ ).ask()
122
+
123
+ config["instance_type"] = questionary.select(
124
+ "Instance type:", choices=["A40", "A100", "H100", "RTX4090"]
125
+ ).ask()
126
+
127
+ config["auto_scale"] = questionary.confirm("Enable auto-scaling?").ask()
128
+
129
+ if not all([config["region"], config["instance_type"]]):
130
+ console.print("Configuration cancelled")
131
+ raise typer.Exit(1)
132
+
133
+ except KeyboardInterrupt:
134
+ console.print("\nEnvironment creation cancelled")
135
+ raise typer.Exit(1)
136
+
137
+ # Create environment
138
+ with console.status(f"Creating environment '{name}'..."):
139
+ create_deployment_environment(name, config)
140
+
141
+ # Success message
142
+ panel_content = f"Environment '[bold]{name}[/bold]' created successfully\n\n"
143
+ panel_content += f"Region: {config['region']}\n"
144
+ panel_content += f"Instance: {config['instance_type']}\n"
145
+ panel_content += f"Auto-scale: {'Enabled' if config['auto_scale'] else 'Disabled'}"
146
+
147
+ console.print(Panel(panel_content, title="๐Ÿš€ Environment Created", expand=False))
148
+
149
+ console.print(f"\nNext: [bold]flash deploy send {name}[/bold]")
150
+
151
+
152
+ def send_command(
153
+ name: str = typer.Argument(..., help="Name of the deployment environment"),
154
+ ):
155
+ """Deploy project to deployment environment."""
156
+
157
+ environments = get_deployment_environments()
158
+
159
+ if name not in environments:
160
+ console.print(f"Environment '{name}' not found")
161
+ console.print("Available environments:")
162
+ for env_name in environments.keys():
163
+ console.print(f" โ€ข {env_name}")
164
+ raise typer.Exit(1)
165
+
166
+ # Deploy with mock progress
167
+ console.print(f"๐Ÿš€ Deploying to '[bold]{name}[/bold]'...")
168
+
169
+ try:
170
+ result = deploy_to_environment(name)
171
+
172
+ panel_content = f"Deployed to '[bold]{name}[/bold]' successfully\n\n"
173
+ panel_content += f"Version: {result['version']}\n"
174
+ panel_content += f"URL: {result['url']}\n"
175
+ panel_content += "Status: ๐ŸŸข Active"
176
+
177
+ console.print(
178
+ Panel(panel_content, title="๐Ÿš€ Deployment Complete", expand=False)
179
+ )
180
+
181
+ except Exception as e:
182
+ console.print(f"Deployment failed: {e}")
183
+ raise typer.Exit(1)
184
+
185
+
186
+ def report_command(
187
+ name: str = typer.Argument(..., help="Name of the deployment environment"),
188
+ ):
189
+ """Show detailed environment status and metrics."""
190
+
191
+ environments = get_deployment_environments()
192
+
193
+ if name not in environments:
194
+ console.print(f"Environment '{name}' not found")
195
+ raise typer.Exit(1)
196
+
197
+ env_info = get_environment_info(name)
198
+
199
+ # Environment status
200
+ status = env_info.get("status", "unknown")
201
+ status_display = {
202
+ "active": "๐ŸŸข Active",
203
+ "idle": "๐ŸŸก Idle",
204
+ "error": "๐Ÿ”ด Error",
205
+ }.get(status, "โ“ Unknown")
206
+
207
+ # Main info panel
208
+ main_info = f"Status: {status_display}\n"
209
+ main_info += f"Current Version: {env_info.get('current_version', 'N/A')}\n"
210
+ main_info += f"URL: {env_info.get('url', 'N/A')}\n"
211
+ main_info += f"Last Deployed: {env_info.get('last_deployed', 'Never')}\n"
212
+ main_info += f"Uptime: {env_info.get('uptime', 'N/A')}"
213
+
214
+ console.print(
215
+ Panel(main_info, title=f"๐Ÿ“Š Environment Report: {name}", expand=False)
216
+ )
217
+
218
+ # Version history
219
+ versions = env_info.get("version_history", [])
220
+ if versions:
221
+ version_table = Table(title="Version History")
222
+ version_table.add_column("Version", style="cyan")
223
+ version_table.add_column("Status", justify="center")
224
+ version_table.add_column("Deployed", style="yellow")
225
+ version_table.add_column("Description", style="white")
226
+
227
+ for version in versions[:5]: # Show last 5 versions
228
+ version_status = (
229
+ "๐ŸŸข Current" if version.get("is_current") else "๐Ÿ“ฆ Previous"
230
+ )
231
+ version_table.add_row(
232
+ version.get("version", "N/A"),
233
+ version_status,
234
+ version.get("deployed_at", "N/A"),
235
+ version.get("description", "No description"),
236
+ )
237
+
238
+ console.print(version_table)
239
+
240
+ # Mock metrics
241
+ console.print("\n[bold]Metrics (Last 24h):[/bold]")
242
+ metrics_info = [
243
+ "โ€ข Requests: 145,234",
244
+ "โ€ข Avg Response Time: 245ms",
245
+ "โ€ข Error Rate: 0.02%",
246
+ "โ€ข CPU Usage: 45%",
247
+ "โ€ข Memory Usage: 62%",
248
+ ]
249
+
250
+ for metric in metrics_info:
251
+ console.print(f" {metric}")
252
+
253
+
254
+ def rollback_command(
255
+ name: str = typer.Argument(..., help="Name of the deployment environment"),
256
+ ):
257
+ """Rollback deployment to previous version."""
258
+
259
+ environments = get_deployment_environments()
260
+
261
+ if name not in environments:
262
+ console.print(f"Environment '{name}' not found")
263
+ raise typer.Exit(1)
264
+
265
+ env_info = get_environment_info(name)
266
+ versions = env_info.get("version_history", [])
267
+
268
+ if len(versions) < 2:
269
+ console.print("No previous versions available for rollback")
270
+ raise typer.Exit(1)
271
+
272
+ # Show available versions (excluding current)
273
+ previous_versions = [v for v in versions if not v.get("is_current")]
274
+
275
+ if not previous_versions:
276
+ console.print("No previous versions available for rollback")
277
+ raise typer.Exit(1)
278
+
279
+ try:
280
+ version_choices = [
281
+ f"{v['version']} - {v.get('description', 'No description')}"
282
+ for v in previous_versions[:5]
283
+ ]
284
+
285
+ selected = questionary.select(
286
+ "Select version to rollback to:", choices=version_choices
287
+ ).ask()
288
+
289
+ if not selected:
290
+ console.print("Rollback cancelled")
291
+ raise typer.Exit(1)
292
+
293
+ target_version = selected.split(" - ")[0]
294
+
295
+ # Confirmation
296
+ confirmed = questionary.confirm(
297
+ f"Rollback environment '{name}' to version {target_version}?"
298
+ ).ask()
299
+
300
+ if not confirmed:
301
+ console.print("Rollback cancelled")
302
+ raise typer.Exit(1)
303
+
304
+ except KeyboardInterrupt:
305
+ console.print("\nRollback cancelled")
306
+ raise typer.Exit(1)
307
+
308
+ # Perform rollback
309
+ with console.status(f"Rolling back to {target_version}..."):
310
+ rollback_deployment(name, target_version)
311
+
312
+ console.print(f"Rolled back to version {target_version}")
313
+ console.print(f"Environment '{name}' is now running the previous version.")
314
+
315
+
316
+ def remove_command(
317
+ name: str = typer.Argument(
318
+ ..., help="Name of the deployment environment to remove"
319
+ ),
320
+ ):
321
+ """Remove deployment environment."""
322
+
323
+ environments = get_deployment_environments()
324
+
325
+ if name not in environments:
326
+ console.print(f"Environment '{name}' not found")
327
+ raise typer.Exit(1)
328
+
329
+ env_info = get_environment_info(name)
330
+
331
+ # Show removal preview
332
+ preview_content = f"Environment: {name}\n"
333
+ preview_content += f"Status: {env_info.get('status', 'unknown')}\n"
334
+ preview_content += f"URL: {env_info.get('url', 'N/A')}\n"
335
+ preview_content += f"Current Version: {env_info.get('current_version', 'N/A')}\n\n"
336
+ preview_content += "โš ๏ธ This will permanently remove:\n"
337
+ preview_content += " โ€ข All deployment history\n"
338
+ preview_content += " โ€ข All associated resources\n"
339
+ preview_content += " โ€ข Environment configuration\n"
340
+ preview_content += " โ€ข Access URLs\n\n"
341
+ preview_content += "๐Ÿšจ This action cannot be undone!"
342
+
343
+ console.print(Panel(preview_content, title="โš ๏ธ Removal Preview", expand=False))
344
+
345
+ try:
346
+ # Double confirmation for safety
347
+ confirmed = questionary.confirm(
348
+ f"Are you sure you want to remove environment '{name}'?"
349
+ ).ask()
350
+
351
+ if not confirmed:
352
+ console.print("Removal cancelled")
353
+ raise typer.Exit(1)
354
+
355
+ # Type confirmation
356
+ typed_name = questionary.text(f"Type '{name}' to confirm removal:").ask()
357
+
358
+ if typed_name != name:
359
+ console.print("Confirmation failed - names do not match")
360
+ raise typer.Exit(1)
361
+
362
+ except KeyboardInterrupt:
363
+ console.print("\nRemoval cancelled")
364
+ raise typer.Exit(1)
365
+
366
+ # Remove environment
367
+ with console.status(f"Removing environment '{name}'..."):
368
+ remove_deployment_environment(name)
369
+
370
+ console.print(f"Environment '{name}' removed successfully")
@@ -0,0 +1,119 @@
1
+ """Project initialization command."""
2
+
3
+ from pathlib import Path
4
+ from typing import Optional
5
+
6
+ import typer
7
+ from rich.console import Console
8
+ from rich.panel import Panel
9
+ from rich.table import Table
10
+
11
+ from ..utils.skeleton import create_project_skeleton, detect_file_conflicts
12
+
13
+ console = Console()
14
+
15
+
16
+ def init_command(
17
+ project_name: Optional[str] = typer.Argument(
18
+ None, help="Project name or '.' for current directory"
19
+ ),
20
+ force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing files"),
21
+ ):
22
+ """Create new Flash project with Flash Server and GPU workers."""
23
+
24
+ # Determine target directory and initialization mode
25
+ if project_name is None or project_name == ".":
26
+ # Initialize in current directory
27
+ project_dir = Path.cwd()
28
+ is_current_dir = True
29
+ # Use current directory name as project name
30
+ actual_project_name = project_dir.name
31
+ else:
32
+ # Create new directory
33
+ project_dir = Path(project_name)
34
+ is_current_dir = False
35
+ actual_project_name = project_name
36
+
37
+ # Create project directory if needed
38
+ if not is_current_dir:
39
+ project_dir.mkdir(parents=True, exist_ok=True)
40
+
41
+ # Check for file conflicts in target directory
42
+ conflicts = detect_file_conflicts(project_dir)
43
+ should_overwrite = force # Start with force flag value
44
+
45
+ if conflicts and not force:
46
+ # Show warning and prompt user
47
+ console.print(
48
+ Panel(
49
+ "[yellow]Warning: The following files will be overwritten:[/yellow]\n\n"
50
+ + "\n".join(f" โ€ข {conflict}" for conflict in conflicts),
51
+ title="File Conflicts Detected",
52
+ expand=False,
53
+ )
54
+ )
55
+
56
+ # Prompt user for confirmation
57
+ proceed = typer.confirm("Continue and overwrite these files?", default=False)
58
+ if not proceed:
59
+ console.print("[yellow]Initialization aborted.[/yellow]")
60
+ raise typer.Exit(0)
61
+
62
+ # User confirmed, so we should overwrite
63
+ should_overwrite = True
64
+
65
+ # Create project skeleton
66
+ status_msg = (
67
+ "Initializing Flash project in current directory..."
68
+ if is_current_dir
69
+ else f"Creating Flash project '{project_name}'..."
70
+ )
71
+ with console.status(status_msg):
72
+ create_project_skeleton(project_dir, should_overwrite)
73
+
74
+ # Success output
75
+ if is_current_dir:
76
+ panel_content = f"Flash project '[bold]{actual_project_name}[/bold]' initialized in current directory!\n\n"
77
+ panel_content += "Project structure:\n"
78
+ panel_content += " ./\n"
79
+ else:
80
+ panel_content = f"Flash project '[bold]{actual_project_name}[/bold]' created successfully!\n\n"
81
+ panel_content += "Project structure:\n"
82
+ panel_content += f" {actual_project_name}/\n"
83
+
84
+ panel_content += " โ”œโ”€โ”€ main.py # Flash Server (FastAPI)\n"
85
+ panel_content += " โ”œโ”€โ”€ workers/\n"
86
+ panel_content += " โ”‚ โ”œโ”€โ”€ gpu/ # GPU worker\n"
87
+ panel_content += " โ”‚ โ””โ”€โ”€ cpu/ # CPU worker\n"
88
+ panel_content += " โ”œโ”€โ”€ .env.example\n"
89
+ panel_content += " โ”œโ”€โ”€ requirements.txt\n"
90
+ panel_content += " โ””โ”€โ”€ README.md\n"
91
+
92
+ title = "Project Initialized" if is_current_dir else "Project Created"
93
+ console.print(Panel(panel_content, title=title, expand=False))
94
+
95
+ # Next steps
96
+ console.print("\n[bold]Next steps:[/bold]")
97
+ steps_table = Table(show_header=False, box=None, padding=(0, 1))
98
+ steps_table.add_column("Step", style="bold cyan")
99
+ steps_table.add_column("Description")
100
+
101
+ step_num = 1
102
+ if not is_current_dir:
103
+ steps_table.add_row(f"{step_num}.", f"cd {actual_project_name}")
104
+ step_num += 1
105
+
106
+ steps_table.add_row(f"{step_num}.", "pip install -r requirements.txt")
107
+ step_num += 1
108
+ steps_table.add_row(f"{step_num}.", "cp .env.example .env")
109
+ step_num += 1
110
+ steps_table.add_row(f"{step_num}.", "Add your RUNPOD_API_KEY to .env")
111
+ step_num += 1
112
+ steps_table.add_row(f"{step_num}.", "flash run")
113
+
114
+ console.print(steps_table)
115
+
116
+ console.print("\n[bold]Get your API key:[/bold]")
117
+ console.print(" https://docs.runpod.io/get-started/api-keys")
118
+ console.print("\nVisit http://localhost:8888/docs after running")
119
+ console.print("\nCheck out the README.md for more")
@@ -0,0 +1,191 @@
1
+ """Resource management commands."""
2
+
3
+ import typer
4
+ from rich.console import Console
5
+ from rich.table import Table
6
+ from rich.panel import Panel
7
+ from rich.live import Live
8
+ from rich.progress import Progress, SpinnerColumn, TextColumn
9
+ import questionary
10
+ import time
11
+
12
+ from ...core.resources.resource_manager import ResourceManager
13
+
14
+ console = Console()
15
+
16
+
17
+ def report_command(
18
+ live: bool = typer.Option(False, "--live", "-l", help="Live updating status"),
19
+ refresh: int = typer.Option(
20
+ 2, "--refresh", "-r", help="Refresh interval for live mode"
21
+ ),
22
+ ):
23
+ """Show resource status dashboard."""
24
+
25
+ resource_manager = ResourceManager()
26
+
27
+ if live:
28
+ try:
29
+ with Live(
30
+ generate_resource_table(resource_manager),
31
+ console=console,
32
+ refresh_per_second=1 / refresh,
33
+ screen=True,
34
+ ) as live_display:
35
+ while True:
36
+ time.sleep(refresh)
37
+ live_display.update(generate_resource_table(resource_manager))
38
+ except KeyboardInterrupt:
39
+ console.print("\n๐Ÿ“Š Live monitoring stopped")
40
+ else:
41
+ table = generate_resource_table(resource_manager)
42
+ console.print(table)
43
+
44
+
45
+ def clean_command(
46
+ force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
47
+ ):
48
+ """Remove all tracked resources after confirmation."""
49
+
50
+ resource_manager = ResourceManager()
51
+ resources = resource_manager._resources
52
+
53
+ if not resources:
54
+ console.print("๐Ÿงน No resources to clean")
55
+ return
56
+
57
+ # Show cleanup preview
58
+ console.print(generate_cleanup_preview(resources))
59
+
60
+ # Confirmation unless forced
61
+ if not force:
62
+ try:
63
+ confirmed = questionary.confirm(
64
+ "Are you sure you want to clean all resources?"
65
+ ).ask()
66
+
67
+ if not confirmed:
68
+ console.print("Cleanup cancelled")
69
+ return
70
+ except KeyboardInterrupt:
71
+ console.print("\nCleanup cancelled")
72
+ return
73
+
74
+ # Clean resources with progress
75
+ with Progress(
76
+ SpinnerColumn(),
77
+ TextColumn("[progress.description]{task.description}"),
78
+ console=console,
79
+ ) as progress:
80
+ task = progress.add_task("Cleaning resources...", total=len(resources))
81
+
82
+ for uid in list(resources.keys()):
83
+ resource = resources[uid]
84
+ progress.update(
85
+ task, description=f"Removing {resource.__class__.__name__}..."
86
+ )
87
+
88
+ # Remove resource (this will also clean up remotely if needed)
89
+ resource_manager.remove_resource(uid)
90
+
91
+ progress.advance(task)
92
+ time.sleep(0.1) # Small delay for visual feedback
93
+
94
+ console.print("All resources cleaned successfully")
95
+
96
+
97
+ def generate_resource_table(resource_manager: ResourceManager) -> Panel:
98
+ """Generate a formatted table of resources."""
99
+
100
+ resources = resource_manager._resources
101
+
102
+ if not resources:
103
+ return Panel(
104
+ "๐Ÿ“Š No resources currently tracked\n\n"
105
+ "Resources will appear here after running your Tetra applications.",
106
+ title="Resource Status Report",
107
+ expand=False,
108
+ )
109
+
110
+ table = Table(title="Resource Status Report")
111
+ table.add_column("Resource ID", style="cyan", no_wrap=True)
112
+ table.add_column("Status", justify="center")
113
+ table.add_column("Type", style="magenta")
114
+ table.add_column("URL", style="blue")
115
+ table.add_column("Health", justify="center")
116
+
117
+ active_count = 0
118
+ error_count = 0
119
+
120
+ for uid, resource in resources.items():
121
+ # Determine status
122
+ try:
123
+ is_deployed = resource.is_deployed()
124
+ if is_deployed:
125
+ status = "๐ŸŸข Active"
126
+ active_count += 1
127
+ else:
128
+ status = "๐Ÿ”ด Inactive"
129
+ error_count += 1
130
+ except Exception:
131
+ status = "๐ŸŸก Unknown"
132
+
133
+ # Get resource info
134
+ resource_type = resource.__class__.__name__
135
+
136
+ try:
137
+ url = resource.url if hasattr(resource, "url") else "N/A"
138
+ except Exception:
139
+ url = "N/A"
140
+
141
+ # Health check (simplified for now)
142
+ health = "โœ“" if status == "๐ŸŸข Active" else "โœ—"
143
+
144
+ table.add_row(
145
+ uid[:20] + "..." if len(uid) > 20 else uid,
146
+ status,
147
+ resource_type,
148
+ url,
149
+ health,
150
+ )
151
+
152
+ # Summary
153
+ total = len(resources)
154
+ idle_count = total - active_count - error_count
155
+ summary = f"Total: {total} resources ({active_count} active"
156
+ if idle_count > 0:
157
+ summary += f", {idle_count} idle"
158
+ if error_count > 0:
159
+ summary += f", {error_count} error"
160
+ summary += ")"
161
+
162
+ return Panel(table, subtitle=summary, expand=False)
163
+
164
+
165
+ def generate_cleanup_preview(resources: dict) -> Panel:
166
+ """Generate a preview of resources to be cleaned."""
167
+
168
+ content = "The following resources will be removed:\n\n"
169
+
170
+ for uid, resource in resources.items():
171
+ resource_type = resource.__class__.__name__
172
+
173
+ try:
174
+ status = "Active" if resource.is_deployed() else "Inactive"
175
+ except Exception:
176
+ status = "Unknown"
177
+
178
+ try:
179
+ url = (
180
+ f" - {resource.url}"
181
+ if hasattr(resource, "url") and resource.url != "N/A"
182
+ else ""
183
+ )
184
+ except Exception:
185
+ url = ""
186
+
187
+ content += f" โ€ข {resource_type} ({status}){url}\n"
188
+
189
+ content += "\nโš ๏ธ This action cannot be undone!"
190
+
191
+ return Panel(content, title="๐Ÿงน Cleanup Preview", expand=False)