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,430 @@
|
|
|
1
|
+
"""Builder for flash_manifest.json."""
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import sys
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Dict, List, Optional
|
|
11
|
+
|
|
12
|
+
from .scanner import RemoteFunctionMetadata, detect_explicit_mothership, detect_main_app
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
RESERVED_PATHS = ["/execute", "/ping"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class ManifestFunction:
|
|
21
|
+
"""Function entry in manifest."""
|
|
22
|
+
|
|
23
|
+
name: str
|
|
24
|
+
module: str
|
|
25
|
+
is_async: bool
|
|
26
|
+
is_class: bool
|
|
27
|
+
http_method: Optional[str] = None # HTTP method for LB endpoints (GET, POST, etc.)
|
|
28
|
+
http_path: Optional[str] = None # HTTP path for LB endpoints (/api/process)
|
|
29
|
+
is_load_balanced: bool = False # Determined by isinstance() at scan time
|
|
30
|
+
is_live_resource: bool = False # LiveLoadBalancer vs LoadBalancerSlsResource
|
|
31
|
+
config_variable: Optional[str] = None # Variable name like "gpu_config"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class ManifestResource:
|
|
36
|
+
"""Resource config entry in manifest."""
|
|
37
|
+
|
|
38
|
+
resource_type: str
|
|
39
|
+
handler_file: str
|
|
40
|
+
functions: List[ManifestFunction]
|
|
41
|
+
is_load_balanced: bool = False # Determined by isinstance() at scan time
|
|
42
|
+
is_live_resource: bool = False # LiveLoadBalancer vs LoadBalancerSlsResource
|
|
43
|
+
config_variable: Optional[str] = None # Variable name for test-mothership
|
|
44
|
+
is_mothership: bool = False # Special flag for mothership endpoint
|
|
45
|
+
is_explicit: bool = False # Flag indicating explicit mothership.py configuration
|
|
46
|
+
main_file: Optional[str] = None # Filename of main.py for mothership
|
|
47
|
+
app_variable: Optional[str] = None # Variable name of FastAPI app
|
|
48
|
+
imageName: Optional[str] = None # Docker image name for auto-provisioning
|
|
49
|
+
templateId: Optional[str] = None # RunPod template ID for auto-provisioning
|
|
50
|
+
gpuIds: Optional[list] = None # GPU types/IDs for auto-provisioning
|
|
51
|
+
workersMin: Optional[int] = None # Min worker count for auto-provisioning
|
|
52
|
+
workersMax: Optional[int] = None # Max worker count for auto-provisioning
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ManifestBuilder:
|
|
56
|
+
"""Builds flash_manifest.json from discovered remote functions."""
|
|
57
|
+
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
project_name: str,
|
|
61
|
+
remote_functions: List[RemoteFunctionMetadata],
|
|
62
|
+
scanner=None,
|
|
63
|
+
build_dir: Optional[Path] = None,
|
|
64
|
+
):
|
|
65
|
+
self.project_name = project_name
|
|
66
|
+
self.remote_functions = remote_functions
|
|
67
|
+
self.scanner = (
|
|
68
|
+
scanner # Optional: RemoteDecoratorScanner with resource config info
|
|
69
|
+
)
|
|
70
|
+
self.build_dir = build_dir
|
|
71
|
+
|
|
72
|
+
def _extract_deployment_config(
|
|
73
|
+
self, resource_name: str, config_variable: Optional[str], resource_type: str
|
|
74
|
+
) -> Dict[str, Any]:
|
|
75
|
+
"""Extract deployment config (imageName, templateId, etc.) from resource object.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
resource_name: Name of the resource
|
|
79
|
+
config_variable: Variable name of the resource config (e.g., "gpu_config")
|
|
80
|
+
resource_type: Type of the resource (e.g., "LiveServerless")
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Dictionary with deployment config (may be empty if resource not found)
|
|
84
|
+
"""
|
|
85
|
+
config = {}
|
|
86
|
+
|
|
87
|
+
# If no scanner or config variable, can't extract deployment config
|
|
88
|
+
if not self.scanner or not config_variable:
|
|
89
|
+
return config
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
# Get the module where this resource is defined
|
|
93
|
+
# Try to find it in the scanner's discovered files
|
|
94
|
+
resource_file = None
|
|
95
|
+
for func in self.remote_functions:
|
|
96
|
+
if func.config_variable == config_variable:
|
|
97
|
+
resource_file = func.file_path
|
|
98
|
+
break
|
|
99
|
+
|
|
100
|
+
if not resource_file or not resource_file.exists():
|
|
101
|
+
return config
|
|
102
|
+
|
|
103
|
+
# Dynamically import the module and extract the resource config
|
|
104
|
+
spec = importlib.util.spec_from_file_location(
|
|
105
|
+
resource_file.stem, resource_file
|
|
106
|
+
)
|
|
107
|
+
if not spec or not spec.loader:
|
|
108
|
+
return config
|
|
109
|
+
|
|
110
|
+
module = importlib.util.module_from_spec(spec)
|
|
111
|
+
# Add module to sys.modules temporarily to allow relative imports
|
|
112
|
+
sys.modules[spec.name] = module
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
spec.loader.exec_module(module)
|
|
116
|
+
|
|
117
|
+
# Get the resource config object
|
|
118
|
+
if hasattr(module, config_variable):
|
|
119
|
+
resource_config = getattr(module, config_variable)
|
|
120
|
+
|
|
121
|
+
# Extract deployment config properties
|
|
122
|
+
if (
|
|
123
|
+
hasattr(resource_config, "imageName")
|
|
124
|
+
and resource_config.imageName
|
|
125
|
+
):
|
|
126
|
+
config["imageName"] = resource_config.imageName
|
|
127
|
+
|
|
128
|
+
if (
|
|
129
|
+
hasattr(resource_config, "templateId")
|
|
130
|
+
and resource_config.templateId
|
|
131
|
+
):
|
|
132
|
+
config["templateId"] = resource_config.templateId
|
|
133
|
+
|
|
134
|
+
if hasattr(resource_config, "gpuIds") and resource_config.gpuIds:
|
|
135
|
+
config["gpuIds"] = resource_config.gpuIds
|
|
136
|
+
|
|
137
|
+
if hasattr(resource_config, "workersMin"):
|
|
138
|
+
config["workersMin"] = resource_config.workersMin
|
|
139
|
+
|
|
140
|
+
if hasattr(resource_config, "workersMax"):
|
|
141
|
+
config["workersMax"] = resource_config.workersMax
|
|
142
|
+
|
|
143
|
+
# Extract template configuration if present
|
|
144
|
+
if (
|
|
145
|
+
hasattr(resource_config, "template")
|
|
146
|
+
and resource_config.template
|
|
147
|
+
):
|
|
148
|
+
template_obj = resource_config.template
|
|
149
|
+
template_config = {}
|
|
150
|
+
|
|
151
|
+
# Extract only the configurable template fields
|
|
152
|
+
if hasattr(template_obj, "containerDiskInGb"):
|
|
153
|
+
template_config["containerDiskInGb"] = (
|
|
154
|
+
template_obj.containerDiskInGb
|
|
155
|
+
)
|
|
156
|
+
if hasattr(template_obj, "dockerArgs"):
|
|
157
|
+
template_config["dockerArgs"] = template_obj.dockerArgs
|
|
158
|
+
if hasattr(template_obj, "startScript"):
|
|
159
|
+
template_config["startScript"] = template_obj.startScript
|
|
160
|
+
if hasattr(template_obj, "advancedStart"):
|
|
161
|
+
template_config["advancedStart"] = (
|
|
162
|
+
template_obj.advancedStart
|
|
163
|
+
)
|
|
164
|
+
if hasattr(template_obj, "containerRegistryAuthId"):
|
|
165
|
+
template_config["containerRegistryAuthId"] = (
|
|
166
|
+
template_obj.containerRegistryAuthId
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
if template_config:
|
|
170
|
+
config["template"] = template_config
|
|
171
|
+
|
|
172
|
+
finally:
|
|
173
|
+
# Clean up module from sys.modules to avoid conflicts
|
|
174
|
+
if spec.name in sys.modules:
|
|
175
|
+
del sys.modules[spec.name]
|
|
176
|
+
|
|
177
|
+
except Exception as e:
|
|
178
|
+
# Log warning but don't fail - deployment config is optional
|
|
179
|
+
logger.debug(
|
|
180
|
+
f"Failed to extract deployment config for {resource_name}: {e}"
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
return config
|
|
184
|
+
|
|
185
|
+
def _create_mothership_resource(self, main_app_config: dict) -> Dict[str, Any]:
|
|
186
|
+
"""Create implicit mothership resource from main.py.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
main_app_config: Dict with 'file_path', 'app_variable', 'has_routes' keys
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
Dictionary representing the mothership resource for the manifest
|
|
193
|
+
"""
|
|
194
|
+
return {
|
|
195
|
+
"resource_type": "CpuLiveLoadBalancer",
|
|
196
|
+
"handler_file": "handler_mothership.py",
|
|
197
|
+
"functions": [],
|
|
198
|
+
"is_load_balanced": True,
|
|
199
|
+
"is_live_resource": True,
|
|
200
|
+
"is_mothership": True,
|
|
201
|
+
"main_file": main_app_config["file_path"].name,
|
|
202
|
+
"app_variable": main_app_config["app_variable"],
|
|
203
|
+
"imageName": "runpod/tetra-rp-lb-cpu:latest",
|
|
204
|
+
"workersMin": 1,
|
|
205
|
+
"workersMax": 3,
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
def _create_mothership_from_explicit(
|
|
209
|
+
self, explicit_config: dict, search_dir: Path
|
|
210
|
+
) -> Dict[str, Any]:
|
|
211
|
+
"""Create mothership resource from explicit mothership.py configuration.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
explicit_config: Configuration dict from detect_explicit_mothership()
|
|
215
|
+
search_dir: Project directory
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
Dictionary representing the mothership resource for the manifest
|
|
219
|
+
"""
|
|
220
|
+
# Detect FastAPI app details for handler generation
|
|
221
|
+
main_app_config = detect_main_app(search_dir, explicit_mothership_exists=False)
|
|
222
|
+
|
|
223
|
+
if not main_app_config:
|
|
224
|
+
# No FastAPI app found, use defaults
|
|
225
|
+
main_file = "main.py"
|
|
226
|
+
app_variable = "app"
|
|
227
|
+
else:
|
|
228
|
+
main_file = main_app_config["file_path"].name
|
|
229
|
+
app_variable = main_app_config["app_variable"]
|
|
230
|
+
|
|
231
|
+
# Map resource type to image name
|
|
232
|
+
resource_type = explicit_config.get("resource_type", "CpuLiveLoadBalancer")
|
|
233
|
+
if resource_type == "LiveLoadBalancer":
|
|
234
|
+
image_name = "runpod/tetra-rp-lb:latest" # GPU load balancer
|
|
235
|
+
else:
|
|
236
|
+
image_name = "runpod/tetra-rp-lb-cpu:latest" # CPU load balancer
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
"resource_type": resource_type,
|
|
240
|
+
"handler_file": "handler_mothership.py",
|
|
241
|
+
"functions": [],
|
|
242
|
+
"is_load_balanced": True,
|
|
243
|
+
"is_live_resource": True,
|
|
244
|
+
"is_mothership": True,
|
|
245
|
+
"is_explicit": True, # Flag to indicate explicit configuration
|
|
246
|
+
"main_file": main_file,
|
|
247
|
+
"app_variable": app_variable,
|
|
248
|
+
"imageName": image_name,
|
|
249
|
+
"workersMin": explicit_config.get("workersMin", 1),
|
|
250
|
+
"workersMax": explicit_config.get("workersMax", 3),
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
def build(self) -> Dict[str, Any]:
|
|
254
|
+
"""Build the manifest dictionary."""
|
|
255
|
+
# Group functions by resource_config_name
|
|
256
|
+
resources: Dict[str, List[RemoteFunctionMetadata]] = {}
|
|
257
|
+
|
|
258
|
+
for func in self.remote_functions:
|
|
259
|
+
if func.resource_config_name not in resources:
|
|
260
|
+
resources[func.resource_config_name] = []
|
|
261
|
+
resources[func.resource_config_name].append(func)
|
|
262
|
+
|
|
263
|
+
# Build manifest structure
|
|
264
|
+
resources_dict: Dict[str, Dict[str, Any]] = {}
|
|
265
|
+
function_registry: Dict[str, str] = {}
|
|
266
|
+
routes_dict: Dict[
|
|
267
|
+
str, Dict[str, str]
|
|
268
|
+
] = {} # resource_name -> {route_key -> function_name}
|
|
269
|
+
|
|
270
|
+
for resource_name, functions in sorted(resources.items()):
|
|
271
|
+
handler_file = f"handler_{resource_name}.py"
|
|
272
|
+
|
|
273
|
+
# Use actual resource type from first function in group
|
|
274
|
+
resource_type = (
|
|
275
|
+
functions[0].resource_type if functions else "LiveServerless"
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
# Extract flags from first function (determined by isinstance() at scan time)
|
|
279
|
+
is_load_balanced = functions[0].is_load_balanced if functions else False
|
|
280
|
+
is_live_resource = functions[0].is_live_resource if functions else False
|
|
281
|
+
|
|
282
|
+
# Validate and collect routing for LB endpoints
|
|
283
|
+
resource_routes = {}
|
|
284
|
+
if is_load_balanced:
|
|
285
|
+
for f in functions:
|
|
286
|
+
if not f.http_method or not f.http_path:
|
|
287
|
+
raise ValueError(
|
|
288
|
+
f"{resource_type} endpoint '{resource_name}' requires "
|
|
289
|
+
f"method and path for function '{f.function_name}'. "
|
|
290
|
+
f"Got method={f.http_method}, path={f.http_path}"
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
# Check for route conflicts (same method + path)
|
|
294
|
+
route_key = f"{f.http_method} {f.http_path}"
|
|
295
|
+
if route_key in resource_routes:
|
|
296
|
+
raise ValueError(
|
|
297
|
+
f"Duplicate route '{route_key}' in resource '{resource_name}': "
|
|
298
|
+
f"both '{resource_routes[route_key]}' and '{f.function_name}' "
|
|
299
|
+
f"are mapped to the same route"
|
|
300
|
+
)
|
|
301
|
+
resource_routes[route_key] = f.function_name
|
|
302
|
+
|
|
303
|
+
# Check for reserved paths
|
|
304
|
+
if f.http_path in RESERVED_PATHS:
|
|
305
|
+
raise ValueError(
|
|
306
|
+
f"Function '{f.function_name}' cannot use reserved path '{f.http_path}'. "
|
|
307
|
+
f"Reserved paths: {', '.join(RESERVED_PATHS)}"
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
# Extract config_variable from first function (all functions in same resource share same config)
|
|
311
|
+
config_variable = functions[0].config_variable if functions else None
|
|
312
|
+
|
|
313
|
+
functions_list = [
|
|
314
|
+
{
|
|
315
|
+
"name": f.function_name,
|
|
316
|
+
"module": f.module_path,
|
|
317
|
+
"is_async": f.is_async,
|
|
318
|
+
"is_class": f.is_class,
|
|
319
|
+
"is_load_balanced": f.is_load_balanced,
|
|
320
|
+
"is_live_resource": f.is_live_resource,
|
|
321
|
+
"config_variable": f.config_variable,
|
|
322
|
+
**(
|
|
323
|
+
{"http_method": f.http_method, "http_path": f.http_path}
|
|
324
|
+
if is_load_balanced
|
|
325
|
+
else {}
|
|
326
|
+
),
|
|
327
|
+
}
|
|
328
|
+
for f in functions
|
|
329
|
+
]
|
|
330
|
+
|
|
331
|
+
# Extract deployment config (imageName, templateId, etc.) for auto-provisioning
|
|
332
|
+
deployment_config = self._extract_deployment_config(
|
|
333
|
+
resource_name, config_variable, resource_type
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
resources_dict[resource_name] = {
|
|
337
|
+
"resource_type": resource_type,
|
|
338
|
+
"handler_file": handler_file,
|
|
339
|
+
"functions": functions_list,
|
|
340
|
+
"is_load_balanced": is_load_balanced,
|
|
341
|
+
"is_live_resource": is_live_resource,
|
|
342
|
+
"config_variable": config_variable,
|
|
343
|
+
**deployment_config, # Include imageName, templateId, gpuIds, workers config
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
# Store routes for LB endpoints
|
|
347
|
+
if resource_routes:
|
|
348
|
+
routes_dict[resource_name] = resource_routes
|
|
349
|
+
|
|
350
|
+
# Build function registry for quick lookup
|
|
351
|
+
for f in functions:
|
|
352
|
+
if f.function_name in function_registry:
|
|
353
|
+
raise ValueError(
|
|
354
|
+
f"Duplicate function name '{f.function_name}' found in "
|
|
355
|
+
f"resources '{function_registry[f.function_name]}' and '{resource_name}'"
|
|
356
|
+
)
|
|
357
|
+
function_registry[f.function_name] = resource_name
|
|
358
|
+
|
|
359
|
+
# === MOTHERSHIP DETECTION (EXPLICIT THEN FALLBACK) ===
|
|
360
|
+
search_dir = self.build_dir if self.build_dir else Path.cwd()
|
|
361
|
+
|
|
362
|
+
# Step 1: Check for explicit mothership.py
|
|
363
|
+
explicit_mothership = detect_explicit_mothership(search_dir)
|
|
364
|
+
|
|
365
|
+
if explicit_mothership:
|
|
366
|
+
# Use explicit configuration
|
|
367
|
+
logger.info("Found explicit mothership configuration in mothership.py")
|
|
368
|
+
|
|
369
|
+
# Check for name conflict
|
|
370
|
+
mothership_name = explicit_mothership.get("name", "mothership")
|
|
371
|
+
if mothership_name in resources_dict:
|
|
372
|
+
logger.warning(
|
|
373
|
+
f"Project has a @remote resource named '{mothership_name}'. "
|
|
374
|
+
f"Using 'mothership-entrypoint' for explicit mothership endpoint."
|
|
375
|
+
)
|
|
376
|
+
mothership_name = "mothership-entrypoint"
|
|
377
|
+
|
|
378
|
+
# Create mothership resource from explicit config
|
|
379
|
+
mothership_resource = self._create_mothership_from_explicit(
|
|
380
|
+
explicit_mothership, search_dir
|
|
381
|
+
)
|
|
382
|
+
resources_dict[mothership_name] = mothership_resource
|
|
383
|
+
|
|
384
|
+
else:
|
|
385
|
+
# Step 2: Fallback to auto-detection
|
|
386
|
+
main_app_config = detect_main_app(
|
|
387
|
+
search_dir, explicit_mothership_exists=False
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
if main_app_config and main_app_config["has_routes"]:
|
|
391
|
+
logger.warning(
|
|
392
|
+
"Auto-detected FastAPI app in main.py (no mothership.py found). "
|
|
393
|
+
"Consider running 'flash init' to create explicit mothership configuration."
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
# Check for name conflict
|
|
397
|
+
if "mothership" in resources_dict:
|
|
398
|
+
logger.warning(
|
|
399
|
+
"Project has a @remote resource named 'mothership'. "
|
|
400
|
+
"Using 'mothership-entrypoint' for auto-generated mothership endpoint."
|
|
401
|
+
)
|
|
402
|
+
mothership_name = "mothership-entrypoint"
|
|
403
|
+
else:
|
|
404
|
+
mothership_name = "mothership"
|
|
405
|
+
|
|
406
|
+
# Create mothership resource from auto-detection (legacy behavior)
|
|
407
|
+
mothership_resource = self._create_mothership_resource(main_app_config)
|
|
408
|
+
resources_dict[mothership_name] = mothership_resource
|
|
409
|
+
|
|
410
|
+
manifest = {
|
|
411
|
+
"version": "1.0",
|
|
412
|
+
"generated_at": datetime.now(timezone.utc)
|
|
413
|
+
.isoformat()
|
|
414
|
+
.replace("+00:00", "Z"),
|
|
415
|
+
"project_name": self.project_name,
|
|
416
|
+
"resources": resources_dict,
|
|
417
|
+
"function_registry": function_registry,
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
# Add routes section if there are LB endpoints with routing
|
|
421
|
+
if routes_dict:
|
|
422
|
+
manifest["routes"] = routes_dict
|
|
423
|
+
|
|
424
|
+
return manifest
|
|
425
|
+
|
|
426
|
+
def write_to_file(self, output_path: Path) -> Path:
|
|
427
|
+
"""Write manifest to file."""
|
|
428
|
+
manifest = self.build()
|
|
429
|
+
output_path.write_text(json.dumps(manifest, indent=2))
|
|
430
|
+
return output_path
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Generator for mothership handler that serves main.py FastAPI app."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
MOTHERSHIP_HANDLER_TEMPLATE = '''"""Auto-generated handler for mothership endpoint."""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
12
|
+
from {main_module} import {app_variable} as app
|
|
13
|
+
|
|
14
|
+
# Add CORS middleware for cross-origin requests
|
|
15
|
+
app.add_middleware(
|
|
16
|
+
CORSMiddleware,
|
|
17
|
+
allow_origins=["*"],
|
|
18
|
+
allow_credentials=True,
|
|
19
|
+
allow_methods=["*"],
|
|
20
|
+
allow_headers=["*"],
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# Mothership endpoint information (set by RunPod when deployed)
|
|
24
|
+
MOTHERSHIP_ID = os.getenv("RUNPOD_ENDPOINT_ID")
|
|
25
|
+
MOTHERSHIP_URL = os.getenv("RUNPOD_ENDPOINT_URL")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@app.get("/ping")
|
|
29
|
+
async def ping():
|
|
30
|
+
"""Health check endpoint required by RunPod."""
|
|
31
|
+
return {{"status": "healthy", "endpoint": "mothership", "id": MOTHERSHIP_ID}}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
if __name__ == "__main__":
|
|
35
|
+
import uvicorn
|
|
36
|
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
|
37
|
+
'''
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def generate_mothership_handler(
|
|
41
|
+
main_file: str,
|
|
42
|
+
app_variable: str,
|
|
43
|
+
output_path: Path,
|
|
44
|
+
) -> None:
|
|
45
|
+
"""Generate handler that imports and serves user's main.py FastAPI app.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
main_file: Filename of main.py (e.g., "main.py")
|
|
49
|
+
app_variable: Name of the FastAPI app variable (e.g., "app")
|
|
50
|
+
output_path: Path where to write the generated handler file
|
|
51
|
+
|
|
52
|
+
Raises:
|
|
53
|
+
ValueError: If parameters are invalid
|
|
54
|
+
"""
|
|
55
|
+
if not main_file or not main_file.endswith(".py"):
|
|
56
|
+
raise ValueError(f"Invalid main_file: {main_file}")
|
|
57
|
+
|
|
58
|
+
if not app_variable or not app_variable.isidentifier():
|
|
59
|
+
raise ValueError(f"Invalid app_variable: {app_variable}")
|
|
60
|
+
|
|
61
|
+
# Convert filename to module name (e.g., "main.py" -> "main")
|
|
62
|
+
main_module = main_file.replace(".py", "")
|
|
63
|
+
|
|
64
|
+
# Generate handler code
|
|
65
|
+
handler_code = MOTHERSHIP_HANDLER_TEMPLATE.format(
|
|
66
|
+
main_module=main_module,
|
|
67
|
+
app_variable=app_variable,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Ensure output directory exists
|
|
71
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
72
|
+
|
|
73
|
+
# Write handler file
|
|
74
|
+
output_path.write_text(handler_code)
|
|
75
|
+
logger.info(f"Generated mothership handler: {output_path}")
|