tetra-rp 0.6.0__py3-none-any.whl → 0.24.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.
- tetra_rp/__init__.py +109 -19
- tetra_rp/cli/commands/__init__.py +1 -0
- tetra_rp/cli/commands/apps.py +143 -0
- tetra_rp/cli/commands/build.py +1082 -0
- tetra_rp/cli/commands/build_utils/__init__.py +1 -0
- tetra_rp/cli/commands/build_utils/handler_generator.py +176 -0
- tetra_rp/cli/commands/build_utils/lb_handler_generator.py +309 -0
- tetra_rp/cli/commands/build_utils/manifest.py +430 -0
- tetra_rp/cli/commands/build_utils/mothership_handler_generator.py +75 -0
- tetra_rp/cli/commands/build_utils/scanner.py +596 -0
- tetra_rp/cli/commands/deploy.py +580 -0
- tetra_rp/cli/commands/init.py +123 -0
- tetra_rp/cli/commands/resource.py +108 -0
- tetra_rp/cli/commands/run.py +296 -0
- tetra_rp/cli/commands/test_mothership.py +458 -0
- tetra_rp/cli/commands/undeploy.py +533 -0
- tetra_rp/cli/main.py +97 -0
- tetra_rp/cli/utils/__init__.py +1 -0
- tetra_rp/cli/utils/app.py +15 -0
- tetra_rp/cli/utils/conda.py +127 -0
- tetra_rp/cli/utils/deployment.py +530 -0
- tetra_rp/cli/utils/ignore.py +143 -0
- tetra_rp/cli/utils/skeleton.py +184 -0
- tetra_rp/cli/utils/skeleton_template/.env.example +4 -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 +263 -0
- tetra_rp/cli/utils/skeleton_template/main.py +44 -0
- tetra_rp/cli/utils/skeleton_template/mothership.py +55 -0
- tetra_rp/cli/utils/skeleton_template/pyproject.toml +58 -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 +19 -0
- tetra_rp/cli/utils/skeleton_template/workers/cpu/endpoint.py +36 -0
- tetra_rp/cli/utils/skeleton_template/workers/gpu/__init__.py +19 -0
- tetra_rp/cli/utils/skeleton_template/workers/gpu/endpoint.py +61 -0
- tetra_rp/client.py +136 -33
- tetra_rp/config.py +29 -0
- tetra_rp/core/api/runpod.py +591 -39
- tetra_rp/core/deployment.py +232 -0
- tetra_rp/core/discovery.py +425 -0
- tetra_rp/core/exceptions.py +50 -0
- tetra_rp/core/resources/__init__.py +27 -9
- tetra_rp/core/resources/app.py +738 -0
- tetra_rp/core/resources/base.py +139 -4
- tetra_rp/core/resources/constants.py +21 -0
- tetra_rp/core/resources/cpu.py +115 -13
- tetra_rp/core/resources/gpu.py +182 -16
- tetra_rp/core/resources/live_serverless.py +153 -16
- tetra_rp/core/resources/load_balancer_sls_resource.py +440 -0
- tetra_rp/core/resources/network_volume.py +126 -31
- tetra_rp/core/resources/resource_manager.py +436 -35
- tetra_rp/core/resources/serverless.py +537 -120
- tetra_rp/core/resources/serverless_cpu.py +201 -0
- tetra_rp/core/resources/template.py +1 -59
- tetra_rp/core/utils/constants.py +10 -0
- tetra_rp/core/utils/file_lock.py +260 -0
- tetra_rp/core/utils/http.py +67 -0
- tetra_rp/core/utils/lru_cache.py +75 -0
- tetra_rp/core/utils/singleton.py +36 -1
- tetra_rp/core/validation.py +44 -0
- tetra_rp/execute_class.py +301 -0
- tetra_rp/protos/remote_execution.py +98 -9
- tetra_rp/runtime/__init__.py +1 -0
- tetra_rp/runtime/circuit_breaker.py +274 -0
- tetra_rp/runtime/config.py +12 -0
- tetra_rp/runtime/exceptions.py +49 -0
- tetra_rp/runtime/generic_handler.py +206 -0
- tetra_rp/runtime/lb_handler.py +189 -0
- tetra_rp/runtime/load_balancer.py +160 -0
- tetra_rp/runtime/manifest_fetcher.py +192 -0
- tetra_rp/runtime/metrics.py +325 -0
- tetra_rp/runtime/models.py +73 -0
- tetra_rp/runtime/mothership_provisioner.py +512 -0
- tetra_rp/runtime/production_wrapper.py +266 -0
- tetra_rp/runtime/reliability_config.py +149 -0
- tetra_rp/runtime/retry_manager.py +118 -0
- tetra_rp/runtime/serialization.py +124 -0
- tetra_rp/runtime/service_registry.py +346 -0
- tetra_rp/runtime/state_manager_client.py +248 -0
- tetra_rp/stubs/live_serverless.py +35 -17
- tetra_rp/stubs/load_balancer_sls.py +357 -0
- tetra_rp/stubs/registry.py +145 -19
- {tetra_rp-0.6.0.dist-info → tetra_rp-0.24.0.dist-info}/METADATA +398 -60
- tetra_rp-0.24.0.dist-info/RECORD +99 -0
- {tetra_rp-0.6.0.dist-info → tetra_rp-0.24.0.dist-info}/WHEEL +1 -1
- tetra_rp-0.24.0.dist-info/entry_points.txt +2 -0
- tetra_rp/core/pool/cluster_manager.py +0 -177
- tetra_rp/core/pool/dataclass.py +0 -18
- tetra_rp/core/pool/ex.py +0 -38
- tetra_rp/core/pool/job.py +0 -22
- tetra_rp/core/pool/worker.py +0 -19
- tetra_rp/core/resources/utils.py +0 -50
- tetra_rp/core/utils/json.py +0 -33
- tetra_rp-0.6.0.dist-info/RECORD +0 -39
- /tetra_rp/{core/pool → cli}/__init__.py +0 -0
- {tetra_rp-0.6.0.dist-info → tetra_rp-0.24.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Build utilities for Flash handler generation."""
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Generator for handler_<name>.py files."""
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
import importlib.util
|
|
5
|
+
import logging
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Dict, List, Union
|
|
8
|
+
|
|
9
|
+
from tetra_rp.runtime.models import Manifest
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
HANDLER_TEMPLATE = '''"""
|
|
14
|
+
Auto-generated handler for resource: {resource_name}
|
|
15
|
+
Generated at: {timestamp}
|
|
16
|
+
|
|
17
|
+
This file is generated by the Flash build process. Do not edit manually.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import importlib
|
|
21
|
+
from tetra_rp.runtime.generic_handler import create_handler
|
|
22
|
+
|
|
23
|
+
# Import all functions/classes that belong to this resource
|
|
24
|
+
{imports}
|
|
25
|
+
|
|
26
|
+
# Function registry for this handler
|
|
27
|
+
FUNCTION_REGISTRY = {{
|
|
28
|
+
{registry}
|
|
29
|
+
}}
|
|
30
|
+
|
|
31
|
+
# Create configured handler
|
|
32
|
+
handler = create_handler(FUNCTION_REGISTRY)
|
|
33
|
+
|
|
34
|
+
if __name__ == "__main__":
|
|
35
|
+
import runpod
|
|
36
|
+
runpod.serverless.start({{"handler": handler}})
|
|
37
|
+
'''
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class HandlerGenerator:
|
|
41
|
+
"""Generates handler_<name>.py files for each resource config."""
|
|
42
|
+
|
|
43
|
+
def __init__(self, manifest: Union[Dict[str, Any], Manifest], build_dir: Path):
|
|
44
|
+
self.manifest = manifest
|
|
45
|
+
self.build_dir = build_dir
|
|
46
|
+
|
|
47
|
+
def generate_handlers(self) -> List[Path]:
|
|
48
|
+
"""Generate all handler files for queue-based (non-LB) resources."""
|
|
49
|
+
handler_paths = []
|
|
50
|
+
|
|
51
|
+
# Handle both dict and Manifest types
|
|
52
|
+
resources = (
|
|
53
|
+
self.manifest.resources
|
|
54
|
+
if isinstance(self.manifest, Manifest)
|
|
55
|
+
else self.manifest.get("resources", {})
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
for resource_name, resource_data in resources.items():
|
|
59
|
+
# Skip load-balanced resources (handled by LBHandlerGenerator)
|
|
60
|
+
# Use flag determined by isinstance() at scan time
|
|
61
|
+
is_load_balanced = (
|
|
62
|
+
resource_data.is_load_balanced
|
|
63
|
+
if hasattr(resource_data, "is_load_balanced")
|
|
64
|
+
else resource_data.get("is_load_balanced", False)
|
|
65
|
+
)
|
|
66
|
+
if is_load_balanced:
|
|
67
|
+
continue
|
|
68
|
+
|
|
69
|
+
handler_path = self._generate_handler(resource_name, resource_data)
|
|
70
|
+
handler_paths.append(handler_path)
|
|
71
|
+
|
|
72
|
+
return handler_paths
|
|
73
|
+
|
|
74
|
+
def _generate_handler(self, resource_name: str, resource_data: Any) -> Path:
|
|
75
|
+
"""Generate a single handler file."""
|
|
76
|
+
handler_filename = f"handler_{resource_name}.py"
|
|
77
|
+
handler_path = self.build_dir / handler_filename
|
|
78
|
+
|
|
79
|
+
# Get timestamp from manifest
|
|
80
|
+
timestamp = (
|
|
81
|
+
self.manifest.generated_at
|
|
82
|
+
if isinstance(self.manifest, Manifest)
|
|
83
|
+
else self.manifest.get("generated_at", "")
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Get functions from resource (handle both dict and ResourceConfig)
|
|
87
|
+
functions = (
|
|
88
|
+
resource_data.functions
|
|
89
|
+
if hasattr(resource_data, "functions")
|
|
90
|
+
else resource_data.get("functions", [])
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Generate imports section
|
|
94
|
+
imports = self._generate_imports(functions)
|
|
95
|
+
|
|
96
|
+
# Generate function registry
|
|
97
|
+
registry = self._generate_registry(functions)
|
|
98
|
+
|
|
99
|
+
# Format template
|
|
100
|
+
handler_code = HANDLER_TEMPLATE.format(
|
|
101
|
+
resource_name=resource_name,
|
|
102
|
+
timestamp=timestamp,
|
|
103
|
+
imports=imports,
|
|
104
|
+
registry=registry,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
handler_path.write_text(handler_code)
|
|
108
|
+
|
|
109
|
+
# Validate that generated handler can be imported
|
|
110
|
+
self._validate_handler_imports(handler_path)
|
|
111
|
+
|
|
112
|
+
return handler_path
|
|
113
|
+
|
|
114
|
+
def _generate_imports(self, functions: List[Any]) -> str:
|
|
115
|
+
"""Generate import statements for functions using dynamic imports.
|
|
116
|
+
|
|
117
|
+
Uses importlib.import_module() to handle module names with invalid
|
|
118
|
+
Python identifiers (e.g., names starting with digits like '01_hello_world').
|
|
119
|
+
"""
|
|
120
|
+
if not functions:
|
|
121
|
+
return "# No functions to import"
|
|
122
|
+
|
|
123
|
+
imports = []
|
|
124
|
+
for func in functions:
|
|
125
|
+
# Handle both dict and FunctionMetadata
|
|
126
|
+
module = func.module if hasattr(func, "module") else func.get("module")
|
|
127
|
+
name = func.name if hasattr(func, "name") else func.get("name")
|
|
128
|
+
|
|
129
|
+
if module and name:
|
|
130
|
+
# Use dynamic import to handle invalid identifiers
|
|
131
|
+
imports.append(f"{name} = importlib.import_module('{module}').{name}")
|
|
132
|
+
|
|
133
|
+
return "\n".join(imports) if imports else "# No functions to import"
|
|
134
|
+
|
|
135
|
+
def _generate_registry(self, functions: List[Any]) -> str:
|
|
136
|
+
"""Generate function registry dictionary."""
|
|
137
|
+
if not functions:
|
|
138
|
+
return " # No functions registered"
|
|
139
|
+
|
|
140
|
+
registry_lines = []
|
|
141
|
+
|
|
142
|
+
for func in functions:
|
|
143
|
+
# Handle both dict and FunctionMetadata
|
|
144
|
+
name = func.name if hasattr(func, "name") else func.get("name")
|
|
145
|
+
registry_lines.append(f' "{name}": {name},')
|
|
146
|
+
|
|
147
|
+
return "\n".join(registry_lines)
|
|
148
|
+
|
|
149
|
+
def _validate_handler_imports(self, handler_path: Path) -> None:
|
|
150
|
+
"""Validate that generated handler has valid Python syntax.
|
|
151
|
+
|
|
152
|
+
Attempts to load the handler module to catch syntax errors.
|
|
153
|
+
ImportErrors for missing worker modules are logged but not fatal,
|
|
154
|
+
as those imports may not be available at build time.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
handler_path: Path to generated handler file
|
|
158
|
+
|
|
159
|
+
Raises:
|
|
160
|
+
ValueError: If handler has syntax errors or cannot be parsed
|
|
161
|
+
"""
|
|
162
|
+
try:
|
|
163
|
+
spec = importlib.util.spec_from_file_location("handler", handler_path)
|
|
164
|
+
if spec and spec.loader:
|
|
165
|
+
module = importlib.util.module_from_spec(spec)
|
|
166
|
+
spec.loader.exec_module(module)
|
|
167
|
+
else:
|
|
168
|
+
raise ValueError("Failed to create module spec")
|
|
169
|
+
except SyntaxError as e:
|
|
170
|
+
raise ValueError(f"Handler has syntax errors: {e}") from e
|
|
171
|
+
except ImportError as e:
|
|
172
|
+
# Log but don't fail - imports might not be available at build time
|
|
173
|
+
logger.debug(f"Handler import validation: {e}")
|
|
174
|
+
except Exception as e:
|
|
175
|
+
# Only raise for truly unexpected errors
|
|
176
|
+
logger.warning(f"Handler validation warning: {e}")
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"""Generator for FastAPI handlers for LoadBalancerSlsResource endpoints."""
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
import logging
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Dict, List, Union
|
|
7
|
+
|
|
8
|
+
from tetra_rp.runtime.models import Manifest
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
LB_HANDLER_TEMPLATE = '''"""
|
|
13
|
+
Auto-generated FastAPI handler for LoadBalancerSlsResource: {resource_name}
|
|
14
|
+
Generated at: {timestamp}
|
|
15
|
+
|
|
16
|
+
This file is generated by the Flash build process. Do not edit manually.
|
|
17
|
+
|
|
18
|
+
Load-balanced endpoints expose HTTP servers directly to clients, enabling:
|
|
19
|
+
- REST APIs with custom HTTP routing
|
|
20
|
+
- WebSocket servers
|
|
21
|
+
- Real-time communication patterns
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import asyncio
|
|
25
|
+
import logging
|
|
26
|
+
from contextlib import asynccontextmanager
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Optional
|
|
29
|
+
|
|
30
|
+
from fastapi import FastAPI, Request
|
|
31
|
+
from tetra_rp.runtime.lb_handler import create_lb_handler
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
# Import all functions/classes that belong to this resource
|
|
36
|
+
{imports}
|
|
37
|
+
|
|
38
|
+
# Route registry: (method, path) -> function
|
|
39
|
+
ROUTE_REGISTRY = {{
|
|
40
|
+
{registry}
|
|
41
|
+
}}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# Lifespan context manager for startup/shutdown
|
|
45
|
+
@asynccontextmanager
|
|
46
|
+
async def lifespan(app: FastAPI):
|
|
47
|
+
"""Handle application startup and shutdown."""
|
|
48
|
+
# Startup
|
|
49
|
+
logger.info("Starting {resource_name} endpoint")
|
|
50
|
+
|
|
51
|
+
# Check if this is the mothership and run reconciliation
|
|
52
|
+
# Note: Resources are now provisioned upfront by the CLI during deployment.
|
|
53
|
+
# This background task runs reconciliation on mothership startup to ensure
|
|
54
|
+
# all resources are still deployed and in sync with the manifest.
|
|
55
|
+
try:
|
|
56
|
+
from tetra_rp.runtime.mothership_provisioner import (
|
|
57
|
+
is_mothership,
|
|
58
|
+
reconcile_children,
|
|
59
|
+
get_mothership_url,
|
|
60
|
+
)
|
|
61
|
+
from tetra_rp.runtime.state_manager_client import StateManagerClient
|
|
62
|
+
|
|
63
|
+
if is_mothership():
|
|
64
|
+
logger.info("=" * 60)
|
|
65
|
+
logger.info("Mothership detected - Starting reconciliation task")
|
|
66
|
+
logger.info("Resources are provisioned upfront by the CLI")
|
|
67
|
+
logger.info("This task ensures all resources remain in sync")
|
|
68
|
+
logger.info("=" * 60)
|
|
69
|
+
try:
|
|
70
|
+
mothership_url = get_mothership_url()
|
|
71
|
+
logger.info(f"Mothership URL: {{mothership_url}}")
|
|
72
|
+
|
|
73
|
+
# Initialize State Manager client for reconciliation
|
|
74
|
+
state_client = StateManagerClient()
|
|
75
|
+
|
|
76
|
+
# Spawn background reconciliation task (non-blocking)
|
|
77
|
+
# This will verify all resources from manifest are deployed
|
|
78
|
+
manifest_path = Path(__file__).parent / "flash_manifest.json"
|
|
79
|
+
task = asyncio.create_task(
|
|
80
|
+
reconcile_children(manifest_path, mothership_url, state_client)
|
|
81
|
+
)
|
|
82
|
+
# Add error callback to catch and log background task exceptions
|
|
83
|
+
task.add_done_callback(
|
|
84
|
+
lambda t: logger.error(f"Reconciliation task failed: {{t.exception()}}")
|
|
85
|
+
if t.exception()
|
|
86
|
+
else None
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
except Exception as e:
|
|
90
|
+
logger.error(f"Failed to start reconciliation task: {{e}}")
|
|
91
|
+
# Don't fail startup - continue serving traffic
|
|
92
|
+
|
|
93
|
+
except ImportError:
|
|
94
|
+
logger.debug("Mothership provisioning modules not available")
|
|
95
|
+
|
|
96
|
+
yield
|
|
97
|
+
|
|
98
|
+
# Shutdown
|
|
99
|
+
logger.info("Shutting down {resource_name} endpoint")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# Create FastAPI app with routes and lifespan
|
|
103
|
+
# Note: include_execute={include_execute} for this endpoint type
|
|
104
|
+
# - LiveLoadBalancer (local): include_execute=True for /execute endpoint
|
|
105
|
+
# - LoadBalancerSlsResource (deployed): include_execute=False (security)
|
|
106
|
+
app = create_lb_handler(ROUTE_REGISTRY, include_execute={include_execute}, lifespan=lifespan)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# Health check endpoint (required for RunPod load-balancer endpoints)
|
|
110
|
+
@app.get("/ping")
|
|
111
|
+
def ping():
|
|
112
|
+
"""Health check endpoint for RunPod load-balancer.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
dict: Status response
|
|
116
|
+
"""
|
|
117
|
+
return {{"status": "healthy"}}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
if __name__ == "__main__":
|
|
121
|
+
import uvicorn
|
|
122
|
+
# Local development server for testing
|
|
123
|
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
|
124
|
+
'''
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class LBHandlerGenerator:
|
|
128
|
+
"""Generates FastAPI handlers for LoadBalancerSlsResource endpoints."""
|
|
129
|
+
|
|
130
|
+
def __init__(self, manifest: Union[Dict[str, Any], Manifest], build_dir: Path):
|
|
131
|
+
self.manifest = manifest
|
|
132
|
+
self.build_dir = build_dir
|
|
133
|
+
|
|
134
|
+
def generate_handlers(self) -> List[Path]:
|
|
135
|
+
"""Generate all LB handler files."""
|
|
136
|
+
handler_paths = []
|
|
137
|
+
|
|
138
|
+
# Handle both dict and Manifest types
|
|
139
|
+
resources = (
|
|
140
|
+
self.manifest.resources
|
|
141
|
+
if isinstance(self.manifest, Manifest)
|
|
142
|
+
else self.manifest.get("resources", {})
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
for resource_name, resource_data in resources.items():
|
|
146
|
+
# Generate for both LiveLoadBalancer (local dev) and LoadBalancerSlsResource (deployed)
|
|
147
|
+
# Use flag determined by isinstance() at scan time
|
|
148
|
+
is_load_balanced = (
|
|
149
|
+
resource_data.is_load_balanced
|
|
150
|
+
if hasattr(resource_data, "is_load_balanced")
|
|
151
|
+
else resource_data.get("is_load_balanced", False)
|
|
152
|
+
)
|
|
153
|
+
if not is_load_balanced:
|
|
154
|
+
continue
|
|
155
|
+
|
|
156
|
+
handler_path = self._generate_handler(resource_name, resource_data)
|
|
157
|
+
handler_paths.append(handler_path)
|
|
158
|
+
|
|
159
|
+
return handler_paths
|
|
160
|
+
|
|
161
|
+
def _generate_handler(self, resource_name: str, resource_data: Any) -> Path:
|
|
162
|
+
"""Generate a single FastAPI handler file."""
|
|
163
|
+
handler_filename = f"handler_{resource_name}.py"
|
|
164
|
+
handler_path = self.build_dir / handler_filename
|
|
165
|
+
|
|
166
|
+
# Get timestamp from manifest
|
|
167
|
+
timestamp = (
|
|
168
|
+
self.manifest.generated_at
|
|
169
|
+
if isinstance(self.manifest, Manifest)
|
|
170
|
+
else self.manifest.get("generated_at", "")
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# Determine if /execute endpoint should be included
|
|
174
|
+
# LiveLoadBalancer (local dev) includes /execute, deployed LoadBalancerSlsResource does not
|
|
175
|
+
# Use flag determined by isinstance() at scan time
|
|
176
|
+
include_execute = (
|
|
177
|
+
resource_data.is_live_resource
|
|
178
|
+
if hasattr(resource_data, "is_live_resource")
|
|
179
|
+
else resource_data.get("is_live_resource", False)
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# Get functions from resource (handle both dict and ResourceConfig)
|
|
183
|
+
functions = (
|
|
184
|
+
resource_data.functions
|
|
185
|
+
if hasattr(resource_data, "functions")
|
|
186
|
+
else resource_data.get("functions", [])
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Generate imports section
|
|
190
|
+
imports = self._generate_imports(functions)
|
|
191
|
+
|
|
192
|
+
# Generate route registry
|
|
193
|
+
registry = self._generate_route_registry(functions)
|
|
194
|
+
|
|
195
|
+
# Format template
|
|
196
|
+
handler_code = LB_HANDLER_TEMPLATE.format(
|
|
197
|
+
resource_name=resource_name,
|
|
198
|
+
timestamp=timestamp,
|
|
199
|
+
imports=imports,
|
|
200
|
+
registry=registry,
|
|
201
|
+
include_execute=str(include_execute),
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
handler_path.write_text(handler_code)
|
|
205
|
+
|
|
206
|
+
# Validate that generated handler can be imported
|
|
207
|
+
self._validate_handler_imports(handler_path)
|
|
208
|
+
|
|
209
|
+
return handler_path
|
|
210
|
+
|
|
211
|
+
def _generate_imports(self, functions: List[Any]) -> str:
|
|
212
|
+
"""Generate import statements for functions.
|
|
213
|
+
|
|
214
|
+
Uses importlib to handle module paths with any characters,
|
|
215
|
+
including numeric prefixes that aren't valid Python identifiers.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
functions: List of function metadata (dicts or FunctionMetadata objects)
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
Import statements as string
|
|
222
|
+
"""
|
|
223
|
+
if not functions:
|
|
224
|
+
return "# No functions to import"
|
|
225
|
+
|
|
226
|
+
imports = ["import importlib"]
|
|
227
|
+
|
|
228
|
+
for func in functions:
|
|
229
|
+
# Handle both dict and FunctionMetadata
|
|
230
|
+
module = func.module if hasattr(func, "module") else func.get("module")
|
|
231
|
+
name = func.name if hasattr(func, "name") else func.get("name")
|
|
232
|
+
|
|
233
|
+
if module and name:
|
|
234
|
+
# Use importlib to handle module names with invalid identifiers
|
|
235
|
+
imports.append(f"{name} = importlib.import_module('{module}').{name}")
|
|
236
|
+
|
|
237
|
+
return "\n".join(imports)
|
|
238
|
+
|
|
239
|
+
def _generate_route_registry(self, functions: List[Any]) -> str:
|
|
240
|
+
"""Generate route registry for FastAPI app.
|
|
241
|
+
|
|
242
|
+
Creates mapping of (method, path) tuples to function names.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
functions: List of function metadata dicts with http_method and http_path
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
Registry dictionary as string
|
|
249
|
+
"""
|
|
250
|
+
if not functions:
|
|
251
|
+
return " # No functions registered"
|
|
252
|
+
|
|
253
|
+
registry_lines = []
|
|
254
|
+
|
|
255
|
+
for func in functions:
|
|
256
|
+
# Handle both dict and FunctionMetadata
|
|
257
|
+
name = func.name if hasattr(func, "name") else func.get("name")
|
|
258
|
+
method = (
|
|
259
|
+
func.http_method
|
|
260
|
+
if hasattr(func, "http_method")
|
|
261
|
+
else func.get("http_method")
|
|
262
|
+
)
|
|
263
|
+
path = (
|
|
264
|
+
func.http_path if hasattr(func, "http_path") else func.get("http_path")
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
if name and method and path:
|
|
268
|
+
# Create tuple key: ("GET", "/api/process")
|
|
269
|
+
registry_lines.append(f' ("{method}", "{path}"): {name},')
|
|
270
|
+
elif name:
|
|
271
|
+
# Skip if method or path missing (shouldn't happen with validation)
|
|
272
|
+
logger.warning(
|
|
273
|
+
f"Function '{name}' missing http_method or http_path. Skipping."
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
return (
|
|
277
|
+
"\n".join(registry_lines)
|
|
278
|
+
if registry_lines
|
|
279
|
+
else " # No routes registered"
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
def _validate_handler_imports(self, handler_path: Path) -> None:
|
|
283
|
+
"""Validate that generated handler has valid Python syntax.
|
|
284
|
+
|
|
285
|
+
Attempts to load the handler module to catch syntax errors.
|
|
286
|
+
ImportErrors for missing worker modules are logged but not fatal,
|
|
287
|
+
as those imports may not be available at build time.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
handler_path: Path to generated handler file
|
|
291
|
+
|
|
292
|
+
Raises:
|
|
293
|
+
ValueError: If handler has syntax errors or cannot be parsed
|
|
294
|
+
"""
|
|
295
|
+
try:
|
|
296
|
+
spec = importlib.util.spec_from_file_location("handler", handler_path)
|
|
297
|
+
if spec and spec.loader:
|
|
298
|
+
module = importlib.util.module_from_spec(spec)
|
|
299
|
+
spec.loader.exec_module(module)
|
|
300
|
+
else:
|
|
301
|
+
raise ValueError("Failed to create module spec")
|
|
302
|
+
except SyntaxError as e:
|
|
303
|
+
raise ValueError(f"Handler has syntax errors: {e}") from e
|
|
304
|
+
except ImportError as e:
|
|
305
|
+
# Log but don't fail - imports might not be available at build time
|
|
306
|
+
logger.debug(f"Handler import validation: {e}")
|
|
307
|
+
except Exception as e:
|
|
308
|
+
# Only raise for truly unexpected errors
|
|
309
|
+
logger.warning(f"Handler validation warning: {e}")
|