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 @@
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}")