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.
Files changed (97) hide show
  1. tetra_rp/__init__.py +109 -19
  2. tetra_rp/cli/commands/__init__.py +1 -0
  3. tetra_rp/cli/commands/apps.py +143 -0
  4. tetra_rp/cli/commands/build.py +1082 -0
  5. tetra_rp/cli/commands/build_utils/__init__.py +1 -0
  6. tetra_rp/cli/commands/build_utils/handler_generator.py +176 -0
  7. tetra_rp/cli/commands/build_utils/lb_handler_generator.py +309 -0
  8. tetra_rp/cli/commands/build_utils/manifest.py +430 -0
  9. tetra_rp/cli/commands/build_utils/mothership_handler_generator.py +75 -0
  10. tetra_rp/cli/commands/build_utils/scanner.py +596 -0
  11. tetra_rp/cli/commands/deploy.py +580 -0
  12. tetra_rp/cli/commands/init.py +123 -0
  13. tetra_rp/cli/commands/resource.py +108 -0
  14. tetra_rp/cli/commands/run.py +296 -0
  15. tetra_rp/cli/commands/test_mothership.py +458 -0
  16. tetra_rp/cli/commands/undeploy.py +533 -0
  17. tetra_rp/cli/main.py +97 -0
  18. tetra_rp/cli/utils/__init__.py +1 -0
  19. tetra_rp/cli/utils/app.py +15 -0
  20. tetra_rp/cli/utils/conda.py +127 -0
  21. tetra_rp/cli/utils/deployment.py +530 -0
  22. tetra_rp/cli/utils/ignore.py +143 -0
  23. tetra_rp/cli/utils/skeleton.py +184 -0
  24. tetra_rp/cli/utils/skeleton_template/.env.example +4 -0
  25. tetra_rp/cli/utils/skeleton_template/.flashignore +40 -0
  26. tetra_rp/cli/utils/skeleton_template/.gitignore +44 -0
  27. tetra_rp/cli/utils/skeleton_template/README.md +263 -0
  28. tetra_rp/cli/utils/skeleton_template/main.py +44 -0
  29. tetra_rp/cli/utils/skeleton_template/mothership.py +55 -0
  30. tetra_rp/cli/utils/skeleton_template/pyproject.toml +58 -0
  31. tetra_rp/cli/utils/skeleton_template/requirements.txt +1 -0
  32. tetra_rp/cli/utils/skeleton_template/workers/__init__.py +0 -0
  33. tetra_rp/cli/utils/skeleton_template/workers/cpu/__init__.py +19 -0
  34. tetra_rp/cli/utils/skeleton_template/workers/cpu/endpoint.py +36 -0
  35. tetra_rp/cli/utils/skeleton_template/workers/gpu/__init__.py +19 -0
  36. tetra_rp/cli/utils/skeleton_template/workers/gpu/endpoint.py +61 -0
  37. tetra_rp/client.py +136 -33
  38. tetra_rp/config.py +29 -0
  39. tetra_rp/core/api/runpod.py +591 -39
  40. tetra_rp/core/deployment.py +232 -0
  41. tetra_rp/core/discovery.py +425 -0
  42. tetra_rp/core/exceptions.py +50 -0
  43. tetra_rp/core/resources/__init__.py +27 -9
  44. tetra_rp/core/resources/app.py +738 -0
  45. tetra_rp/core/resources/base.py +139 -4
  46. tetra_rp/core/resources/constants.py +21 -0
  47. tetra_rp/core/resources/cpu.py +115 -13
  48. tetra_rp/core/resources/gpu.py +182 -16
  49. tetra_rp/core/resources/live_serverless.py +153 -16
  50. tetra_rp/core/resources/load_balancer_sls_resource.py +440 -0
  51. tetra_rp/core/resources/network_volume.py +126 -31
  52. tetra_rp/core/resources/resource_manager.py +436 -35
  53. tetra_rp/core/resources/serverless.py +537 -120
  54. tetra_rp/core/resources/serverless_cpu.py +201 -0
  55. tetra_rp/core/resources/template.py +1 -59
  56. tetra_rp/core/utils/constants.py +10 -0
  57. tetra_rp/core/utils/file_lock.py +260 -0
  58. tetra_rp/core/utils/http.py +67 -0
  59. tetra_rp/core/utils/lru_cache.py +75 -0
  60. tetra_rp/core/utils/singleton.py +36 -1
  61. tetra_rp/core/validation.py +44 -0
  62. tetra_rp/execute_class.py +301 -0
  63. tetra_rp/protos/remote_execution.py +98 -9
  64. tetra_rp/runtime/__init__.py +1 -0
  65. tetra_rp/runtime/circuit_breaker.py +274 -0
  66. tetra_rp/runtime/config.py +12 -0
  67. tetra_rp/runtime/exceptions.py +49 -0
  68. tetra_rp/runtime/generic_handler.py +206 -0
  69. tetra_rp/runtime/lb_handler.py +189 -0
  70. tetra_rp/runtime/load_balancer.py +160 -0
  71. tetra_rp/runtime/manifest_fetcher.py +192 -0
  72. tetra_rp/runtime/metrics.py +325 -0
  73. tetra_rp/runtime/models.py +73 -0
  74. tetra_rp/runtime/mothership_provisioner.py +512 -0
  75. tetra_rp/runtime/production_wrapper.py +266 -0
  76. tetra_rp/runtime/reliability_config.py +149 -0
  77. tetra_rp/runtime/retry_manager.py +118 -0
  78. tetra_rp/runtime/serialization.py +124 -0
  79. tetra_rp/runtime/service_registry.py +346 -0
  80. tetra_rp/runtime/state_manager_client.py +248 -0
  81. tetra_rp/stubs/live_serverless.py +35 -17
  82. tetra_rp/stubs/load_balancer_sls.py +357 -0
  83. tetra_rp/stubs/registry.py +145 -19
  84. {tetra_rp-0.6.0.dist-info → tetra_rp-0.24.0.dist-info}/METADATA +398 -60
  85. tetra_rp-0.24.0.dist-info/RECORD +99 -0
  86. {tetra_rp-0.6.0.dist-info → tetra_rp-0.24.0.dist-info}/WHEEL +1 -1
  87. tetra_rp-0.24.0.dist-info/entry_points.txt +2 -0
  88. tetra_rp/core/pool/cluster_manager.py +0 -177
  89. tetra_rp/core/pool/dataclass.py +0 -18
  90. tetra_rp/core/pool/ex.py +0 -38
  91. tetra_rp/core/pool/job.py +0 -22
  92. tetra_rp/core/pool/worker.py +0 -19
  93. tetra_rp/core/resources/utils.py +0 -50
  94. tetra_rp/core/utils/json.py +0 -33
  95. tetra_rp-0.6.0.dist-info/RECORD +0 -39
  96. /tetra_rp/{core/pool → cli}/__init__.py +0 -0
  97. {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}")