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.
- tetra_rp/__init__.py +43 -0
- tetra_rp/cli/__init__.py +0 -0
- tetra_rp/cli/commands/__init__.py +1 -0
- tetra_rp/cli/commands/build.py +534 -0
- tetra_rp/cli/commands/deploy.py +370 -0
- tetra_rp/cli/commands/init.py +119 -0
- tetra_rp/cli/commands/resource.py +191 -0
- tetra_rp/cli/commands/run.py +100 -0
- tetra_rp/cli/main.py +85 -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 +184 -0
- tetra_rp/cli/utils/skeleton_template/.env.example +3 -0
- tetra_rp/cli/utils/skeleton_template/.flashignore +40 -0
- tetra_rp/cli/utils/skeleton_template/.gitignore +44 -0
- tetra_rp/cli/utils/skeleton_template/README.md +256 -0
- tetra_rp/cli/utils/skeleton_template/main.py +43 -0
- tetra_rp/cli/utils/skeleton_template/requirements.txt +1 -0
- tetra_rp/cli/utils/skeleton_template/workers/__init__.py +0 -0
- tetra_rp/cli/utils/skeleton_template/workers/cpu/__init__.py +20 -0
- tetra_rp/cli/utils/skeleton_template/workers/cpu/endpoint.py +38 -0
- tetra_rp/cli/utils/skeleton_template/workers/gpu/__init__.py +20 -0
- tetra_rp/cli/utils/skeleton_template/workers/gpu/endpoint.py +62 -0
- tetra_rp/client.py +128 -0
- tetra_rp/config.py +29 -0
- tetra_rp/core/__init__.py +0 -0
- tetra_rp/core/api/__init__.py +6 -0
- tetra_rp/core/api/runpod.py +319 -0
- tetra_rp/core/exceptions.py +50 -0
- tetra_rp/core/resources/__init__.py +37 -0
- tetra_rp/core/resources/base.py +47 -0
- tetra_rp/core/resources/cloud.py +4 -0
- tetra_rp/core/resources/constants.py +4 -0
- tetra_rp/core/resources/cpu.py +146 -0
- tetra_rp/core/resources/environment.py +41 -0
- tetra_rp/core/resources/gpu.py +68 -0
- tetra_rp/core/resources/live_serverless.py +62 -0
- tetra_rp/core/resources/network_volume.py +148 -0
- tetra_rp/core/resources/resource_manager.py +145 -0
- tetra_rp/core/resources/serverless.py +463 -0
- tetra_rp/core/resources/serverless_cpu.py +162 -0
- tetra_rp/core/resources/template.py +94 -0
- tetra_rp/core/resources/utils.py +50 -0
- tetra_rp/core/utils/__init__.py +0 -0
- tetra_rp/core/utils/backoff.py +43 -0
- tetra_rp/core/utils/constants.py +10 -0
- tetra_rp/core/utils/file_lock.py +260 -0
- tetra_rp/core/utils/json.py +33 -0
- tetra_rp/core/utils/lru_cache.py +75 -0
- tetra_rp/core/utils/singleton.py +21 -0
- tetra_rp/core/validation.py +44 -0
- tetra_rp/execute_class.py +319 -0
- tetra_rp/logger.py +34 -0
- tetra_rp/protos/__init__.py +0 -0
- tetra_rp/protos/remote_execution.py +148 -0
- tetra_rp/stubs/__init__.py +5 -0
- tetra_rp/stubs/live_serverless.py +155 -0
- tetra_rp/stubs/registry.py +117 -0
- tetra_rp/stubs/serverless.py +30 -0
- tetra_rp-0.17.1.dist-info/METADATA +976 -0
- tetra_rp-0.17.1.dist-info/RECORD +66 -0
- tetra_rp-0.17.1.dist-info/WHEEL +5 -0
- tetra_rp-0.17.1.dist-info/entry_points.txt +2 -0
- tetra_rp-0.17.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Run Flash development server."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
|
|
11
|
+
console = Console()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def run_command(
|
|
15
|
+
host: str = typer.Option("localhost", "--host", help="Host to bind to"),
|
|
16
|
+
port: int = typer.Option(8888, "--port", "-p", help="Port to bind to"),
|
|
17
|
+
reload: bool = typer.Option(
|
|
18
|
+
True, "--reload/--no-reload", help="Enable auto-reload"
|
|
19
|
+
),
|
|
20
|
+
):
|
|
21
|
+
"""Run Flash development server with uvicorn."""
|
|
22
|
+
|
|
23
|
+
# Discover entry point
|
|
24
|
+
entry_point = discover_entry_point()
|
|
25
|
+
if not entry_point:
|
|
26
|
+
console.print("[red]Error:[/red] No entry point found")
|
|
27
|
+
console.print("Create main.py with a FastAPI app")
|
|
28
|
+
raise typer.Exit(1)
|
|
29
|
+
|
|
30
|
+
# Check if entry point has FastAPI app
|
|
31
|
+
app_location = check_fastapi_app(entry_point)
|
|
32
|
+
if not app_location:
|
|
33
|
+
console.print(f"[red]Error:[/red] No FastAPI app found in {entry_point}")
|
|
34
|
+
console.print("Make sure your main.py contains: app = FastAPI()")
|
|
35
|
+
raise typer.Exit(1)
|
|
36
|
+
|
|
37
|
+
console.print("[green]Starting Flash Server[/green]")
|
|
38
|
+
console.print(f"Entry point: [bold]{app_location}[/bold]")
|
|
39
|
+
console.print(f"Server: [bold]http://{host}:{port}[/bold]")
|
|
40
|
+
console.print(f"Auto-reload: [bold]{'enabled' if reload else 'disabled'}[/bold]")
|
|
41
|
+
console.print("\nPress CTRL+C to stop\n")
|
|
42
|
+
|
|
43
|
+
# Build uvicorn command
|
|
44
|
+
cmd = [
|
|
45
|
+
sys.executable,
|
|
46
|
+
"-m",
|
|
47
|
+
"uvicorn",
|
|
48
|
+
app_location,
|
|
49
|
+
"--host",
|
|
50
|
+
host,
|
|
51
|
+
"--port",
|
|
52
|
+
str(port),
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
if reload:
|
|
56
|
+
cmd.append("--reload")
|
|
57
|
+
|
|
58
|
+
# Run uvicorn
|
|
59
|
+
try:
|
|
60
|
+
subprocess.run(cmd)
|
|
61
|
+
except KeyboardInterrupt:
|
|
62
|
+
console.print("\n[yellow]Server stopped[/yellow]")
|
|
63
|
+
raise typer.Exit(0)
|
|
64
|
+
except Exception as e:
|
|
65
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
66
|
+
raise typer.Exit(1)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def discover_entry_point() -> Optional[str]:
|
|
70
|
+
"""Discover the main entry point file."""
|
|
71
|
+
candidates = ["main.py", "app.py", "server.py"]
|
|
72
|
+
|
|
73
|
+
for candidate in candidates:
|
|
74
|
+
if Path(candidate).exists():
|
|
75
|
+
return candidate
|
|
76
|
+
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def check_fastapi_app(entry_point: str) -> Optional[str]:
|
|
81
|
+
"""
|
|
82
|
+
Check if entry point has a FastAPI app and return the app location.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
App location in format "module:app" or None
|
|
86
|
+
"""
|
|
87
|
+
try:
|
|
88
|
+
# Read the file
|
|
89
|
+
content = Path(entry_point).read_text()
|
|
90
|
+
|
|
91
|
+
# Check for FastAPI app
|
|
92
|
+
if "app = FastAPI(" in content or "app=FastAPI(" in content:
|
|
93
|
+
# Extract module name from file path
|
|
94
|
+
module = entry_point.replace(".py", "").replace("/", ".")
|
|
95
|
+
return f"{module}:app"
|
|
96
|
+
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
except Exception:
|
|
100
|
+
return None
|
tetra_rp/cli/main.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Main CLI entry point for Flash CLI."""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
from importlib import metadata
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.panel import Panel
|
|
7
|
+
|
|
8
|
+
from .commands import (
|
|
9
|
+
init,
|
|
10
|
+
run,
|
|
11
|
+
build,
|
|
12
|
+
# resource,
|
|
13
|
+
deploy,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_version() -> str:
|
|
18
|
+
"""Get the package version from metadata."""
|
|
19
|
+
try:
|
|
20
|
+
return metadata.version("tetra_rp")
|
|
21
|
+
except metadata.PackageNotFoundError:
|
|
22
|
+
return "unknown"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
console = Console()
|
|
26
|
+
|
|
27
|
+
# command: flash
|
|
28
|
+
app = typer.Typer(
|
|
29
|
+
name="flash",
|
|
30
|
+
help="Runpod Flash CLI - Distributed inference and serving framework",
|
|
31
|
+
no_args_is_help=True,
|
|
32
|
+
rich_markup_mode="rich",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# command: flash <command>
|
|
36
|
+
app.command("init")(init.init_command)
|
|
37
|
+
app.command("run")(run.run_command)
|
|
38
|
+
app.command("build")(build.build_command)
|
|
39
|
+
# app.command("report")(resource.report_command)
|
|
40
|
+
# app.command("clean")(resource.clean_command)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# command: flash deploy
|
|
44
|
+
deploy_app = typer.Typer(
|
|
45
|
+
name="deploy",
|
|
46
|
+
help="Deployment environment management commands",
|
|
47
|
+
no_args_is_help=True,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
deploy_app.command("list")(deploy.list_command)
|
|
51
|
+
deploy_app.command("new")(deploy.new_command)
|
|
52
|
+
# deploy_app.command("send")(deploy.send_command)
|
|
53
|
+
# deploy_app.command("report")(deploy.report_command)
|
|
54
|
+
# deploy_app.command("rollback")(deploy.rollback_command)
|
|
55
|
+
# deploy_app.command("remove")(deploy.remove_command)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# command: flash deploy *
|
|
59
|
+
app.add_typer(deploy_app, name="deploy")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@app.callback(invoke_without_command=True)
|
|
63
|
+
def main(
|
|
64
|
+
ctx: typer.Context,
|
|
65
|
+
version: bool = typer.Option(False, "--version", "-v", help="Show version"),
|
|
66
|
+
):
|
|
67
|
+
"""Runpod Flash CLI - Distributed inference and serving framework."""
|
|
68
|
+
if version:
|
|
69
|
+
console.print(f"Runpod Flash CLI v{get_version()}")
|
|
70
|
+
raise typer.Exit()
|
|
71
|
+
|
|
72
|
+
if ctx.invoked_subcommand is None:
|
|
73
|
+
console.print(
|
|
74
|
+
Panel(
|
|
75
|
+
"[bold blue]Runpod Flash CLI[/bold blue]\n\n"
|
|
76
|
+
"A framework for distributed inference and serving of ML models.\n\n"
|
|
77
|
+
"Use [bold]flash --help[/bold] to see available commands.",
|
|
78
|
+
title="Welcome",
|
|
79
|
+
expand=False,
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
if __name__ == "__main__":
|
|
85
|
+
app()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI utility modules."""
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Conda environment management utilities."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
from typing import List, Tuple
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
|
|
7
|
+
console = Console()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def check_conda_available() -> bool:
|
|
11
|
+
"""Check if conda is available on the system."""
|
|
12
|
+
try:
|
|
13
|
+
result = subprocess.run(
|
|
14
|
+
["conda", "--version"],
|
|
15
|
+
capture_output=True,
|
|
16
|
+
text=True,
|
|
17
|
+
timeout=5,
|
|
18
|
+
)
|
|
19
|
+
return result.returncode == 0
|
|
20
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
|
21
|
+
return False
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def create_conda_environment(
|
|
25
|
+
env_name: str, python_version: str = "3.11"
|
|
26
|
+
) -> Tuple[bool, str]:
|
|
27
|
+
"""
|
|
28
|
+
Create a new conda environment.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
env_name: Name of the conda environment
|
|
32
|
+
python_version: Python version to use
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Tuple of (success, message)
|
|
36
|
+
"""
|
|
37
|
+
try:
|
|
38
|
+
console.print(f"Creating conda environment: {env_name}")
|
|
39
|
+
|
|
40
|
+
result = subprocess.run(
|
|
41
|
+
["conda", "create", "-n", env_name, f"python={python_version}", "-y"],
|
|
42
|
+
capture_output=True,
|
|
43
|
+
text=True,
|
|
44
|
+
timeout=300, # 5 minutes timeout
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
if result.returncode == 0:
|
|
48
|
+
return True, f"Conda environment '{env_name}' created successfully"
|
|
49
|
+
else:
|
|
50
|
+
return False, f"Failed to create environment: {result.stderr}"
|
|
51
|
+
|
|
52
|
+
except subprocess.TimeoutExpired:
|
|
53
|
+
return False, "Environment creation timed out"
|
|
54
|
+
except Exception as e:
|
|
55
|
+
return False, f"Error creating environment: {e}"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def install_packages_in_env(
|
|
59
|
+
env_name: str, packages: List[str], use_pip: bool = True
|
|
60
|
+
) -> Tuple[bool, str]:
|
|
61
|
+
"""
|
|
62
|
+
Install packages in a conda environment.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
env_name: Name of the conda environment
|
|
66
|
+
packages: List of packages to install
|
|
67
|
+
use_pip: If True, use pip install; otherwise use conda install
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Tuple of (success, message)
|
|
71
|
+
"""
|
|
72
|
+
try:
|
|
73
|
+
console.print(f"Installing packages: {', '.join(packages)}")
|
|
74
|
+
|
|
75
|
+
if use_pip:
|
|
76
|
+
# Use conda run to execute pip in the environment
|
|
77
|
+
cmd = [
|
|
78
|
+
"conda",
|
|
79
|
+
"run",
|
|
80
|
+
"-n",
|
|
81
|
+
env_name,
|
|
82
|
+
"pip",
|
|
83
|
+
"install",
|
|
84
|
+
] + packages
|
|
85
|
+
else:
|
|
86
|
+
cmd = ["conda", "install", "-n", env_name, "-y"] + packages
|
|
87
|
+
|
|
88
|
+
result = subprocess.run(
|
|
89
|
+
cmd,
|
|
90
|
+
capture_output=True,
|
|
91
|
+
text=True,
|
|
92
|
+
timeout=600, # 10 minutes timeout
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
if result.returncode == 0:
|
|
96
|
+
return True, "Packages installed successfully"
|
|
97
|
+
else:
|
|
98
|
+
return False, f"Failed to install packages: {result.stderr}"
|
|
99
|
+
|
|
100
|
+
except subprocess.TimeoutExpired:
|
|
101
|
+
return False, "Package installation timed out"
|
|
102
|
+
except Exception as e:
|
|
103
|
+
return False, f"Error installing packages: {e}"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def environment_exists(env_name: str) -> bool:
|
|
107
|
+
"""Check if a conda environment exists."""
|
|
108
|
+
try:
|
|
109
|
+
result = subprocess.run(
|
|
110
|
+
["conda", "env", "list"],
|
|
111
|
+
capture_output=True,
|
|
112
|
+
text=True,
|
|
113
|
+
timeout=10,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
if result.returncode == 0:
|
|
117
|
+
# Check if environment name appears in the output
|
|
118
|
+
return env_name in result.stdout
|
|
119
|
+
return False
|
|
120
|
+
|
|
121
|
+
except Exception:
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def get_activation_command(env_name: str) -> str:
|
|
126
|
+
"""Get the command to activate the conda environment."""
|
|
127
|
+
return f"conda activate {env_name}"
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Deployment environment management utilities."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Dict, Any
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
from tetra_rp.config import get_paths
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_deployment_environments() -> Dict[str, Dict[str, Any]]:
|
|
11
|
+
"""Get all deployment environments."""
|
|
12
|
+
paths = get_paths()
|
|
13
|
+
deployments_file = paths.deployments_file
|
|
14
|
+
|
|
15
|
+
if not deployments_file.exists():
|
|
16
|
+
return {}
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
with open(deployments_file) as f:
|
|
20
|
+
return json.load(f)
|
|
21
|
+
except (json.JSONDecodeError, FileNotFoundError):
|
|
22
|
+
return {}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def save_deployment_environments(environments: Dict[str, Dict[str, Any]]):
|
|
26
|
+
"""Save deployment environments to file."""
|
|
27
|
+
paths = get_paths()
|
|
28
|
+
deployments_file = paths.deployments_file
|
|
29
|
+
|
|
30
|
+
# Ensure .tetra directory exists
|
|
31
|
+
paths.ensure_tetra_dir()
|
|
32
|
+
|
|
33
|
+
with open(deployments_file, "w") as f:
|
|
34
|
+
json.dump(environments, f, indent=2)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def create_deployment_environment(name: str, config: Dict[str, Any]):
|
|
38
|
+
"""Create a new deployment environment."""
|
|
39
|
+
environments = get_deployment_environments()
|
|
40
|
+
|
|
41
|
+
# Mock environment creation
|
|
42
|
+
environments[name] = {
|
|
43
|
+
"status": "idle",
|
|
44
|
+
"config": config,
|
|
45
|
+
"created_at": datetime.now().isoformat(),
|
|
46
|
+
"current_version": None,
|
|
47
|
+
"last_deployed": None,
|
|
48
|
+
"url": None,
|
|
49
|
+
"version_history": [],
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
save_deployment_environments(environments)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def remove_deployment_environment(name: str):
|
|
56
|
+
"""Remove a deployment environment."""
|
|
57
|
+
environments = get_deployment_environments()
|
|
58
|
+
|
|
59
|
+
if name in environments:
|
|
60
|
+
del environments[name]
|
|
61
|
+
save_deployment_environments(environments)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def deploy_to_environment(name: str) -> Dict[str, Any]:
|
|
65
|
+
"""Deploy current project to environment (mock implementation)."""
|
|
66
|
+
environments = get_deployment_environments()
|
|
67
|
+
|
|
68
|
+
if name not in environments:
|
|
69
|
+
raise ValueError(f"Environment {name} not found")
|
|
70
|
+
|
|
71
|
+
# Mock deployment
|
|
72
|
+
version = f"v1.{len(environments[name]['version_history'])}.0"
|
|
73
|
+
url = f"https://{name.lower()}.example.com"
|
|
74
|
+
|
|
75
|
+
# Update environment
|
|
76
|
+
environments[name].update(
|
|
77
|
+
{
|
|
78
|
+
"status": "active",
|
|
79
|
+
"current_version": version,
|
|
80
|
+
"last_deployed": datetime.now().isoformat(),
|
|
81
|
+
"url": url,
|
|
82
|
+
"uptime": "99.9%",
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Add to version history
|
|
87
|
+
version_entry = {
|
|
88
|
+
"version": version,
|
|
89
|
+
"deployed_at": datetime.now().isoformat(),
|
|
90
|
+
"description": "Deployment via CLI",
|
|
91
|
+
"is_current": True,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# Mark previous versions as not current
|
|
95
|
+
for v in environments[name]["version_history"]:
|
|
96
|
+
v["is_current"] = False
|
|
97
|
+
|
|
98
|
+
environments[name]["version_history"].insert(0, version_entry)
|
|
99
|
+
|
|
100
|
+
save_deployment_environments(environments)
|
|
101
|
+
|
|
102
|
+
return {"version": version, "url": url, "status": "active"}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def rollback_deployment(name: str, target_version: str):
|
|
106
|
+
"""Rollback deployment to a previous version (mock implementation)."""
|
|
107
|
+
environments = get_deployment_environments()
|
|
108
|
+
|
|
109
|
+
if name not in environments:
|
|
110
|
+
raise ValueError(f"Environment {name} not found")
|
|
111
|
+
|
|
112
|
+
# Find target version
|
|
113
|
+
target_version_info = None
|
|
114
|
+
for version in environments[name]["version_history"]:
|
|
115
|
+
if version["version"] == target_version:
|
|
116
|
+
target_version_info = version
|
|
117
|
+
break
|
|
118
|
+
|
|
119
|
+
if not target_version_info:
|
|
120
|
+
raise ValueError(f"Version {target_version} not found")
|
|
121
|
+
|
|
122
|
+
# Update current version
|
|
123
|
+
environments[name]["current_version"] = target_version
|
|
124
|
+
environments[name]["last_deployed"] = datetime.now().isoformat()
|
|
125
|
+
|
|
126
|
+
# Update version history
|
|
127
|
+
for version in environments[name]["version_history"]:
|
|
128
|
+
version["is_current"] = version["version"] == target_version
|
|
129
|
+
|
|
130
|
+
save_deployment_environments(environments)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def get_environment_info(name: str) -> Dict[str, Any]:
|
|
134
|
+
"""Get detailed information about an environment."""
|
|
135
|
+
environments = get_deployment_environments()
|
|
136
|
+
|
|
137
|
+
if name not in environments:
|
|
138
|
+
raise ValueError(f"Environment {name} not found")
|
|
139
|
+
|
|
140
|
+
env_info = environments[name].copy()
|
|
141
|
+
|
|
142
|
+
# Add mock metrics and additional info
|
|
143
|
+
if env_info["status"] == "active":
|
|
144
|
+
env_info.update(
|
|
145
|
+
{
|
|
146
|
+
"uptime": "99.9%",
|
|
147
|
+
"requests_24h": 145234,
|
|
148
|
+
"avg_response_time": "245ms",
|
|
149
|
+
"error_rate": "0.02%",
|
|
150
|
+
"cpu_usage": "45%",
|
|
151
|
+
"memory_usage": "62%",
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Ensure version history exists and is properly formatted
|
|
156
|
+
if "version_history" not in env_info:
|
|
157
|
+
env_info["version_history"] = []
|
|
158
|
+
|
|
159
|
+
# Add sample version history if empty
|
|
160
|
+
if not env_info["version_history"] and env_info["current_version"]:
|
|
161
|
+
env_info["version_history"] = [
|
|
162
|
+
{
|
|
163
|
+
"version": env_info["current_version"],
|
|
164
|
+
"deployed_at": env_info.get(
|
|
165
|
+
"last_deployed", datetime.now().isoformat()
|
|
166
|
+
),
|
|
167
|
+
"description": "Initial deployment",
|
|
168
|
+
"is_current": True,
|
|
169
|
+
}
|
|
170
|
+
]
|
|
171
|
+
|
|
172
|
+
return env_info
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Ignore pattern matching utilities for Flash build."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pathspec
|
|
7
|
+
|
|
8
|
+
log = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def parse_ignore_file(file_path: Path) -> list[str]:
|
|
12
|
+
"""
|
|
13
|
+
Parse an ignore file and return list of patterns.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
file_path: Path to ignore file (.flashignore or .gitignore)
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
List of pattern strings
|
|
20
|
+
"""
|
|
21
|
+
if not file_path.exists():
|
|
22
|
+
return []
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
content = file_path.read_text(encoding="utf-8")
|
|
26
|
+
patterns = []
|
|
27
|
+
|
|
28
|
+
for line in content.splitlines():
|
|
29
|
+
line = line.strip()
|
|
30
|
+
# Skip empty lines and comments
|
|
31
|
+
if line and not line.startswith("#"):
|
|
32
|
+
patterns.append(line)
|
|
33
|
+
|
|
34
|
+
return patterns
|
|
35
|
+
|
|
36
|
+
except Exception as e:
|
|
37
|
+
log.warning(f"Failed to read {file_path.name}: {e}")
|
|
38
|
+
return []
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def load_ignore_patterns(project_dir: Path) -> pathspec.PathSpec:
|
|
42
|
+
"""
|
|
43
|
+
Load ignore patterns from .flashignore and .gitignore files.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
project_dir: Flash project directory
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
PathSpec object for pattern matching
|
|
50
|
+
"""
|
|
51
|
+
patterns = []
|
|
52
|
+
|
|
53
|
+
# Load .flashignore
|
|
54
|
+
flashignore = project_dir / ".flashignore"
|
|
55
|
+
if flashignore.exists():
|
|
56
|
+
flash_patterns = parse_ignore_file(flashignore)
|
|
57
|
+
patterns.extend(flash_patterns)
|
|
58
|
+
log.debug(f"Loaded {len(flash_patterns)} patterns from .flashignore")
|
|
59
|
+
|
|
60
|
+
# Load .gitignore
|
|
61
|
+
gitignore = project_dir / ".gitignore"
|
|
62
|
+
if gitignore.exists():
|
|
63
|
+
git_patterns = parse_ignore_file(gitignore)
|
|
64
|
+
patterns.extend(git_patterns)
|
|
65
|
+
log.debug(f"Loaded {len(git_patterns)} patterns from .gitignore")
|
|
66
|
+
|
|
67
|
+
# Always exclude build artifacts
|
|
68
|
+
always_ignore = [
|
|
69
|
+
".build/",
|
|
70
|
+
".tetra/",
|
|
71
|
+
"*.tar.gz",
|
|
72
|
+
".git/",
|
|
73
|
+
]
|
|
74
|
+
patterns.extend(always_ignore)
|
|
75
|
+
|
|
76
|
+
# Create PathSpec with gitwildmatch pattern (gitignore-style)
|
|
77
|
+
return pathspec.PathSpec.from_lines("gitwildmatch", patterns)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def should_ignore(file_path: Path, spec: pathspec.PathSpec, base_dir: Path) -> bool:
|
|
81
|
+
"""
|
|
82
|
+
Check if a file should be ignored based on patterns.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
file_path: File path to check
|
|
86
|
+
spec: PathSpec object with ignore patterns
|
|
87
|
+
base_dir: Base directory for relative path calculation
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
True if file should be ignored
|
|
91
|
+
"""
|
|
92
|
+
try:
|
|
93
|
+
# Get relative path for pattern matching
|
|
94
|
+
rel_path = file_path.relative_to(base_dir)
|
|
95
|
+
|
|
96
|
+
# Check if file matches any ignore pattern
|
|
97
|
+
return spec.match_file(str(rel_path))
|
|
98
|
+
|
|
99
|
+
except ValueError:
|
|
100
|
+
# file_path is not relative to base_dir
|
|
101
|
+
return False
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def get_file_tree(
|
|
105
|
+
directory: Path, spec: pathspec.PathSpec, base_dir: Path | None = None
|
|
106
|
+
) -> list[Path]:
|
|
107
|
+
"""
|
|
108
|
+
Recursively collect all files in directory excluding ignored patterns.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
directory: Directory to scan
|
|
112
|
+
spec: PathSpec object with ignore patterns
|
|
113
|
+
base_dir: Base directory for relative paths (defaults to directory)
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
List of file paths that should be included
|
|
117
|
+
"""
|
|
118
|
+
if base_dir is None:
|
|
119
|
+
base_dir = directory
|
|
120
|
+
|
|
121
|
+
files = []
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
for item in directory.iterdir():
|
|
125
|
+
# Check if should ignore
|
|
126
|
+
if should_ignore(item, spec, base_dir):
|
|
127
|
+
log.debug(f"Ignoring: {item.relative_to(base_dir)}")
|
|
128
|
+
continue
|
|
129
|
+
|
|
130
|
+
if item.is_file():
|
|
131
|
+
files.append(item)
|
|
132
|
+
elif item.is_dir():
|
|
133
|
+
# Recursively collect files from subdirectory
|
|
134
|
+
files.extend(get_file_tree(item, spec, base_dir))
|
|
135
|
+
|
|
136
|
+
except PermissionError as e:
|
|
137
|
+
log.warning(f"Permission denied: {directory} - {e}")
|
|
138
|
+
|
|
139
|
+
return files
|