tetra-rp 0.12.0__tar.gz → 0.14.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.

Files changed (58) hide show
  1. {tetra_rp-0.12.0 → tetra_rp-0.14.0}/PKG-INFO +6 -1
  2. {tetra_rp-0.12.0 → tetra_rp-0.14.0}/README.md +2 -1
  3. {tetra_rp-0.12.0 → tetra_rp-0.14.0}/pyproject.toml +11 -1
  4. tetra_rp-0.14.0/src/tetra_rp/cli/commands/__init__.py +1 -0
  5. tetra_rp-0.14.0/src/tetra_rp/cli/commands/build.py +501 -0
  6. tetra_rp-0.14.0/src/tetra_rp/cli/commands/deploy.py +336 -0
  7. tetra_rp-0.14.0/src/tetra_rp/cli/commands/init.py +135 -0
  8. tetra_rp-0.14.0/src/tetra_rp/cli/commands/resource.py +191 -0
  9. tetra_rp-0.14.0/src/tetra_rp/cli/commands/run.py +100 -0
  10. tetra_rp-0.14.0/src/tetra_rp/cli/main.py +83 -0
  11. tetra_rp-0.14.0/src/tetra_rp/cli/utils/__init__.py +1 -0
  12. tetra_rp-0.14.0/src/tetra_rp/cli/utils/conda.py +127 -0
  13. tetra_rp-0.14.0/src/tetra_rp/cli/utils/deployment.py +172 -0
  14. tetra_rp-0.14.0/src/tetra_rp/cli/utils/ignore.py +139 -0
  15. tetra_rp-0.14.0/src/tetra_rp/cli/utils/skeleton.py +347 -0
  16. {tetra_rp-0.12.0 → tetra_rp-0.14.0}/src/tetra_rp/client.py +35 -14
  17. tetra_rp-0.14.0/src/tetra_rp/config.py +29 -0
  18. {tetra_rp-0.12.0 → tetra_rp-0.14.0}/src/tetra_rp/core/resources/cpu.py +9 -0
  19. {tetra_rp-0.12.0 → tetra_rp-0.14.0}/src/tetra_rp/core/resources/serverless_cpu.py +10 -2
  20. {tetra_rp-0.12.0 → tetra_rp-0.14.0}/src/tetra_rp/execute_class.py +0 -3
  21. tetra_rp-0.14.0/src/tetra_rp/protos/__init__.py +0 -0
  22. {tetra_rp-0.12.0 → tetra_rp-0.14.0}/src/tetra_rp/protos/remote_execution.py +0 -4
  23. {tetra_rp-0.12.0 → tetra_rp-0.14.0}/src/tetra_rp/stubs/live_serverless.py +1 -3
  24. {tetra_rp-0.12.0 → tetra_rp-0.14.0}/src/tetra_rp/stubs/registry.py +0 -4
  25. {tetra_rp-0.12.0 → tetra_rp-0.14.0}/src/tetra_rp.egg-info/PKG-INFO +6 -1
  26. {tetra_rp-0.12.0 → tetra_rp-0.14.0}/src/tetra_rp.egg-info/SOURCES.txt +15 -0
  27. tetra_rp-0.14.0/src/tetra_rp.egg-info/entry_points.txt +2 -0
  28. {tetra_rp-0.12.0 → tetra_rp-0.14.0}/src/tetra_rp.egg-info/requires.txt +4 -0
  29. {tetra_rp-0.12.0 → tetra_rp-0.14.0}/setup.cfg +0 -0
  30. {tetra_rp-0.12.0 → tetra_rp-0.14.0}/src/tetra_rp/__init__.py +0 -0
  31. {tetra_rp-0.12.0/src/tetra_rp/core → tetra_rp-0.14.0/src/tetra_rp/cli}/__init__.py +0 -0
  32. {tetra_rp-0.12.0/src/tetra_rp/core/utils → tetra_rp-0.14.0/src/tetra_rp/core}/__init__.py +0 -0
  33. {tetra_rp-0.12.0 → tetra_rp-0.14.0}/src/tetra_rp/core/api/__init__.py +0 -0
  34. {tetra_rp-0.12.0 → tetra_rp-0.14.0}/src/tetra_rp/core/api/runpod.py +0 -0
  35. {tetra_rp-0.12.0 → tetra_rp-0.14.0}/src/tetra_rp/core/resources/__init__.py +0 -0
  36. {tetra_rp-0.12.0 → tetra_rp-0.14.0}/src/tetra_rp/core/resources/base.py +0 -0
  37. {tetra_rp-0.12.0 → tetra_rp-0.14.0}/src/tetra_rp/core/resources/cloud.py +0 -0
  38. {tetra_rp-0.12.0 → tetra_rp-0.14.0}/src/tetra_rp/core/resources/constants.py +0 -0
  39. {tetra_rp-0.12.0 → tetra_rp-0.14.0}/src/tetra_rp/core/resources/environment.py +0 -0
  40. {tetra_rp-0.12.0 → tetra_rp-0.14.0}/src/tetra_rp/core/resources/gpu.py +0 -0
  41. {tetra_rp-0.12.0 → tetra_rp-0.14.0}/src/tetra_rp/core/resources/live_serverless.py +0 -0
  42. {tetra_rp-0.12.0 → tetra_rp-0.14.0}/src/tetra_rp/core/resources/network_volume.py +0 -0
  43. {tetra_rp-0.12.0 → tetra_rp-0.14.0}/src/tetra_rp/core/resources/resource_manager.py +0 -0
  44. {tetra_rp-0.12.0 → tetra_rp-0.14.0}/src/tetra_rp/core/resources/serverless.py +0 -0
  45. {tetra_rp-0.12.0 → tetra_rp-0.14.0}/src/tetra_rp/core/resources/template.py +0 -0
  46. {tetra_rp-0.12.0 → tetra_rp-0.14.0}/src/tetra_rp/core/resources/utils.py +0 -0
  47. {tetra_rp-0.12.0/src/tetra_rp/protos → tetra_rp-0.14.0/src/tetra_rp/core/utils}/__init__.py +0 -0
  48. {tetra_rp-0.12.0 → tetra_rp-0.14.0}/src/tetra_rp/core/utils/backoff.py +0 -0
  49. {tetra_rp-0.12.0 → tetra_rp-0.14.0}/src/tetra_rp/core/utils/constants.py +0 -0
  50. {tetra_rp-0.12.0 → tetra_rp-0.14.0}/src/tetra_rp/core/utils/file_lock.py +0 -0
  51. {tetra_rp-0.12.0 → tetra_rp-0.14.0}/src/tetra_rp/core/utils/json.py +0 -0
  52. {tetra_rp-0.12.0 → tetra_rp-0.14.0}/src/tetra_rp/core/utils/lru_cache.py +0 -0
  53. {tetra_rp-0.12.0 → tetra_rp-0.14.0}/src/tetra_rp/core/utils/singleton.py +0 -0
  54. {tetra_rp-0.12.0 → tetra_rp-0.14.0}/src/tetra_rp/logger.py +0 -0
  55. {tetra_rp-0.12.0 → tetra_rp-0.14.0}/src/tetra_rp/stubs/__init__.py +0 -0
  56. {tetra_rp-0.12.0 → tetra_rp-0.14.0}/src/tetra_rp/stubs/serverless.py +0 -0
  57. {tetra_rp-0.12.0 → tetra_rp-0.14.0}/src/tetra_rp.egg-info/dependency_links.txt +0 -0
  58. {tetra_rp-0.12.0 → tetra_rp-0.14.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.12.0
3
+ Version: 0.14.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,10 @@ 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
20
+ Requires-Dist: pathspec>=0.11.0
17
21
 
18
22
  # Tetra: Serverless computing for AI workloads
19
23
 
@@ -359,6 +363,7 @@ if __name__ == "__main__":
359
363
  ```python
360
364
  import asyncio
361
365
  from tetra_rp import remote, LiveServerless, GpuGroup, PodTemplate
366
+ import base64
362
367
 
363
368
  # Advanced GPU configuration with consolidated template overrides
364
369
  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.12.0"
3
+ version = "0.14.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,10 @@ 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",
28
+ "pathspec>=0.11.0",
25
29
  ]
26
30
 
27
31
  [dependency-groups]
@@ -37,10 +41,16 @@ test = [
37
41
  "twine>=6.1.0",
38
42
  ]
39
43
 
44
+ [project.scripts]
45
+ flash = "tetra_rp.cli.main:app"
46
+
40
47
  [build-system]
41
48
  requires = ["setuptools>=42", "wheel"]
42
49
  build-backend = "setuptools.build_meta"
43
50
 
51
+ [tool.setuptools.packages.find]
52
+ where = ["src"]
53
+
44
54
  [tool.pytest.ini_options]
45
55
  testpaths = ["tests"]
46
56
  python_files = ["test_*.py"]
@@ -0,0 +1 @@
1
+ """CLI command modules."""
@@ -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
+ )