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
tetra_rp/__init__.py ADDED
@@ -0,0 +1,43 @@
1
+ # Load .env vars from file
2
+ # before everything else
3
+ from dotenv import load_dotenv
4
+
5
+ load_dotenv()
6
+
7
+
8
+ from .logger import setup_logging # noqa: E402
9
+
10
+ setup_logging()
11
+
12
+ from .client import remote # noqa: E402
13
+ from .core.resources import ( # noqa: E402
14
+ CpuServerlessEndpoint,
15
+ CpuInstanceType,
16
+ CpuLiveServerless,
17
+ CudaVersion,
18
+ DataCenter,
19
+ GpuGroup,
20
+ LiveServerless,
21
+ PodTemplate,
22
+ ResourceManager,
23
+ ServerlessEndpoint,
24
+ ServerlessType,
25
+ NetworkVolume,
26
+ )
27
+
28
+
29
+ __all__ = [
30
+ "remote",
31
+ "CpuServerlessEndpoint",
32
+ "CpuInstanceType",
33
+ "CpuLiveServerless",
34
+ "CudaVersion",
35
+ "DataCenter",
36
+ "GpuGroup",
37
+ "LiveServerless",
38
+ "PodTemplate",
39
+ "ResourceManager",
40
+ "ServerlessEndpoint",
41
+ "ServerlessType",
42
+ "NetworkVolume",
43
+ ]
File without changes
@@ -0,0 +1 @@
1
+ """CLI command modules."""
@@ -0,0 +1,534 @@
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
+ console.print(
48
+ Panel(
49
+ "[yellow]The build command is coming soon.[/yellow]\n\n"
50
+ "This feature is under development and will be available in a future release.",
51
+ title="Coming Soon",
52
+ expand=False,
53
+ )
54
+ )
55
+ return
56
+
57
+ try:
58
+ # Validate project structure
59
+ project_dir, app_name = discover_flash_project()
60
+
61
+ if not validate_project_structure(project_dir):
62
+ console.print("[red]Error:[/red] Not a valid Flash project")
63
+ console.print("Run [bold]flash init[/bold] to create a Flash project")
64
+ raise typer.Exit(1)
65
+
66
+ # Display configuration
67
+ _display_build_config(project_dir, app_name, no_deps, keep_build, output_name)
68
+
69
+ # Execute build
70
+ with Progress(
71
+ SpinnerColumn(),
72
+ TextColumn("[progress.description]{task.description}"),
73
+ console=console,
74
+ ) as progress:
75
+ # Load ignore patterns
76
+ ignore_task = progress.add_task("Loading ignore patterns...")
77
+ spec = load_ignore_patterns(project_dir)
78
+ progress.update(ignore_task, description="[green]✓ Loaded ignore patterns")
79
+ progress.stop_task(ignore_task)
80
+
81
+ # Collect files
82
+ collect_task = progress.add_task("Collecting project files...")
83
+ files = get_file_tree(project_dir, spec)
84
+ progress.update(
85
+ collect_task,
86
+ description=f"[green]✓ Found {len(files)} files to package",
87
+ )
88
+ progress.stop_task(collect_task)
89
+
90
+ # Create build directory
91
+ build_task = progress.add_task("Creating build directory...")
92
+ build_dir = create_build_directory(project_dir, app_name)
93
+ progress.update(
94
+ build_task,
95
+ description=f"[green]✓ Created .tetra/.build/{app_name}/",
96
+ )
97
+ progress.stop_task(build_task)
98
+
99
+ # Copy files
100
+ copy_task = progress.add_task("Copying project files...")
101
+ copy_project_files(files, project_dir, build_dir)
102
+ progress.update(
103
+ copy_task, description=f"[green]✓ Copied {len(files)} files"
104
+ )
105
+ progress.stop_task(copy_task)
106
+
107
+ # Install dependencies
108
+ deps_task = progress.add_task("Installing dependencies...")
109
+ requirements = collect_requirements(project_dir, build_dir)
110
+
111
+ if not requirements:
112
+ progress.update(
113
+ deps_task,
114
+ description="[yellow]⚠ No dependencies found",
115
+ )
116
+ else:
117
+ progress.update(
118
+ deps_task,
119
+ description=f"Installing {len(requirements)} packages...",
120
+ )
121
+
122
+ success = install_dependencies(build_dir, requirements, no_deps)
123
+
124
+ if not success:
125
+ progress.stop_task(deps_task)
126
+ console.print("[red]Error:[/red] Failed to install dependencies")
127
+ raise typer.Exit(1)
128
+
129
+ progress.update(
130
+ deps_task,
131
+ description=f"[green]✓ Installed {len(requirements)} packages",
132
+ )
133
+
134
+ progress.stop_task(deps_task)
135
+
136
+ # Create archive
137
+ archive_task = progress.add_task("Creating archive...")
138
+ archive_name = output_name or "archive.tar.gz"
139
+ archive_path = project_dir / ".tetra" / archive_name
140
+
141
+ create_tarball(build_dir, archive_path, app_name)
142
+
143
+ # Get archive size
144
+ size_mb = archive_path.stat().st_size / (1024 * 1024)
145
+
146
+ progress.update(
147
+ archive_task,
148
+ description=f"[green]✓ Created {archive_name} ({size_mb:.1f} MB)",
149
+ )
150
+ progress.stop_task(archive_task)
151
+
152
+ # Cleanup
153
+ if not keep_build:
154
+ cleanup_task = progress.add_task("Cleaning up...")
155
+ cleanup_build_directory(build_dir.parent)
156
+ progress.update(
157
+ cleanup_task, description="[green]✓ Removed .build directory"
158
+ )
159
+ progress.stop_task(cleanup_task)
160
+
161
+ # Success summary
162
+ _display_build_summary(archive_path, app_name, len(files), len(requirements))
163
+
164
+ except KeyboardInterrupt:
165
+ console.print("\n[yellow]Build cancelled by user[/yellow]")
166
+ raise typer.Exit(1)
167
+ except Exception as e:
168
+ console.print(f"\n[red]Build failed:[/red] {e}")
169
+ import traceback
170
+
171
+ console.print(traceback.format_exc())
172
+ raise typer.Exit(1)
173
+
174
+
175
+ def discover_flash_project() -> tuple[Path, str]:
176
+ """
177
+ Discover Flash project directory and app name.
178
+
179
+ Returns:
180
+ Tuple of (project_dir, app_name)
181
+
182
+ Raises:
183
+ typer.Exit: If not in a Flash project directory
184
+ """
185
+ project_dir = Path.cwd()
186
+ app_name = project_dir.name
187
+
188
+ return project_dir, app_name
189
+
190
+
191
+ def validate_project_structure(project_dir: Path) -> bool:
192
+ """
193
+ Validate that directory is a Flash project.
194
+
195
+ Args:
196
+ project_dir: Directory to validate
197
+
198
+ Returns:
199
+ True if valid Flash project
200
+ """
201
+ main_py = project_dir / "main.py"
202
+
203
+ if not main_py.exists():
204
+ console.print(f"[red]Error:[/red] main.py not found in {project_dir}")
205
+ return False
206
+
207
+ # Check if main.py has FastAPI app
208
+ try:
209
+ content = main_py.read_text(encoding="utf-8")
210
+ if "FastAPI" not in content:
211
+ console.print(
212
+ "[yellow]Warning:[/yellow] main.py does not appear to have a FastAPI app"
213
+ )
214
+ except Exception:
215
+ pass
216
+
217
+ return True
218
+
219
+
220
+ def create_build_directory(project_dir: Path, app_name: str) -> Path:
221
+ """
222
+ Create .tetra/.build/{app_name}/ directory.
223
+
224
+ Args:
225
+ project_dir: Flash project directory
226
+ app_name: Application name
227
+
228
+ Returns:
229
+ Path to build directory
230
+ """
231
+ tetra_dir = project_dir / ".tetra"
232
+ tetra_dir.mkdir(exist_ok=True)
233
+
234
+ build_base = tetra_dir / ".build"
235
+ build_dir = build_base / app_name
236
+
237
+ # Remove existing build directory
238
+ if build_dir.exists():
239
+ shutil.rmtree(build_dir)
240
+
241
+ build_dir.mkdir(parents=True, exist_ok=True)
242
+
243
+ return build_dir
244
+
245
+
246
+ def copy_project_files(files: list[Path], source_dir: Path, dest_dir: Path) -> None:
247
+ """
248
+ Copy project files to build directory.
249
+
250
+ Args:
251
+ files: List of files to copy
252
+ source_dir: Source directory
253
+ dest_dir: Destination directory
254
+ """
255
+ for file_path in files:
256
+ # Get relative path
257
+ rel_path = file_path.relative_to(source_dir)
258
+
259
+ # Create destination path
260
+ dest_path = dest_dir / rel_path
261
+
262
+ # Create parent directories
263
+ dest_path.parent.mkdir(parents=True, exist_ok=True)
264
+
265
+ # Copy file
266
+ shutil.copy2(file_path, dest_path)
267
+
268
+
269
+ def collect_requirements(project_dir: Path, build_dir: Path) -> list[str]:
270
+ """
271
+ Collect all requirements from requirements.txt and @remote decorators.
272
+
273
+ Args:
274
+ project_dir: Flash project directory
275
+ build_dir: Build directory (to check for workers)
276
+
277
+ Returns:
278
+ List of requirement strings
279
+ """
280
+ requirements = []
281
+
282
+ # Load requirements.txt
283
+ req_file = project_dir / "requirements.txt"
284
+ if req_file.exists():
285
+ try:
286
+ content = req_file.read_text(encoding="utf-8")
287
+ for line in content.splitlines():
288
+ line = line.strip()
289
+ # Skip empty lines and comments
290
+ if line and not line.startswith("#"):
291
+ requirements.append(line)
292
+ except Exception as e:
293
+ console.print(
294
+ f"[yellow]Warning:[/yellow] Failed to read requirements.txt: {e}"
295
+ )
296
+
297
+ # Extract dependencies from @remote decorators
298
+ workers_dir = build_dir / "workers"
299
+ if workers_dir.exists():
300
+ remote_deps = extract_remote_dependencies(workers_dir)
301
+ requirements.extend(remote_deps)
302
+
303
+ # Remove duplicates while preserving order
304
+ seen = set()
305
+ unique_requirements = []
306
+ for req in requirements:
307
+ if req not in seen:
308
+ seen.add(req)
309
+ unique_requirements.append(req)
310
+
311
+ return unique_requirements
312
+
313
+
314
+ def extract_remote_dependencies(workers_dir: Path) -> list[str]:
315
+ """
316
+ Extract dependencies from @remote decorators in worker files.
317
+
318
+ Args:
319
+ workers_dir: Path to workers directory
320
+
321
+ Returns:
322
+ List of dependency strings
323
+ """
324
+ dependencies = []
325
+
326
+ for py_file in workers_dir.glob("**/*.py"):
327
+ if py_file.name == "__init__.py":
328
+ continue
329
+
330
+ try:
331
+ tree = ast.parse(py_file.read_text(encoding="utf-8"))
332
+
333
+ for node in ast.walk(tree):
334
+ if isinstance(node, (ast.ClassDef, ast.FunctionDef)):
335
+ for decorator in node.decorator_list:
336
+ if isinstance(decorator, ast.Call):
337
+ func_name = None
338
+ if isinstance(decorator.func, ast.Name):
339
+ func_name = decorator.func.id
340
+ elif isinstance(decorator.func, ast.Attribute):
341
+ func_name = decorator.func.attr
342
+
343
+ if func_name == "remote":
344
+ # Extract dependencies keyword argument
345
+ for keyword in decorator.keywords:
346
+ if keyword.arg == "dependencies":
347
+ if isinstance(keyword.value, ast.List):
348
+ for elt in keyword.value.elts:
349
+ if isinstance(elt, ast.Constant):
350
+ dependencies.append(elt.value)
351
+
352
+ except Exception as e:
353
+ console.print(
354
+ f"[yellow]Warning:[/yellow] Failed to parse {py_file.name}: {e}"
355
+ )
356
+
357
+ return dependencies
358
+
359
+
360
+ def install_dependencies(
361
+ build_dir: Path, requirements: list[str], no_deps: bool
362
+ ) -> bool:
363
+ """
364
+ Install dependencies to build directory using pip or uv pip.
365
+
366
+ Args:
367
+ build_dir: Build directory (pip --target)
368
+ requirements: List of requirements to install
369
+ no_deps: If True, skip transitive dependencies
370
+
371
+ Returns:
372
+ True if successful
373
+ """
374
+ if not requirements:
375
+ return True
376
+
377
+ # Try python -m pip first
378
+ pip_cmd = [sys.executable, "-m", "pip"]
379
+ pip_available = False
380
+
381
+ try:
382
+ result = subprocess.run(
383
+ pip_cmd + ["--version"],
384
+ capture_output=True,
385
+ text=True,
386
+ timeout=5,
387
+ )
388
+ if result.returncode == 0:
389
+ pip_available = True
390
+ except (subprocess.SubprocessError, FileNotFoundError):
391
+ pass
392
+
393
+ # If pip not available, try uv pip
394
+ if not pip_available:
395
+ try:
396
+ result = subprocess.run(
397
+ ["uv", "pip", "--version"],
398
+ capture_output=True,
399
+ text=True,
400
+ timeout=5,
401
+ )
402
+ if result.returncode == 0:
403
+ pip_cmd = ["uv", "pip"]
404
+ pip_available = True
405
+ console.print(
406
+ "[yellow]Note:[/yellow] Using 'uv pip' (pip not found in venv)"
407
+ )
408
+ except (subprocess.SubprocessError, FileNotFoundError):
409
+ pass
410
+
411
+ # If neither available, error out
412
+ if not pip_available:
413
+ console.print("[red]Error:[/red] Neither pip nor uv pip found")
414
+ console.print("\n[yellow]Install pip with one of:[/yellow]")
415
+ console.print(" • python -m ensurepip --upgrade")
416
+ console.print(" • uv pip install pip")
417
+ return False
418
+
419
+ cmd = pip_cmd + [
420
+ "install",
421
+ "--target",
422
+ str(build_dir),
423
+ "--upgrade",
424
+ ]
425
+
426
+ if no_deps:
427
+ cmd.append("--no-deps")
428
+
429
+ cmd.extend(requirements)
430
+
431
+ try:
432
+ result = subprocess.run(
433
+ cmd,
434
+ capture_output=True,
435
+ text=True,
436
+ timeout=PIP_INSTALL_TIMEOUT_SECONDS,
437
+ )
438
+
439
+ if result.returncode != 0:
440
+ console.print(f"[red]pip install failed:[/red]\n{result.stderr}")
441
+ return False
442
+
443
+ return True
444
+
445
+ except subprocess.TimeoutExpired:
446
+ console.print(
447
+ f"[red]pip install timed out ({PIP_INSTALL_TIMEOUT_SECONDS} seconds)[/red]"
448
+ )
449
+ return False
450
+ except Exception as e:
451
+ console.print(f"[red]pip install error:[/red] {e}")
452
+ return False
453
+
454
+
455
+ def create_tarball(build_dir: Path, output_path: Path, app_name: str) -> None:
456
+ """
457
+ Create gzipped tarball of build directory.
458
+
459
+ Args:
460
+ build_dir: Build directory to archive
461
+ output_path: Output archive path
462
+ app_name: Application name (used as archive root)
463
+ """
464
+ # Remove existing archive
465
+ if output_path.exists():
466
+ output_path.unlink()
467
+
468
+ # Create tarball with app_name as root directory
469
+ with tarfile.open(output_path, "w:gz") as tar:
470
+ tar.add(build_dir, arcname=app_name)
471
+
472
+
473
+ def cleanup_build_directory(build_base: Path) -> None:
474
+ """
475
+ Remove build directory.
476
+
477
+ Args:
478
+ build_base: .build directory to remove
479
+ """
480
+ if build_base.exists():
481
+ shutil.rmtree(build_base)
482
+
483
+
484
+ def _display_build_config(
485
+ project_dir: Path,
486
+ app_name: str,
487
+ no_deps: bool,
488
+ keep_build: bool,
489
+ output_name: str | None,
490
+ ):
491
+ """Display build configuration."""
492
+ archive_name = output_name or "archive.tar.gz"
493
+
494
+ console.print(
495
+ Panel(
496
+ f"[bold]Project:[/bold] {app_name}\n"
497
+ f"[bold]Directory:[/bold] {project_dir}\n"
498
+ f"[bold]Archive:[/bold] .tetra/{archive_name}\n"
499
+ f"[bold]Skip transitive deps:[/bold] {no_deps}\n"
500
+ f"[bold]Keep build dir:[/bold] {keep_build}",
501
+ title="Flash Build Configuration",
502
+ expand=False,
503
+ )
504
+ )
505
+
506
+
507
+ def _display_build_summary(
508
+ archive_path: Path, app_name: str, file_count: int, dep_count: int
509
+ ):
510
+ """Display build summary."""
511
+ size_mb = archive_path.stat().st_size / (1024 * 1024)
512
+
513
+ summary = Table(show_header=False, box=None)
514
+ summary.add_column("Item", style="bold")
515
+ summary.add_column("Value", style="cyan")
516
+
517
+ summary.add_row("Application", app_name)
518
+ summary.add_row("Files packaged", str(file_count))
519
+ summary.add_row("Dependencies", str(dep_count))
520
+ summary.add_row("Archive", str(archive_path.relative_to(Path.cwd())))
521
+ summary.add_row("Size", f"{size_mb:.1f} MB")
522
+
523
+ console.print("\n")
524
+ console.print(summary)
525
+
526
+ console.print(
527
+ Panel(
528
+ f"[bold]{app_name}[/bold] built successfully!\n\n"
529
+ f"Archive ready for deployment.",
530
+ title="✓ Build Complete",
531
+ expand=False,
532
+ border_style="green",
533
+ )
534
+ )