tetra-rp 0.13.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.
@@ -1,122 +1,100 @@
1
- """Execute main entry point command."""
1
+ """Run Flash development server."""
2
2
 
3
- import asyncio
3
+ import subprocess
4
4
  import sys
5
5
  from pathlib import Path
6
6
  from typing import Optional
7
+
7
8
  import typer
8
9
  from rich.console import Console
9
- from rich.progress import Progress, SpinnerColumn, TextColumn
10
- from rich.panel import Panel
11
-
12
- from tetra_rp.config import get_paths
13
10
 
14
11
  console = Console()
15
12
 
16
13
 
17
14
  def run_command(
18
- entry_point: Optional[str] = typer.Option(
19
- None, "--entry", "-e", help="Entry point file to execute"
20
- ),
21
- no_deploy: bool = typer.Option(
22
- False, "--no-deploy", help="Skip resource deployment"
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"
23
19
  ),
24
20
  ):
25
- """Execute the main entry point of the app."""
21
+ """Run Flash development server with uvicorn."""
26
22
 
27
- # Discover entry point if not provided
23
+ # Discover entry point
24
+ entry_point = discover_entry_point()
28
25
  if not entry_point:
29
- entry_point = discover_entry_point()
30
- if not entry_point:
31
- console.print("No entry point found")
32
- console.print("Specify entry point with --entry or create main.py")
33
- raise typer.Exit(1)
34
-
35
- # Validate entry point exists
36
- entry_path = Path(entry_point)
37
- if not entry_path.exists():
38
- console.print(f"Entry point not found: {entry_point}")
26
+ console.print("[red]Error:[/red] No entry point found")
27
+ console.print("Create main.py with a FastAPI app")
39
28
  raise typer.Exit(1)
40
29
 
41
- console.print(f"🚀 Executing entry point: [bold]{entry_point}[/bold]")
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)
42
36
 
43
- # Run the entry point
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
44
59
  try:
45
- asyncio.run(execute_entry_point(entry_path, no_deploy))
60
+ subprocess.run(cmd)
46
61
  except KeyboardInterrupt:
47
- console.print("\nExecution interrupted by user")
48
- raise typer.Exit(1)
62
+ console.print("\n[yellow]Server stopped[/yellow]")
63
+ raise typer.Exit(0)
49
64
  except Exception as e:
50
- console.print(f"Execution failed: {e}")
65
+ console.print(f"[red]Error:[/red] {e}")
51
66
  raise typer.Exit(1)
52
67
 
53
68
 
54
69
  def discover_entry_point() -> Optional[str]:
55
70
  """Discover the main entry point file."""
56
- # Check common entry point names
57
- candidates = ["main.py", "app.py", "run.py", "__main__.py"]
71
+ candidates = ["main.py", "app.py", "server.py"]
58
72
 
59
73
  for candidate in candidates:
60
74
  if Path(candidate).exists():
61
75
  return candidate
62
76
 
63
- # Check for .tetra/config.json entry point
64
- paths = get_paths()
65
- config_path = paths.config_file
66
- if config_path.exists():
67
- try:
68
- import json
77
+ return None
69
78
 
70
- with open(config_path) as f:
71
- config = json.load(f)
72
- return config.get("entry_point")
73
- except (json.JSONDecodeError, KeyError):
74
- pass
75
79
 
76
- return None
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"
77
96
 
97
+ return None
78
98
 
79
- async def execute_entry_point(entry_path: Path, no_deploy: bool = False):
80
- """Execute the entry point with progress tracking."""
81
-
82
- with Progress(
83
- SpinnerColumn(),
84
- TextColumn("[progress.description]{task.description}"),
85
- console=console,
86
- ) as progress:
87
- if not no_deploy:
88
- # Deployment phase
89
- deploy_task = progress.add_task("Preparing resources...", total=None)
90
- await asyncio.sleep(1) # Mock deployment time
91
- progress.update(deploy_task, description="Resources ready")
92
- progress.stop_task(deploy_task)
93
-
94
- # Execution phase
95
- exec_task = progress.add_task("Executing...", total=None)
96
-
97
- # Execute the Python file
98
- try:
99
- # Import and run the module
100
- spec = __import__("importlib.util").util.spec_from_file_location(
101
- entry_path.stem, entry_path
102
- )
103
- module = __import__("importlib.util").util.module_from_spec(spec)
104
-
105
- # Add the directory to sys.path so imports work
106
- sys.path.insert(0, str(entry_path.parent))
107
-
108
- spec.loader.exec_module(module)
109
-
110
- progress.update(exec_task, description="Complete!")
111
- await asyncio.sleep(0.5) # Brief pause to show completion
112
-
113
- except Exception as e:
114
- progress.update(exec_task, description=f"Failed: {e}")
115
- raise
116
- finally:
117
- progress.stop_task(exec_task)
118
-
119
- # Success message
120
- console.print(
121
- Panel("Execution completed successfully", title="Success", expand=False)
122
- )
99
+ except Exception:
100
+ return None
tetra_rp/cli/main.py CHANGED
@@ -8,6 +8,7 @@ from rich.panel import Panel
8
8
  from .commands import (
9
9
  init,
10
10
  run,
11
+ build,
11
12
  resource,
12
13
  deploy,
13
14
  )
@@ -26,7 +27,7 @@ console = Console()
26
27
  # command: flash
27
28
  app = typer.Typer(
28
29
  name="flash",
29
- help="Flash CLI - Distributed inference and serving framework",
30
+ help="Runpod Flash CLI - Distributed inference and serving framework",
30
31
  no_args_is_help=True,
31
32
  rich_markup_mode="rich",
32
33
  )
@@ -34,6 +35,7 @@ app = typer.Typer(
34
35
  # command: flash <command>
35
36
  app.command("init")(init.init_command)
36
37
  app.command("run")(run.run_command)
38
+ app.command("build")(build.build_command)
37
39
  app.command("report")(resource.report_command)
38
40
  app.command("clean")(resource.clean_command)
39
41
 
@@ -60,15 +62,15 @@ def main(
60
62
  ctx: typer.Context,
61
63
  version: bool = typer.Option(False, "--version", "-v", help="Show version"),
62
64
  ):
63
- """Flash CLI - Distributed inference and serving framework."""
65
+ """Runpod Flash CLI - Distributed inference and serving framework."""
64
66
  if version:
65
- console.print(f"Flash CLI v{get_version()}")
67
+ console.print(f"Runpod Flash CLI v{get_version()}")
66
68
  raise typer.Exit()
67
69
 
68
70
  if ctx.invoked_subcommand is None:
69
71
  console.print(
70
72
  Panel(
71
- "[bold blue]Flash CLI[/bold blue]\n\n"
73
+ "[bold blue]Runpod Flash CLI[/bold blue]\n\n"
72
74
  "A framework for distributed inference and serving of ML models.\n\n"
73
75
  "Use [bold]flash --help[/bold] to see available commands.",
74
76
  title="Welcome",
@@ -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,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