tetra-rp 0.12.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/__init__.py +0 -0
- tetra_rp/cli/commands/__init__.py +1 -0
- tetra_rp/cli/commands/build.py +501 -0
- tetra_rp/cli/commands/deploy.py +336 -0
- tetra_rp/cli/commands/init.py +135 -0
- tetra_rp/cli/commands/resource.py +191 -0
- tetra_rp/cli/commands/run.py +100 -0
- tetra_rp/cli/main.py +83 -0
- tetra_rp/cli/utils/__init__.py +1 -0
- tetra_rp/cli/utils/conda.py +127 -0
- tetra_rp/cli/utils/deployment.py +172 -0
- tetra_rp/cli/utils/ignore.py +139 -0
- tetra_rp/cli/utils/skeleton.py +347 -0
- tetra_rp/client.py +35 -14
- tetra_rp/config.py +29 -0
- tetra_rp/core/resources/cpu.py +9 -0
- tetra_rp/core/resources/serverless_cpu.py +10 -2
- tetra_rp/execute_class.py +0 -3
- tetra_rp/protos/remote_execution.py +0 -4
- tetra_rp/stubs/live_serverless.py +1 -3
- tetra_rp/stubs/registry.py +0 -4
- {tetra_rp-0.12.0.dist-info → tetra_rp-0.14.0.dist-info}/METADATA +6 -1
- {tetra_rp-0.12.0.dist-info → tetra_rp-0.14.0.dist-info}/RECORD +26 -11
- tetra_rp-0.14.0.dist-info/entry_points.txt +2 -0
- {tetra_rp-0.12.0.dist-info → tetra_rp-0.14.0.dist-info}/WHEEL +0 -0
- {tetra_rp-0.12.0.dist-info → tetra_rp-0.14.0.dist-info}/top_level.txt +0 -0
tetra_rp/cli/__init__.py
ADDED
|
File without changes
|
|
@@ -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
|
+
)
|