tetra-rp 0.13.0__py3-none-any.whl → 0.14.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.
Potentially problematic release.
This version of tetra-rp might be problematic. Click here for more details.
- tetra_rp/cli/commands/build.py +501 -0
- tetra_rp/cli/commands/init.py +106 -57
- tetra_rp/cli/commands/run.py +65 -87
- tetra_rp/cli/main.py +6 -4
- tetra_rp/cli/utils/conda.py +127 -0
- tetra_rp/cli/utils/ignore.py +139 -0
- tetra_rp/cli/utils/skeleton.py +317 -71
- tetra_rp/client.py +35 -8
- tetra_rp/core/resources/cpu.py +9 -0
- tetra_rp/core/resources/serverless_cpu.py +10 -2
- {tetra_rp-0.13.0.dist-info → tetra_rp-0.14.0.dist-info}/METADATA +2 -1
- {tetra_rp-0.13.0.dist-info → tetra_rp-0.14.0.dist-info}/RECORD +15 -18
- tetra_rp/cli/templates/advanced/main.py +0 -58
- tetra_rp/cli/templates/advanced/utils.py +0 -24
- tetra_rp/cli/templates/basic/main.py +0 -32
- tetra_rp/cli/templates/gpu-compute/main.py +0 -64
- tetra_rp/cli/templates/web-api/api.py +0 -67
- tetra_rp/cli/templates/web-api/main.py +0 -42
- {tetra_rp-0.13.0.dist-info → tetra_rp-0.14.0.dist-info}/WHEEL +0 -0
- {tetra_rp-0.13.0.dist-info → tetra_rp-0.14.0.dist-info}/entry_points.txt +0 -0
- {tetra_rp-0.13.0.dist-info → tetra_rp-0.14.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
"""Flash build command - Package Flash applications for deployment."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
import tarfile
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.panel import Panel
|
|
13
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
14
|
+
from rich.table import Table
|
|
15
|
+
|
|
16
|
+
from ..utils.ignore import get_file_tree, load_ignore_patterns
|
|
17
|
+
|
|
18
|
+
console = Console()
|
|
19
|
+
|
|
20
|
+
# Constants
|
|
21
|
+
PIP_INSTALL_TIMEOUT_SECONDS = 600 # 10 minute timeout for pip install
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def build_command(
|
|
25
|
+
no_deps: bool = typer.Option(
|
|
26
|
+
False, "--no-deps", help="Skip transitive dependencies during pip install"
|
|
27
|
+
),
|
|
28
|
+
keep_build: bool = typer.Option(
|
|
29
|
+
False, "--keep-build", help="Keep .build directory after creating archive"
|
|
30
|
+
),
|
|
31
|
+
output_name: str | None = typer.Option(
|
|
32
|
+
None, "--output", "-o", help="Custom archive name (default: archive.tar.gz)"
|
|
33
|
+
),
|
|
34
|
+
):
|
|
35
|
+
"""
|
|
36
|
+
Build Flash application for deployment.
|
|
37
|
+
|
|
38
|
+
Packages the application code and dependencies into a self-contained tarball,
|
|
39
|
+
similar to AWS Lambda packaging. All pip packages are installed as local modules.
|
|
40
|
+
|
|
41
|
+
Examples:
|
|
42
|
+
flash build # Build with all dependencies
|
|
43
|
+
flash build --no-deps # Skip transitive dependencies
|
|
44
|
+
flash build --keep-build # Keep temporary build directory
|
|
45
|
+
flash build -o my-app.tar.gz # Custom archive name
|
|
46
|
+
"""
|
|
47
|
+
try:
|
|
48
|
+
# Validate project structure
|
|
49
|
+
project_dir, app_name = discover_flash_project()
|
|
50
|
+
|
|
51
|
+
if not validate_project_structure(project_dir):
|
|
52
|
+
console.print("[red]Error:[/red] Not a valid Flash project")
|
|
53
|
+
console.print("Run [bold]flash init[/bold] to create a Flash project")
|
|
54
|
+
raise typer.Exit(1)
|
|
55
|
+
|
|
56
|
+
# Display configuration
|
|
57
|
+
_display_build_config(project_dir, app_name, no_deps, keep_build, output_name)
|
|
58
|
+
|
|
59
|
+
# Execute build
|
|
60
|
+
with Progress(
|
|
61
|
+
SpinnerColumn(),
|
|
62
|
+
TextColumn("[progress.description]{task.description}"),
|
|
63
|
+
console=console,
|
|
64
|
+
) as progress:
|
|
65
|
+
# Load ignore patterns
|
|
66
|
+
ignore_task = progress.add_task("Loading ignore patterns...")
|
|
67
|
+
spec = load_ignore_patterns(project_dir)
|
|
68
|
+
progress.update(ignore_task, description="[green]✓ Loaded ignore patterns")
|
|
69
|
+
progress.stop_task(ignore_task)
|
|
70
|
+
|
|
71
|
+
# Collect files
|
|
72
|
+
collect_task = progress.add_task("Collecting project files...")
|
|
73
|
+
files = get_file_tree(project_dir, spec)
|
|
74
|
+
progress.update(
|
|
75
|
+
collect_task,
|
|
76
|
+
description=f"[green]✓ Found {len(files)} files to package",
|
|
77
|
+
)
|
|
78
|
+
progress.stop_task(collect_task)
|
|
79
|
+
|
|
80
|
+
# Create build directory
|
|
81
|
+
build_task = progress.add_task("Creating build directory...")
|
|
82
|
+
build_dir = create_build_directory(project_dir, app_name)
|
|
83
|
+
progress.update(
|
|
84
|
+
build_task,
|
|
85
|
+
description=f"[green]✓ Created .tetra/.build/{app_name}/",
|
|
86
|
+
)
|
|
87
|
+
progress.stop_task(build_task)
|
|
88
|
+
|
|
89
|
+
# Copy files
|
|
90
|
+
copy_task = progress.add_task("Copying project files...")
|
|
91
|
+
copy_project_files(files, project_dir, build_dir)
|
|
92
|
+
progress.update(
|
|
93
|
+
copy_task, description=f"[green]✓ Copied {len(files)} files"
|
|
94
|
+
)
|
|
95
|
+
progress.stop_task(copy_task)
|
|
96
|
+
|
|
97
|
+
# Install dependencies
|
|
98
|
+
deps_task = progress.add_task("Installing dependencies...")
|
|
99
|
+
requirements = collect_requirements(project_dir, build_dir)
|
|
100
|
+
|
|
101
|
+
if not requirements:
|
|
102
|
+
progress.update(
|
|
103
|
+
deps_task,
|
|
104
|
+
description="[yellow]⚠ No dependencies found",
|
|
105
|
+
)
|
|
106
|
+
else:
|
|
107
|
+
progress.update(
|
|
108
|
+
deps_task,
|
|
109
|
+
description=f"Installing {len(requirements)} packages...",
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
success = install_dependencies(build_dir, requirements, no_deps)
|
|
113
|
+
|
|
114
|
+
if not success:
|
|
115
|
+
progress.stop_task(deps_task)
|
|
116
|
+
console.print("[red]Error:[/red] Failed to install dependencies")
|
|
117
|
+
raise typer.Exit(1)
|
|
118
|
+
|
|
119
|
+
progress.update(
|
|
120
|
+
deps_task,
|
|
121
|
+
description=f"[green]✓ Installed {len(requirements)} packages",
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
progress.stop_task(deps_task)
|
|
125
|
+
|
|
126
|
+
# Create archive
|
|
127
|
+
archive_task = progress.add_task("Creating archive...")
|
|
128
|
+
archive_name = output_name or "archive.tar.gz"
|
|
129
|
+
archive_path = project_dir / ".tetra" / archive_name
|
|
130
|
+
|
|
131
|
+
create_tarball(build_dir, archive_path, app_name)
|
|
132
|
+
|
|
133
|
+
# Get archive size
|
|
134
|
+
size_mb = archive_path.stat().st_size / (1024 * 1024)
|
|
135
|
+
|
|
136
|
+
progress.update(
|
|
137
|
+
archive_task,
|
|
138
|
+
description=f"[green]✓ Created {archive_name} ({size_mb:.1f} MB)",
|
|
139
|
+
)
|
|
140
|
+
progress.stop_task(archive_task)
|
|
141
|
+
|
|
142
|
+
# Cleanup
|
|
143
|
+
if not keep_build:
|
|
144
|
+
cleanup_task = progress.add_task("Cleaning up...")
|
|
145
|
+
cleanup_build_directory(build_dir.parent)
|
|
146
|
+
progress.update(
|
|
147
|
+
cleanup_task, description="[green]✓ Removed .build directory"
|
|
148
|
+
)
|
|
149
|
+
progress.stop_task(cleanup_task)
|
|
150
|
+
|
|
151
|
+
# Success summary
|
|
152
|
+
_display_build_summary(archive_path, app_name, len(files), len(requirements))
|
|
153
|
+
|
|
154
|
+
except KeyboardInterrupt:
|
|
155
|
+
console.print("\n[yellow]Build cancelled by user[/yellow]")
|
|
156
|
+
raise typer.Exit(1)
|
|
157
|
+
except Exception as e:
|
|
158
|
+
console.print(f"\n[red]Build failed:[/red] {e}")
|
|
159
|
+
import traceback
|
|
160
|
+
|
|
161
|
+
console.print(traceback.format_exc())
|
|
162
|
+
raise typer.Exit(1)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def discover_flash_project() -> tuple[Path, str]:
|
|
166
|
+
"""
|
|
167
|
+
Discover Flash project directory and app name.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
Tuple of (project_dir, app_name)
|
|
171
|
+
|
|
172
|
+
Raises:
|
|
173
|
+
typer.Exit: If not in a Flash project directory
|
|
174
|
+
"""
|
|
175
|
+
project_dir = Path.cwd()
|
|
176
|
+
app_name = project_dir.name
|
|
177
|
+
|
|
178
|
+
return project_dir, app_name
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def validate_project_structure(project_dir: Path) -> bool:
|
|
182
|
+
"""
|
|
183
|
+
Validate that directory is a Flash project.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
project_dir: Directory to validate
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
True if valid Flash project
|
|
190
|
+
"""
|
|
191
|
+
main_py = project_dir / "main.py"
|
|
192
|
+
|
|
193
|
+
if not main_py.exists():
|
|
194
|
+
console.print(f"[red]Error:[/red] main.py not found in {project_dir}")
|
|
195
|
+
return False
|
|
196
|
+
|
|
197
|
+
# Check if main.py has FastAPI app
|
|
198
|
+
try:
|
|
199
|
+
content = main_py.read_text(encoding="utf-8")
|
|
200
|
+
if "FastAPI" not in content:
|
|
201
|
+
console.print(
|
|
202
|
+
"[yellow]Warning:[/yellow] main.py does not appear to have a FastAPI app"
|
|
203
|
+
)
|
|
204
|
+
except Exception:
|
|
205
|
+
pass
|
|
206
|
+
|
|
207
|
+
return True
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def create_build_directory(project_dir: Path, app_name: str) -> Path:
|
|
211
|
+
"""
|
|
212
|
+
Create .tetra/.build/{app_name}/ directory.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
project_dir: Flash project directory
|
|
216
|
+
app_name: Application name
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
Path to build directory
|
|
220
|
+
"""
|
|
221
|
+
tetra_dir = project_dir / ".tetra"
|
|
222
|
+
tetra_dir.mkdir(exist_ok=True)
|
|
223
|
+
|
|
224
|
+
build_base = tetra_dir / ".build"
|
|
225
|
+
build_dir = build_base / app_name
|
|
226
|
+
|
|
227
|
+
# Remove existing build directory
|
|
228
|
+
if build_dir.exists():
|
|
229
|
+
shutil.rmtree(build_dir)
|
|
230
|
+
|
|
231
|
+
build_dir.mkdir(parents=True, exist_ok=True)
|
|
232
|
+
|
|
233
|
+
return build_dir
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def copy_project_files(files: list[Path], source_dir: Path, dest_dir: Path) -> None:
|
|
237
|
+
"""
|
|
238
|
+
Copy project files to build directory.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
files: List of files to copy
|
|
242
|
+
source_dir: Source directory
|
|
243
|
+
dest_dir: Destination directory
|
|
244
|
+
"""
|
|
245
|
+
for file_path in files:
|
|
246
|
+
# Get relative path
|
|
247
|
+
rel_path = file_path.relative_to(source_dir)
|
|
248
|
+
|
|
249
|
+
# Create destination path
|
|
250
|
+
dest_path = dest_dir / rel_path
|
|
251
|
+
|
|
252
|
+
# Create parent directories
|
|
253
|
+
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
|
254
|
+
|
|
255
|
+
# Copy file
|
|
256
|
+
shutil.copy2(file_path, dest_path)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def collect_requirements(project_dir: Path, build_dir: Path) -> list[str]:
|
|
260
|
+
"""
|
|
261
|
+
Collect all requirements from requirements.txt and @remote decorators.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
project_dir: Flash project directory
|
|
265
|
+
build_dir: Build directory (to check for workers)
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
List of requirement strings
|
|
269
|
+
"""
|
|
270
|
+
requirements = []
|
|
271
|
+
|
|
272
|
+
# Load requirements.txt
|
|
273
|
+
req_file = project_dir / "requirements.txt"
|
|
274
|
+
if req_file.exists():
|
|
275
|
+
try:
|
|
276
|
+
content = req_file.read_text(encoding="utf-8")
|
|
277
|
+
for line in content.splitlines():
|
|
278
|
+
line = line.strip()
|
|
279
|
+
# Skip empty lines and comments
|
|
280
|
+
if line and not line.startswith("#"):
|
|
281
|
+
requirements.append(line)
|
|
282
|
+
except Exception as e:
|
|
283
|
+
console.print(
|
|
284
|
+
f"[yellow]Warning:[/yellow] Failed to read requirements.txt: {e}"
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
# Extract dependencies from @remote decorators
|
|
288
|
+
workers_dir = build_dir / "workers"
|
|
289
|
+
if workers_dir.exists():
|
|
290
|
+
remote_deps = extract_remote_dependencies(workers_dir)
|
|
291
|
+
requirements.extend(remote_deps)
|
|
292
|
+
|
|
293
|
+
# Remove duplicates while preserving order
|
|
294
|
+
seen = set()
|
|
295
|
+
unique_requirements = []
|
|
296
|
+
for req in requirements:
|
|
297
|
+
if req not in seen:
|
|
298
|
+
seen.add(req)
|
|
299
|
+
unique_requirements.append(req)
|
|
300
|
+
|
|
301
|
+
return unique_requirements
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def extract_remote_dependencies(workers_dir: Path) -> list[str]:
|
|
305
|
+
"""
|
|
306
|
+
Extract dependencies from @remote decorators in worker files.
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
workers_dir: Path to workers directory
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
List of dependency strings
|
|
313
|
+
"""
|
|
314
|
+
dependencies = []
|
|
315
|
+
|
|
316
|
+
for py_file in workers_dir.glob("*.py"):
|
|
317
|
+
if py_file.name == "__init__.py":
|
|
318
|
+
continue
|
|
319
|
+
|
|
320
|
+
try:
|
|
321
|
+
tree = ast.parse(py_file.read_text(encoding="utf-8"))
|
|
322
|
+
|
|
323
|
+
for node in ast.walk(tree):
|
|
324
|
+
if isinstance(node, (ast.ClassDef, ast.FunctionDef)):
|
|
325
|
+
for decorator in node.decorator_list:
|
|
326
|
+
if isinstance(decorator, ast.Call):
|
|
327
|
+
func_name = None
|
|
328
|
+
if isinstance(decorator.func, ast.Name):
|
|
329
|
+
func_name = decorator.func.id
|
|
330
|
+
elif isinstance(decorator.func, ast.Attribute):
|
|
331
|
+
func_name = decorator.func.attr
|
|
332
|
+
|
|
333
|
+
if func_name == "remote":
|
|
334
|
+
# Extract dependencies keyword argument
|
|
335
|
+
for keyword in decorator.keywords:
|
|
336
|
+
if keyword.arg == "dependencies":
|
|
337
|
+
if isinstance(keyword.value, ast.List):
|
|
338
|
+
for elt in keyword.value.elts:
|
|
339
|
+
if isinstance(elt, ast.Constant):
|
|
340
|
+
dependencies.append(elt.value)
|
|
341
|
+
|
|
342
|
+
except Exception as e:
|
|
343
|
+
console.print(
|
|
344
|
+
f"[yellow]Warning:[/yellow] Failed to parse {py_file.name}: {e}"
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
return dependencies
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def install_dependencies(
|
|
351
|
+
build_dir: Path, requirements: list[str], no_deps: bool
|
|
352
|
+
) -> bool:
|
|
353
|
+
"""
|
|
354
|
+
Install dependencies to build directory using pip.
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
build_dir: Build directory (pip --target)
|
|
358
|
+
requirements: List of requirements to install
|
|
359
|
+
no_deps: If True, skip transitive dependencies
|
|
360
|
+
|
|
361
|
+
Returns:
|
|
362
|
+
True if successful
|
|
363
|
+
"""
|
|
364
|
+
if not requirements:
|
|
365
|
+
return True
|
|
366
|
+
|
|
367
|
+
# Use only sys.executable -m pip to ensure correct environment
|
|
368
|
+
pip_cmd = [sys.executable, "-m", "pip"]
|
|
369
|
+
|
|
370
|
+
# Check if pip is available
|
|
371
|
+
try:
|
|
372
|
+
result = subprocess.run(
|
|
373
|
+
pip_cmd + ["--version"],
|
|
374
|
+
capture_output=True,
|
|
375
|
+
timeout=5,
|
|
376
|
+
)
|
|
377
|
+
if result.returncode != 0:
|
|
378
|
+
console.print(
|
|
379
|
+
"[red]Error:[/red] pip not found in current Python environment"
|
|
380
|
+
)
|
|
381
|
+
return False
|
|
382
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
|
383
|
+
console.print("[red]Error:[/red] pip not found in current Python environment")
|
|
384
|
+
return False
|
|
385
|
+
|
|
386
|
+
cmd = pip_cmd + [
|
|
387
|
+
"install",
|
|
388
|
+
"--target",
|
|
389
|
+
str(build_dir),
|
|
390
|
+
"--upgrade",
|
|
391
|
+
]
|
|
392
|
+
|
|
393
|
+
if no_deps:
|
|
394
|
+
cmd.append("--no-deps")
|
|
395
|
+
|
|
396
|
+
cmd.extend(requirements)
|
|
397
|
+
|
|
398
|
+
try:
|
|
399
|
+
result = subprocess.run(
|
|
400
|
+
cmd,
|
|
401
|
+
capture_output=True,
|
|
402
|
+
text=True,
|
|
403
|
+
timeout=PIP_INSTALL_TIMEOUT_SECONDS,
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
if result.returncode != 0:
|
|
407
|
+
console.print(f"[red]pip install failed:[/red]\n{result.stderr}")
|
|
408
|
+
return False
|
|
409
|
+
|
|
410
|
+
return True
|
|
411
|
+
|
|
412
|
+
except subprocess.TimeoutExpired:
|
|
413
|
+
console.print(
|
|
414
|
+
f"[red]pip install timed out ({PIP_INSTALL_TIMEOUT_SECONDS} seconds)[/red]"
|
|
415
|
+
)
|
|
416
|
+
return False
|
|
417
|
+
except Exception as e:
|
|
418
|
+
console.print(f"[red]pip install error:[/red] {e}")
|
|
419
|
+
return False
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def create_tarball(build_dir: Path, output_path: Path, app_name: str) -> None:
|
|
423
|
+
"""
|
|
424
|
+
Create gzipped tarball of build directory.
|
|
425
|
+
|
|
426
|
+
Args:
|
|
427
|
+
build_dir: Build directory to archive
|
|
428
|
+
output_path: Output archive path
|
|
429
|
+
app_name: Application name (used as archive root)
|
|
430
|
+
"""
|
|
431
|
+
# Remove existing archive
|
|
432
|
+
if output_path.exists():
|
|
433
|
+
output_path.unlink()
|
|
434
|
+
|
|
435
|
+
# Create tarball with app_name as root directory
|
|
436
|
+
with tarfile.open(output_path, "w:gz") as tar:
|
|
437
|
+
tar.add(build_dir, arcname=app_name)
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def cleanup_build_directory(build_base: Path) -> None:
|
|
441
|
+
"""
|
|
442
|
+
Remove build directory.
|
|
443
|
+
|
|
444
|
+
Args:
|
|
445
|
+
build_base: .build directory to remove
|
|
446
|
+
"""
|
|
447
|
+
if build_base.exists():
|
|
448
|
+
shutil.rmtree(build_base)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def _display_build_config(
|
|
452
|
+
project_dir: Path,
|
|
453
|
+
app_name: str,
|
|
454
|
+
no_deps: bool,
|
|
455
|
+
keep_build: bool,
|
|
456
|
+
output_name: str | None,
|
|
457
|
+
):
|
|
458
|
+
"""Display build configuration."""
|
|
459
|
+
archive_name = output_name or "archive.tar.gz"
|
|
460
|
+
|
|
461
|
+
console.print(
|
|
462
|
+
Panel(
|
|
463
|
+
f"[bold]Project:[/bold] {app_name}\n"
|
|
464
|
+
f"[bold]Directory:[/bold] {project_dir}\n"
|
|
465
|
+
f"[bold]Archive:[/bold] .tetra/{archive_name}\n"
|
|
466
|
+
f"[bold]Skip transitive deps:[/bold] {no_deps}\n"
|
|
467
|
+
f"[bold]Keep build dir:[/bold] {keep_build}",
|
|
468
|
+
title="Flash Build Configuration",
|
|
469
|
+
expand=False,
|
|
470
|
+
)
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _display_build_summary(
|
|
475
|
+
archive_path: Path, app_name: str, file_count: int, dep_count: int
|
|
476
|
+
):
|
|
477
|
+
"""Display build summary."""
|
|
478
|
+
size_mb = archive_path.stat().st_size / (1024 * 1024)
|
|
479
|
+
|
|
480
|
+
summary = Table(show_header=False, box=None)
|
|
481
|
+
summary.add_column("Item", style="bold")
|
|
482
|
+
summary.add_column("Value", style="cyan")
|
|
483
|
+
|
|
484
|
+
summary.add_row("Application", app_name)
|
|
485
|
+
summary.add_row("Files packaged", str(file_count))
|
|
486
|
+
summary.add_row("Dependencies", str(dep_count))
|
|
487
|
+
summary.add_row("Archive", str(archive_path.relative_to(Path.cwd())))
|
|
488
|
+
summary.add_row("Size", f"{size_mb:.1f} MB")
|
|
489
|
+
|
|
490
|
+
console.print("\n")
|
|
491
|
+
console.print(summary)
|
|
492
|
+
|
|
493
|
+
console.print(
|
|
494
|
+
Panel(
|
|
495
|
+
f"[bold]{app_name}[/bold] built successfully!\n\n"
|
|
496
|
+
f"Archive ready for deployment.",
|
|
497
|
+
title="✓ Build Complete",
|
|
498
|
+
expand=False,
|
|
499
|
+
border_style="green",
|
|
500
|
+
)
|
|
501
|
+
)
|
tetra_rp/cli/commands/init.py
CHANGED
|
@@ -1,77 +1,119 @@
|
|
|
1
1
|
"""Project initialization command."""
|
|
2
2
|
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
3
5
|
import typer
|
|
4
|
-
from typing import Optional
|
|
5
6
|
from rich.console import Console
|
|
6
7
|
from rich.panel import Panel
|
|
7
8
|
from rich.table import Table
|
|
8
|
-
import questionary
|
|
9
9
|
|
|
10
|
-
from
|
|
11
|
-
from ..utils.
|
|
10
|
+
from ..utils.skeleton import create_project_skeleton
|
|
11
|
+
from ..utils.conda import (
|
|
12
|
+
check_conda_available,
|
|
13
|
+
create_conda_environment,
|
|
14
|
+
install_packages_in_env,
|
|
15
|
+
environment_exists,
|
|
16
|
+
get_activation_command,
|
|
17
|
+
)
|
|
12
18
|
|
|
13
19
|
console = Console()
|
|
14
20
|
|
|
21
|
+
# Required packages for flash run to work smoothly
|
|
22
|
+
REQUIRED_PACKAGES = [
|
|
23
|
+
"fastapi>=0.104.0",
|
|
24
|
+
"uvicorn[standard]>=0.24.0",
|
|
25
|
+
"python-dotenv>=1.0.0",
|
|
26
|
+
"pydantic>=2.0.0",
|
|
27
|
+
"aiohttp>=3.9.0",
|
|
28
|
+
]
|
|
29
|
+
|
|
15
30
|
|
|
16
31
|
def init_command(
|
|
17
|
-
|
|
18
|
-
|
|
32
|
+
project_name: str = typer.Argument(..., help="Project name"),
|
|
33
|
+
no_env: bool = typer.Option(
|
|
34
|
+
False, "--no-env", help="Skip conda environment creation"
|
|
35
|
+
),
|
|
36
|
+
force: bool = typer.Option(
|
|
37
|
+
False, "--force", "-f", help="Overwrite existing directory"
|
|
19
38
|
),
|
|
20
|
-
force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing files"),
|
|
21
39
|
):
|
|
22
|
-
"""Create
|
|
40
|
+
"""Create new Flash project with Flash Server and GPU workers."""
|
|
23
41
|
|
|
24
|
-
#
|
|
25
|
-
|
|
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)
|
|
42
|
+
# Create project directory
|
|
43
|
+
project_dir = Path(project_name)
|
|
30
44
|
|
|
31
|
-
|
|
32
|
-
|
|
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']}")
|
|
45
|
+
if project_dir.exists() and not force:
|
|
46
|
+
console.print(f"Directory '{project_name}' already exists")
|
|
47
|
+
console.print("Use --force to overwrite")
|
|
60
48
|
raise typer.Exit(1)
|
|
61
49
|
|
|
62
|
-
# Create project
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
with console.status(f"Creating project
|
|
66
|
-
|
|
50
|
+
# Create project directory
|
|
51
|
+
project_dir.mkdir(parents=True, exist_ok=True)
|
|
52
|
+
|
|
53
|
+
with console.status(f"Creating Flash project '{project_name}'..."):
|
|
54
|
+
create_project_skeleton(project_dir, force)
|
|
55
|
+
|
|
56
|
+
# Create conda environment if requested
|
|
57
|
+
env_created = False
|
|
58
|
+
if not no_env:
|
|
59
|
+
if not check_conda_available():
|
|
60
|
+
console.print(
|
|
61
|
+
"[yellow]Warning: conda not found. Skipping environment creation.[/yellow]"
|
|
62
|
+
)
|
|
63
|
+
console.print(
|
|
64
|
+
"Install Miniconda or Anaconda, or use --no-env flag to skip this step."
|
|
65
|
+
)
|
|
66
|
+
else:
|
|
67
|
+
# Check if environment already exists
|
|
68
|
+
if environment_exists(project_name):
|
|
69
|
+
console.print(
|
|
70
|
+
f"[yellow]Conda environment '{project_name}' already exists. Skipping creation.[/yellow]"
|
|
71
|
+
)
|
|
72
|
+
env_created = True
|
|
73
|
+
else:
|
|
74
|
+
# Create conda environment
|
|
75
|
+
with console.status(f"Creating conda environment '{project_name}'..."):
|
|
76
|
+
success, message = create_conda_environment(project_name)
|
|
77
|
+
|
|
78
|
+
if not success:
|
|
79
|
+
console.print(f"[yellow]Warning: {message}[/yellow]")
|
|
80
|
+
console.print(
|
|
81
|
+
"You can manually create the environment and install dependencies."
|
|
82
|
+
)
|
|
83
|
+
else:
|
|
84
|
+
env_created = True
|
|
85
|
+
|
|
86
|
+
# Install required packages
|
|
87
|
+
with console.status("Installing dependencies..."):
|
|
88
|
+
success, message = install_packages_in_env(
|
|
89
|
+
project_name, REQUIRED_PACKAGES, use_pip=True
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if not success:
|
|
93
|
+
console.print(f"[yellow]Warning: {message}[/yellow]")
|
|
94
|
+
console.print(
|
|
95
|
+
"You can manually install dependencies: pip install -r requirements.txt"
|
|
96
|
+
)
|
|
67
97
|
|
|
68
98
|
# Success output
|
|
69
|
-
panel_content =
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
99
|
+
panel_content = (
|
|
100
|
+
f"Flash project '[bold]{project_name}[/bold]' created successfully!\n\n"
|
|
101
|
+
)
|
|
102
|
+
panel_content += "Project structure:\n"
|
|
103
|
+
panel_content += f" {project_name}/\n"
|
|
104
|
+
panel_content += " ├── main.py # Flash Server (FastAPI)\n"
|
|
105
|
+
panel_content += " ├── workers/ # GPU workers\n"
|
|
106
|
+
panel_content += " │ └── example_worker.py\n"
|
|
107
|
+
panel_content += " ├── .env.example\n"
|
|
108
|
+
panel_content += " ├── requirements.txt\n"
|
|
109
|
+
panel_content += " └── README.md\n"
|
|
110
|
+
|
|
111
|
+
if env_created:
|
|
112
|
+
panel_content += (
|
|
113
|
+
f"\nConda environment '[bold]{project_name}[/bold]' created and configured"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
console.print(Panel(panel_content, title="Project Created", expand=False))
|
|
75
117
|
|
|
76
118
|
# Next steps
|
|
77
119
|
console.print("\n[bold]Next steps:[/bold]")
|
|
@@ -79,8 +121,15 @@ def init_command(
|
|
|
79
121
|
steps_table.add_column("Step", style="bold cyan")
|
|
80
122
|
steps_table.add_column("Description")
|
|
81
123
|
|
|
82
|
-
steps_table.add_row("1.", "
|
|
83
|
-
|
|
84
|
-
|
|
124
|
+
steps_table.add_row("1.", f"cd {project_name}")
|
|
125
|
+
|
|
126
|
+
if env_created:
|
|
127
|
+
steps_table.add_row("2.", f"{get_activation_command(project_name)}")
|
|
128
|
+
steps_table.add_row("3.", "cp .env.example .env # Add your RUNPOD_API_KEY")
|
|
129
|
+
steps_table.add_row("4.", "flash run")
|
|
130
|
+
else:
|
|
131
|
+
steps_table.add_row("2.", "pip install -r requirements.txt")
|
|
132
|
+
steps_table.add_row("3.", "cp .env.example .env # Add your RUNPOD_API_KEY")
|
|
133
|
+
steps_table.add_row("4.", "flash run")
|
|
85
134
|
|
|
86
135
|
console.print(steps_table)
|