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
@@ -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