tetra-rp 0.11.0__tar.gz → 0.13.0__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.
Potentially problematic release.
This version of tetra-rp might be problematic. Click here for more details.
- {tetra_rp-0.11.0 → tetra_rp-0.13.0}/PKG-INFO +5 -1
- {tetra_rp-0.11.0 → tetra_rp-0.13.0}/README.md +2 -1
- {tetra_rp-0.11.0 → tetra_rp-0.13.0}/pyproject.toml +7 -1
- {tetra_rp-0.11.0 → tetra_rp-0.13.0}/src/tetra_rp/__init__.py +2 -0
- tetra_rp-0.13.0/src/tetra_rp/cli/commands/__init__.py +1 -0
- tetra_rp-0.13.0/src/tetra_rp/cli/commands/deploy.py +336 -0
- tetra_rp-0.13.0/src/tetra_rp/cli/commands/init.py +86 -0
- tetra_rp-0.13.0/src/tetra_rp/cli/commands/resource.py +191 -0
- tetra_rp-0.13.0/src/tetra_rp/cli/commands/run.py +122 -0
- tetra_rp-0.13.0/src/tetra_rp/cli/main.py +81 -0
- tetra_rp-0.13.0/src/tetra_rp/cli/templates/advanced/main.py +58 -0
- tetra_rp-0.13.0/src/tetra_rp/cli/templates/advanced/utils.py +24 -0
- tetra_rp-0.13.0/src/tetra_rp/cli/templates/basic/main.py +32 -0
- tetra_rp-0.13.0/src/tetra_rp/cli/templates/gpu-compute/main.py +64 -0
- tetra_rp-0.13.0/src/tetra_rp/cli/templates/web-api/api.py +67 -0
- tetra_rp-0.13.0/src/tetra_rp/cli/templates/web-api/main.py +42 -0
- tetra_rp-0.13.0/src/tetra_rp/cli/utils/__init__.py +1 -0
- tetra_rp-0.13.0/src/tetra_rp/cli/utils/deployment.py +172 -0
- tetra_rp-0.13.0/src/tetra_rp/cli/utils/skeleton.py +101 -0
- {tetra_rp-0.11.0 → tetra_rp-0.13.0}/src/tetra_rp/client.py +0 -6
- tetra_rp-0.13.0/src/tetra_rp/config.py +29 -0
- {tetra_rp-0.11.0 → tetra_rp-0.13.0}/src/tetra_rp/core/resources/__init__.py +3 -2
- tetra_rp-0.13.0/src/tetra_rp/core/resources/cpu.py +137 -0
- {tetra_rp-0.11.0 → tetra_rp-0.13.0}/src/tetra_rp/core/resources/gpu.py +29 -14
- tetra_rp-0.13.0/src/tetra_rp/core/resources/live_serverless.py +62 -0
- tetra_rp-0.13.0/src/tetra_rp/core/resources/resource_manager.py +121 -0
- {tetra_rp-0.11.0 → tetra_rp-0.13.0}/src/tetra_rp/core/resources/serverless.py +27 -46
- tetra_rp-0.13.0/src/tetra_rp/core/resources/serverless_cpu.py +154 -0
- tetra_rp-0.13.0/src/tetra_rp/core/utils/file_lock.py +260 -0
- tetra_rp-0.13.0/src/tetra_rp/core/utils/singleton.py +21 -0
- {tetra_rp-0.11.0 → tetra_rp-0.13.0}/src/tetra_rp/execute_class.py +0 -3
- tetra_rp-0.13.0/src/tetra_rp/protos/__init__.py +0 -0
- {tetra_rp-0.11.0 → tetra_rp-0.13.0}/src/tetra_rp/protos/remote_execution.py +0 -4
- {tetra_rp-0.11.0 → tetra_rp-0.13.0}/src/tetra_rp/stubs/live_serverless.py +11 -9
- {tetra_rp-0.11.0 → tetra_rp-0.13.0}/src/tetra_rp/stubs/registry.py +25 -14
- {tetra_rp-0.11.0 → tetra_rp-0.13.0}/src/tetra_rp.egg-info/PKG-INFO +5 -1
- {tetra_rp-0.11.0 → tetra_rp-0.13.0}/src/tetra_rp.egg-info/SOURCES.txt +20 -0
- tetra_rp-0.13.0/src/tetra_rp.egg-info/entry_points.txt +2 -0
- {tetra_rp-0.11.0 → tetra_rp-0.13.0}/src/tetra_rp.egg-info/requires.txt +3 -0
- tetra_rp-0.11.0/src/tetra_rp/core/resources/cpu.py +0 -34
- tetra_rp-0.11.0/src/tetra_rp/core/resources/live_serverless.py +0 -36
- tetra_rp-0.11.0/src/tetra_rp/core/resources/resource_manager.py +0 -80
- tetra_rp-0.11.0/src/tetra_rp/core/utils/singleton.py +0 -7
- {tetra_rp-0.11.0 → tetra_rp-0.13.0}/setup.cfg +0 -0
- {tetra_rp-0.11.0/src/tetra_rp/core → tetra_rp-0.13.0/src/tetra_rp/cli}/__init__.py +0 -0
- {tetra_rp-0.11.0/src/tetra_rp/core/utils → tetra_rp-0.13.0/src/tetra_rp/core}/__init__.py +0 -0
- {tetra_rp-0.11.0 → tetra_rp-0.13.0}/src/tetra_rp/core/api/__init__.py +0 -0
- {tetra_rp-0.11.0 → tetra_rp-0.13.0}/src/tetra_rp/core/api/runpod.py +0 -0
- {tetra_rp-0.11.0 → tetra_rp-0.13.0}/src/tetra_rp/core/resources/base.py +0 -0
- {tetra_rp-0.11.0 → tetra_rp-0.13.0}/src/tetra_rp/core/resources/cloud.py +0 -0
- {tetra_rp-0.11.0 → tetra_rp-0.13.0}/src/tetra_rp/core/resources/constants.py +0 -0
- {tetra_rp-0.11.0 → tetra_rp-0.13.0}/src/tetra_rp/core/resources/environment.py +0 -0
- {tetra_rp-0.11.0 → tetra_rp-0.13.0}/src/tetra_rp/core/resources/network_volume.py +0 -0
- {tetra_rp-0.11.0 → tetra_rp-0.13.0}/src/tetra_rp/core/resources/template.py +0 -0
- {tetra_rp-0.11.0 → tetra_rp-0.13.0}/src/tetra_rp/core/resources/utils.py +0 -0
- {tetra_rp-0.11.0/src/tetra_rp/protos → tetra_rp-0.13.0/src/tetra_rp/core/utils}/__init__.py +0 -0
- {tetra_rp-0.11.0 → tetra_rp-0.13.0}/src/tetra_rp/core/utils/backoff.py +0 -0
- {tetra_rp-0.11.0 → tetra_rp-0.13.0}/src/tetra_rp/core/utils/constants.py +0 -0
- {tetra_rp-0.11.0 → tetra_rp-0.13.0}/src/tetra_rp/core/utils/json.py +0 -0
- {tetra_rp-0.11.0 → tetra_rp-0.13.0}/src/tetra_rp/core/utils/lru_cache.py +0 -0
- {tetra_rp-0.11.0 → tetra_rp-0.13.0}/src/tetra_rp/logger.py +0 -0
- {tetra_rp-0.11.0 → tetra_rp-0.13.0}/src/tetra_rp/stubs/__init__.py +0 -0
- {tetra_rp-0.11.0 → tetra_rp-0.13.0}/src/tetra_rp/stubs/serverless.py +0 -0
- {tetra_rp-0.11.0 → tetra_rp-0.13.0}/src/tetra_rp.egg-info/dependency_links.txt +0 -0
- {tetra_rp-0.11.0 → tetra_rp-0.13.0}/src/tetra_rp.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tetra_rp
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.13.0
|
|
4
4
|
Summary: A Python library for distributed inference and serving of machine learning models
|
|
5
5
|
Author-email: Marut Pandya <pandyamarut@gmail.com>, Patrick Rachford <prachford@icloud.com>, Dean Quinanola <dean.quinanola@runpod.io>
|
|
6
6
|
License: MIT
|
|
@@ -14,6 +14,9 @@ Requires-Dist: cloudpickle>=3.1.1
|
|
|
14
14
|
Requires-Dist: runpod
|
|
15
15
|
Requires-Dist: python-dotenv>=1.0.0
|
|
16
16
|
Requires-Dist: pydantic>=2.0.0
|
|
17
|
+
Requires-Dist: rich>=14.0.0
|
|
18
|
+
Requires-Dist: typer>=0.12.0
|
|
19
|
+
Requires-Dist: questionary>=2.0.0
|
|
17
20
|
|
|
18
21
|
# Tetra: Serverless computing for AI workloads
|
|
19
22
|
|
|
@@ -359,6 +362,7 @@ if __name__ == "__main__":
|
|
|
359
362
|
```python
|
|
360
363
|
import asyncio
|
|
361
364
|
from tetra_rp import remote, LiveServerless, GpuGroup, PodTemplate
|
|
365
|
+
import base64
|
|
362
366
|
|
|
363
367
|
# Advanced GPU configuration with consolidated template overrides
|
|
364
368
|
sd_config = LiveServerless(
|
|
@@ -342,6 +342,7 @@ if __name__ == "__main__":
|
|
|
342
342
|
```python
|
|
343
343
|
import asyncio
|
|
344
344
|
from tetra_rp import remote, LiveServerless, GpuGroup, PodTemplate
|
|
345
|
+
import base64
|
|
345
346
|
|
|
346
347
|
# Advanced GPU configuration with consolidated template overrides
|
|
347
348
|
sd_config = LiveServerless(
|
|
@@ -787,4 +788,4 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
|
|
|
787
788
|
<p align="center">
|
|
788
789
|
<a href="https://github.com/runpod/tetra-rp">Tetra</a> •
|
|
789
790
|
<a href="https://runpod.io">Runpod</a>
|
|
790
|
-
</p>
|
|
791
|
+
</p>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "tetra_rp"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.13.0"
|
|
4
4
|
description = "A Python library for distributed inference and serving of machine learning models"
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "Marut Pandya", email = "pandyamarut@gmail.com" },
|
|
@@ -22,6 +22,9 @@ dependencies = [
|
|
|
22
22
|
"runpod",
|
|
23
23
|
"python-dotenv>=1.0.0",
|
|
24
24
|
"pydantic>=2.0.0",
|
|
25
|
+
"rich>=14.0.0",
|
|
26
|
+
"typer>=0.12.0",
|
|
27
|
+
"questionary>=2.0.0",
|
|
25
28
|
]
|
|
26
29
|
|
|
27
30
|
[dependency-groups]
|
|
@@ -37,6 +40,9 @@ test = [
|
|
|
37
40
|
"twine>=6.1.0",
|
|
38
41
|
]
|
|
39
42
|
|
|
43
|
+
[project.scripts]
|
|
44
|
+
flash = "tetra_rp.cli.main:app"
|
|
45
|
+
|
|
40
46
|
[build-system]
|
|
41
47
|
requires = ["setuptools>=42", "wheel"]
|
|
42
48
|
build-backend = "setuptools.build_meta"
|
|
@@ -13,6 +13,7 @@ from .client import remote # noqa: E402
|
|
|
13
13
|
from .core.resources import ( # noqa: E402
|
|
14
14
|
CpuServerlessEndpoint,
|
|
15
15
|
CpuInstanceType,
|
|
16
|
+
CpuLiveServerless,
|
|
16
17
|
CudaVersion,
|
|
17
18
|
DataCenter,
|
|
18
19
|
GpuGroup,
|
|
@@ -29,6 +30,7 @@ __all__ = [
|
|
|
29
30
|
"remote",
|
|
30
31
|
"CpuServerlessEndpoint",
|
|
31
32
|
"CpuInstanceType",
|
|
33
|
+
"CpuLiveServerless",
|
|
32
34
|
"CudaVersion",
|
|
33
35
|
"DataCenter",
|
|
34
36
|
"GpuGroup",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI command modules."""
|
|
@@ -0,0 +1,336 @@
|
|
|
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
|
+
environments = get_deployment_environments()
|
|
25
|
+
|
|
26
|
+
if not environments:
|
|
27
|
+
console.print(
|
|
28
|
+
Panel(
|
|
29
|
+
"📦 No deployment environments found\n\n"
|
|
30
|
+
"Create one with: [bold]runpod remote deploy new <name>[/bold]",
|
|
31
|
+
title="Deployment Environments",
|
|
32
|
+
expand=False,
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
table = Table(title="Deployment Environments")
|
|
38
|
+
table.add_column("Environment", style="cyan", no_wrap=True)
|
|
39
|
+
table.add_column("Status", justify="center")
|
|
40
|
+
table.add_column("Current Version", style="magenta")
|
|
41
|
+
table.add_column("Last Deployed", style="yellow")
|
|
42
|
+
table.add_column("URL", style="blue")
|
|
43
|
+
|
|
44
|
+
active_count = 0
|
|
45
|
+
idle_count = 0
|
|
46
|
+
|
|
47
|
+
for env_name, env_info in environments.items():
|
|
48
|
+
status = env_info.get("status", "Unknown")
|
|
49
|
+
if status == "active":
|
|
50
|
+
status_display = "🟢 Active"
|
|
51
|
+
active_count += 1
|
|
52
|
+
elif status == "idle":
|
|
53
|
+
status_display = "🟡 Idle"
|
|
54
|
+
idle_count += 1
|
|
55
|
+
else:
|
|
56
|
+
status_display = "🔴 Error"
|
|
57
|
+
|
|
58
|
+
table.add_row(
|
|
59
|
+
env_name,
|
|
60
|
+
status_display,
|
|
61
|
+
env_info.get("current_version", "N/A"),
|
|
62
|
+
env_info.get("last_deployed", "Never"),
|
|
63
|
+
env_info.get("url", "N/A"),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
console.print(table)
|
|
67
|
+
|
|
68
|
+
# Summary
|
|
69
|
+
total = len(environments)
|
|
70
|
+
error_count = total - active_count - idle_count
|
|
71
|
+
summary = f"Total: {total} environments ({active_count} active"
|
|
72
|
+
if idle_count > 0:
|
|
73
|
+
summary += f", {idle_count} idle"
|
|
74
|
+
if error_count > 0:
|
|
75
|
+
summary += f", {error_count} error"
|
|
76
|
+
summary += ")"
|
|
77
|
+
|
|
78
|
+
console.print(f"\n{summary}")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def new_command(name: str):
|
|
82
|
+
"""Create a new deployment environment."""
|
|
83
|
+
|
|
84
|
+
environments = get_deployment_environments()
|
|
85
|
+
|
|
86
|
+
if name in environments:
|
|
87
|
+
console.print(f"Environment '{name}' already exists")
|
|
88
|
+
raise typer.Exit(1)
|
|
89
|
+
|
|
90
|
+
# Interactive configuration
|
|
91
|
+
config = {}
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
config["region"] = questionary.select(
|
|
95
|
+
"Select region:",
|
|
96
|
+
choices=["us-east-1", "us-west-2", "eu-west-1", "ap-southeast-1"],
|
|
97
|
+
).ask()
|
|
98
|
+
|
|
99
|
+
config["instance_type"] = questionary.select(
|
|
100
|
+
"Instance type:", choices=["A40", "A100", "H100", "RTX4090"]
|
|
101
|
+
).ask()
|
|
102
|
+
|
|
103
|
+
config["auto_scale"] = questionary.confirm("Enable auto-scaling?").ask()
|
|
104
|
+
|
|
105
|
+
if not all([config["region"], config["instance_type"]]):
|
|
106
|
+
console.print("Configuration cancelled")
|
|
107
|
+
raise typer.Exit(1)
|
|
108
|
+
|
|
109
|
+
except KeyboardInterrupt:
|
|
110
|
+
console.print("\nEnvironment creation cancelled")
|
|
111
|
+
raise typer.Exit(1)
|
|
112
|
+
|
|
113
|
+
# Create environment
|
|
114
|
+
with console.status(f"Creating environment '{name}'..."):
|
|
115
|
+
create_deployment_environment(name, config)
|
|
116
|
+
|
|
117
|
+
# Success message
|
|
118
|
+
panel_content = f"Environment '[bold]{name}[/bold]' created successfully\n\n"
|
|
119
|
+
panel_content += f"Region: {config['region']}\n"
|
|
120
|
+
panel_content += f"Instance: {config['instance_type']}\n"
|
|
121
|
+
panel_content += f"Auto-scale: {'Enabled' if config['auto_scale'] else 'Disabled'}"
|
|
122
|
+
|
|
123
|
+
console.print(Panel(panel_content, title="🚀 Environment Created", expand=False))
|
|
124
|
+
|
|
125
|
+
console.print(f"\nNext: [bold]flash deploy send {name}[/bold]")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def send_command(name: str):
|
|
129
|
+
"""Deploy project to deployment environment."""
|
|
130
|
+
|
|
131
|
+
environments = get_deployment_environments()
|
|
132
|
+
|
|
133
|
+
if name not in environments:
|
|
134
|
+
console.print(f"Environment '{name}' not found")
|
|
135
|
+
console.print("Available environments:")
|
|
136
|
+
for env_name in environments.keys():
|
|
137
|
+
console.print(f" • {env_name}")
|
|
138
|
+
raise typer.Exit(1)
|
|
139
|
+
|
|
140
|
+
# Deploy with mock progress
|
|
141
|
+
console.print(f"🚀 Deploying to '[bold]{name}[/bold]'...")
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
result = deploy_to_environment(name)
|
|
145
|
+
|
|
146
|
+
panel_content = f"Deployed to '[bold]{name}[/bold]' successfully\n\n"
|
|
147
|
+
panel_content += f"Version: {result['version']}\n"
|
|
148
|
+
panel_content += f"URL: {result['url']}\n"
|
|
149
|
+
panel_content += "Status: 🟢 Active"
|
|
150
|
+
|
|
151
|
+
console.print(
|
|
152
|
+
Panel(panel_content, title="🚀 Deployment Complete", expand=False)
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
except Exception as e:
|
|
156
|
+
console.print(f"Deployment failed: {e}")
|
|
157
|
+
raise typer.Exit(1)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def report_command(name: str):
|
|
161
|
+
"""Show detailed environment status and metrics."""
|
|
162
|
+
|
|
163
|
+
environments = get_deployment_environments()
|
|
164
|
+
|
|
165
|
+
if name not in environments:
|
|
166
|
+
console.print(f"Environment '{name}' not found")
|
|
167
|
+
raise typer.Exit(1)
|
|
168
|
+
|
|
169
|
+
env_info = get_environment_info(name)
|
|
170
|
+
|
|
171
|
+
# Environment status
|
|
172
|
+
status = env_info.get("status", "unknown")
|
|
173
|
+
status_display = {
|
|
174
|
+
"active": "🟢 Active",
|
|
175
|
+
"idle": "🟡 Idle",
|
|
176
|
+
"error": "🔴 Error",
|
|
177
|
+
}.get(status, "❓ Unknown")
|
|
178
|
+
|
|
179
|
+
# Main info panel
|
|
180
|
+
main_info = f"Status: {status_display}\n"
|
|
181
|
+
main_info += f"Current Version: {env_info.get('current_version', 'N/A')}\n"
|
|
182
|
+
main_info += f"URL: {env_info.get('url', 'N/A')}\n"
|
|
183
|
+
main_info += f"Last Deployed: {env_info.get('last_deployed', 'Never')}\n"
|
|
184
|
+
main_info += f"Uptime: {env_info.get('uptime', 'N/A')}"
|
|
185
|
+
|
|
186
|
+
console.print(
|
|
187
|
+
Panel(main_info, title=f"📊 Environment Report: {name}", expand=False)
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# Version history
|
|
191
|
+
versions = env_info.get("version_history", [])
|
|
192
|
+
if versions:
|
|
193
|
+
version_table = Table(title="Version History")
|
|
194
|
+
version_table.add_column("Version", style="cyan")
|
|
195
|
+
version_table.add_column("Status", justify="center")
|
|
196
|
+
version_table.add_column("Deployed", style="yellow")
|
|
197
|
+
version_table.add_column("Description", style="white")
|
|
198
|
+
|
|
199
|
+
for version in versions[:5]: # Show last 5 versions
|
|
200
|
+
version_status = (
|
|
201
|
+
"🟢 Current" if version.get("is_current") else "📦 Previous"
|
|
202
|
+
)
|
|
203
|
+
version_table.add_row(
|
|
204
|
+
version.get("version", "N/A"),
|
|
205
|
+
version_status,
|
|
206
|
+
version.get("deployed_at", "N/A"),
|
|
207
|
+
version.get("description", "No description"),
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
console.print(version_table)
|
|
211
|
+
|
|
212
|
+
# Mock metrics
|
|
213
|
+
console.print("\n[bold]Metrics (Last 24h):[/bold]")
|
|
214
|
+
metrics_info = [
|
|
215
|
+
"• Requests: 145,234",
|
|
216
|
+
"• Avg Response Time: 245ms",
|
|
217
|
+
"• Error Rate: 0.02%",
|
|
218
|
+
"• CPU Usage: 45%",
|
|
219
|
+
"• Memory Usage: 62%",
|
|
220
|
+
]
|
|
221
|
+
|
|
222
|
+
for metric in metrics_info:
|
|
223
|
+
console.print(f" {metric}")
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def rollback_command(name: str):
|
|
227
|
+
"""Rollback deployment to previous version."""
|
|
228
|
+
|
|
229
|
+
environments = get_deployment_environments()
|
|
230
|
+
|
|
231
|
+
if name not in environments:
|
|
232
|
+
console.print(f"Environment '{name}' not found")
|
|
233
|
+
raise typer.Exit(1)
|
|
234
|
+
|
|
235
|
+
env_info = get_environment_info(name)
|
|
236
|
+
versions = env_info.get("version_history", [])
|
|
237
|
+
|
|
238
|
+
if len(versions) < 2:
|
|
239
|
+
console.print("No previous versions available for rollback")
|
|
240
|
+
raise typer.Exit(1)
|
|
241
|
+
|
|
242
|
+
# Show available versions (excluding current)
|
|
243
|
+
previous_versions = [v for v in versions if not v.get("is_current")]
|
|
244
|
+
|
|
245
|
+
if not previous_versions:
|
|
246
|
+
console.print("No previous versions available for rollback")
|
|
247
|
+
raise typer.Exit(1)
|
|
248
|
+
|
|
249
|
+
try:
|
|
250
|
+
version_choices = [
|
|
251
|
+
f"{v['version']} - {v.get('description', 'No description')}"
|
|
252
|
+
for v in previous_versions[:5]
|
|
253
|
+
]
|
|
254
|
+
|
|
255
|
+
selected = questionary.select(
|
|
256
|
+
"Select version to rollback to:", choices=version_choices
|
|
257
|
+
).ask()
|
|
258
|
+
|
|
259
|
+
if not selected:
|
|
260
|
+
console.print("Rollback cancelled")
|
|
261
|
+
raise typer.Exit(1)
|
|
262
|
+
|
|
263
|
+
target_version = selected.split(" - ")[0]
|
|
264
|
+
|
|
265
|
+
# Confirmation
|
|
266
|
+
confirmed = questionary.confirm(
|
|
267
|
+
f"Rollback environment '{name}' to version {target_version}?"
|
|
268
|
+
).ask()
|
|
269
|
+
|
|
270
|
+
if not confirmed:
|
|
271
|
+
console.print("Rollback cancelled")
|
|
272
|
+
raise typer.Exit(1)
|
|
273
|
+
|
|
274
|
+
except KeyboardInterrupt:
|
|
275
|
+
console.print("\nRollback cancelled")
|
|
276
|
+
raise typer.Exit(1)
|
|
277
|
+
|
|
278
|
+
# Perform rollback
|
|
279
|
+
with console.status(f"Rolling back to {target_version}..."):
|
|
280
|
+
rollback_deployment(name, target_version)
|
|
281
|
+
|
|
282
|
+
console.print(f"Rolled back to version {target_version}")
|
|
283
|
+
console.print(f"Environment '{name}' is now running the previous version.")
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def remove_command(name: str):
|
|
287
|
+
"""Remove deployment environment."""
|
|
288
|
+
|
|
289
|
+
environments = get_deployment_environments()
|
|
290
|
+
|
|
291
|
+
if name not in environments:
|
|
292
|
+
console.print(f"Environment '{name}' not found")
|
|
293
|
+
raise typer.Exit(1)
|
|
294
|
+
|
|
295
|
+
env_info = get_environment_info(name)
|
|
296
|
+
|
|
297
|
+
# Show removal preview
|
|
298
|
+
preview_content = f"Environment: {name}\n"
|
|
299
|
+
preview_content += f"Status: {env_info.get('status', 'unknown')}\n"
|
|
300
|
+
preview_content += f"URL: {env_info.get('url', 'N/A')}\n"
|
|
301
|
+
preview_content += f"Current Version: {env_info.get('current_version', 'N/A')}\n\n"
|
|
302
|
+
preview_content += "⚠️ This will permanently remove:\n"
|
|
303
|
+
preview_content += " • All deployment history\n"
|
|
304
|
+
preview_content += " • All associated resources\n"
|
|
305
|
+
preview_content += " • Environment configuration\n"
|
|
306
|
+
preview_content += " • Access URLs\n\n"
|
|
307
|
+
preview_content += "🚨 This action cannot be undone!"
|
|
308
|
+
|
|
309
|
+
console.print(Panel(preview_content, title="⚠️ Removal Preview", expand=False))
|
|
310
|
+
|
|
311
|
+
try:
|
|
312
|
+
# Double confirmation for safety
|
|
313
|
+
confirmed = questionary.confirm(
|
|
314
|
+
f"Are you sure you want to remove environment '{name}'?"
|
|
315
|
+
).ask()
|
|
316
|
+
|
|
317
|
+
if not confirmed:
|
|
318
|
+
console.print("Removal cancelled")
|
|
319
|
+
raise typer.Exit(1)
|
|
320
|
+
|
|
321
|
+
# Type confirmation
|
|
322
|
+
typed_name = questionary.text(f"Type '{name}' to confirm removal:").ask()
|
|
323
|
+
|
|
324
|
+
if typed_name != name:
|
|
325
|
+
console.print("Confirmation failed - names do not match")
|
|
326
|
+
raise typer.Exit(1)
|
|
327
|
+
|
|
328
|
+
except KeyboardInterrupt:
|
|
329
|
+
console.print("\nRemoval cancelled")
|
|
330
|
+
raise typer.Exit(1)
|
|
331
|
+
|
|
332
|
+
# Remove environment
|
|
333
|
+
with console.status(f"Removing environment '{name}'..."):
|
|
334
|
+
remove_deployment_environment(name)
|
|
335
|
+
|
|
336
|
+
console.print(f"Environment '{name}' removed successfully")
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Project initialization command."""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
from typing import Optional
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.panel import Panel
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
import questionary
|
|
9
|
+
|
|
10
|
+
from tetra_rp.config import get_paths
|
|
11
|
+
from ..utils.skeleton import create_project_skeleton, get_available_templates
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def init_command(
|
|
17
|
+
template: Optional[str] = typer.Option(
|
|
18
|
+
None, "--template", "-t", help="Project template to use"
|
|
19
|
+
),
|
|
20
|
+
force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing files"),
|
|
21
|
+
):
|
|
22
|
+
"""Create skeleton application with starter files."""
|
|
23
|
+
|
|
24
|
+
# Check if we're already in a Tetra project
|
|
25
|
+
paths = get_paths()
|
|
26
|
+
if paths.tetra_dir.exists() and not force:
|
|
27
|
+
console.print("Already in a Tetra project directory")
|
|
28
|
+
console.print("Use --force to overwrite existing configuration")
|
|
29
|
+
raise typer.Exit(1)
|
|
30
|
+
|
|
31
|
+
# Get available templates
|
|
32
|
+
available_templates = get_available_templates()
|
|
33
|
+
|
|
34
|
+
# Interactive template selection if not provided
|
|
35
|
+
if not template:
|
|
36
|
+
template_choices = []
|
|
37
|
+
for name, info in available_templates.items():
|
|
38
|
+
template_choices.append(f"{name} - {info['description']}")
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
selected = questionary.select(
|
|
42
|
+
"Choose a project template:", choices=template_choices
|
|
43
|
+
).ask()
|
|
44
|
+
|
|
45
|
+
if not selected:
|
|
46
|
+
console.print("Template selection cancelled")
|
|
47
|
+
raise typer.Exit(1)
|
|
48
|
+
|
|
49
|
+
template = selected.split(" - ")[0]
|
|
50
|
+
except KeyboardInterrupt:
|
|
51
|
+
console.print("\nTemplate selection cancelled")
|
|
52
|
+
raise typer.Exit(1)
|
|
53
|
+
|
|
54
|
+
# Validate template choice
|
|
55
|
+
if template not in available_templates:
|
|
56
|
+
console.print(f"Unknown template: {template}")
|
|
57
|
+
console.print("Available templates:")
|
|
58
|
+
for name, info in available_templates.items():
|
|
59
|
+
console.print(f" • {name} - {info['description']}")
|
|
60
|
+
raise typer.Exit(1)
|
|
61
|
+
|
|
62
|
+
# Create project skeleton
|
|
63
|
+
template_info = available_templates[template]
|
|
64
|
+
|
|
65
|
+
with console.status(f"Creating project with {template} template..."):
|
|
66
|
+
created_files = create_project_skeleton(template, template_info, force)
|
|
67
|
+
|
|
68
|
+
# Success output
|
|
69
|
+
panel_content = f"Project initialized with [bold]{template}[/bold] template\n\n"
|
|
70
|
+
panel_content += "Created files:\n"
|
|
71
|
+
for file_path in created_files:
|
|
72
|
+
panel_content += f" • {file_path}\n"
|
|
73
|
+
|
|
74
|
+
console.print(Panel(panel_content, title="Project Initialized", expand=False))
|
|
75
|
+
|
|
76
|
+
# Next steps
|
|
77
|
+
console.print("\n[bold]Next steps:[/bold]")
|
|
78
|
+
steps_table = Table(show_header=False, box=None, padding=(0, 1))
|
|
79
|
+
steps_table.add_column("Step", style="bold cyan")
|
|
80
|
+
steps_table.add_column("Description")
|
|
81
|
+
|
|
82
|
+
steps_table.add_row("1.", "Edit .env with your RunPod API key")
|
|
83
|
+
steps_table.add_row("2.", "Install dependencies: pip install -r requirements.txt")
|
|
84
|
+
steps_table.add_row("3.", "Run your project: flash run")
|
|
85
|
+
|
|
86
|
+
console.print(steps_table)
|
|
@@ -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)
|