supervaizer 0.10.5__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 (76) hide show
  1. supervaizer/__init__.py +97 -0
  2. supervaizer/__version__.py +10 -0
  3. supervaizer/account.py +308 -0
  4. supervaizer/account_service.py +93 -0
  5. supervaizer/admin/routes.py +1293 -0
  6. supervaizer/admin/static/js/job-start-form.js +373 -0
  7. supervaizer/admin/templates/agent_detail.html +145 -0
  8. supervaizer/admin/templates/agents.html +249 -0
  9. supervaizer/admin/templates/agents_grid.html +82 -0
  10. supervaizer/admin/templates/base.html +233 -0
  11. supervaizer/admin/templates/case_detail.html +230 -0
  12. supervaizer/admin/templates/cases_list.html +182 -0
  13. supervaizer/admin/templates/cases_table.html +134 -0
  14. supervaizer/admin/templates/console.html +389 -0
  15. supervaizer/admin/templates/dashboard.html +153 -0
  16. supervaizer/admin/templates/job_detail.html +192 -0
  17. supervaizer/admin/templates/job_start_test.html +109 -0
  18. supervaizer/admin/templates/jobs_list.html +180 -0
  19. supervaizer/admin/templates/jobs_table.html +122 -0
  20. supervaizer/admin/templates/navigation.html +163 -0
  21. supervaizer/admin/templates/recent_activity.html +81 -0
  22. supervaizer/admin/templates/server.html +105 -0
  23. supervaizer/admin/templates/server_status_cards.html +121 -0
  24. supervaizer/admin/templates/supervaize_instructions.html +212 -0
  25. supervaizer/agent.py +956 -0
  26. supervaizer/case.py +432 -0
  27. supervaizer/cli.py +395 -0
  28. supervaizer/common.py +324 -0
  29. supervaizer/deploy/__init__.py +16 -0
  30. supervaizer/deploy/cli.py +305 -0
  31. supervaizer/deploy/commands/__init__.py +9 -0
  32. supervaizer/deploy/commands/clean.py +294 -0
  33. supervaizer/deploy/commands/down.py +119 -0
  34. supervaizer/deploy/commands/local.py +460 -0
  35. supervaizer/deploy/commands/plan.py +167 -0
  36. supervaizer/deploy/commands/status.py +169 -0
  37. supervaizer/deploy/commands/up.py +281 -0
  38. supervaizer/deploy/docker.py +377 -0
  39. supervaizer/deploy/driver_factory.py +42 -0
  40. supervaizer/deploy/drivers/__init__.py +39 -0
  41. supervaizer/deploy/drivers/aws_app_runner.py +607 -0
  42. supervaizer/deploy/drivers/base.py +196 -0
  43. supervaizer/deploy/drivers/cloud_run.py +570 -0
  44. supervaizer/deploy/drivers/do_app_platform.py +504 -0
  45. supervaizer/deploy/health.py +404 -0
  46. supervaizer/deploy/state.py +210 -0
  47. supervaizer/deploy/templates/Dockerfile.template +44 -0
  48. supervaizer/deploy/templates/debug_env.py +69 -0
  49. supervaizer/deploy/templates/docker-compose.yml.template +37 -0
  50. supervaizer/deploy/templates/dockerignore.template +66 -0
  51. supervaizer/deploy/templates/entrypoint.sh +20 -0
  52. supervaizer/deploy/utils.py +52 -0
  53. supervaizer/event.py +181 -0
  54. supervaizer/examples/controller_template.py +196 -0
  55. supervaizer/instructions.py +145 -0
  56. supervaizer/job.py +392 -0
  57. supervaizer/job_service.py +156 -0
  58. supervaizer/lifecycle.py +417 -0
  59. supervaizer/parameter.py +233 -0
  60. supervaizer/protocol/__init__.py +11 -0
  61. supervaizer/protocol/a2a/__init__.py +21 -0
  62. supervaizer/protocol/a2a/model.py +227 -0
  63. supervaizer/protocol/a2a/routes.py +99 -0
  64. supervaizer/py.typed +1 -0
  65. supervaizer/routes.py +917 -0
  66. supervaizer/server.py +553 -0
  67. supervaizer/server_utils.py +54 -0
  68. supervaizer/storage.py +462 -0
  69. supervaizer/telemetry.py +81 -0
  70. supervaizer/utils/__init__.py +16 -0
  71. supervaizer/utils/version_check.py +56 -0
  72. supervaizer-0.10.5.dist-info/METADATA +317 -0
  73. supervaizer-0.10.5.dist-info/RECORD +76 -0
  74. supervaizer-0.10.5.dist-info/WHEEL +4 -0
  75. supervaizer-0.10.5.dist-info/entry_points.txt +2 -0
  76. supervaizer-0.10.5.dist-info/licenses/LICENSE.md +346 -0
@@ -0,0 +1,377 @@
1
+ # Copyright (c) 2024-2025 Alain Prasquier - Supervaize.com. All rights reserved.
2
+ #
3
+ # This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
4
+ # If a copy of the MPL was not distributed with this file, you can obtain one at
5
+ # https://mozilla.org/MPL/2.0/.
6
+
7
+ """
8
+ Docker Operations
9
+
10
+ This module handles Docker-related operations for deployment.
11
+ """
12
+
13
+ import os
14
+ from pathlib import Path
15
+ from typing import Optional
16
+
17
+ from rich.console import Console
18
+
19
+ from supervaizer.common import log
20
+
21
+ console = Console()
22
+
23
+ TEMPLATE_DIR = Path(__file__).parent / "templates"
24
+
25
+ # List of environment variables to include in Dockerfile
26
+ DOCKER_ENV_VARS = [
27
+ "SUPERVAIZE_API_KEY",
28
+ "SUPERVAIZE_WORKSPACE_ID",
29
+ "SUPERVAIZE_API_URL",
30
+ "SUPERVAIZER_PORT",
31
+ "SUPERVAIZER_PUBLIC_URL",
32
+ ]
33
+
34
+
35
+ def get_docker_env_vars(port: int = 8000) -> dict[str, str]:
36
+ """Get environment variables for Docker deployment.
37
+
38
+ Args:
39
+ port: The application port to use for SUPERVAIZER_PORT
40
+
41
+ Returns:
42
+ Dictionary mapping environment variable names to their values
43
+ """
44
+ env_vars = {}
45
+
46
+ for var_name in DOCKER_ENV_VARS:
47
+ if var_name == "SUPERVAIZER_PORT":
48
+ env_vars[var_name] = str(port)
49
+ else:
50
+ env_vars[var_name] = os.getenv(var_name, "")
51
+
52
+ return env_vars
53
+
54
+
55
+ def get_docker_build_args(port: int = 8000) -> dict[str, str]:
56
+ """Get build arguments for Docker deployment.
57
+
58
+ Args:
59
+ port: The application port to use for SUPERVAIZER_PORT
60
+
61
+ Returns:
62
+ Dictionary mapping build argument names to their values
63
+ """
64
+ build_args = {}
65
+
66
+ for var_name in DOCKER_ENV_VARS:
67
+ if var_name == "SUPERVAIZER_PORT":
68
+ build_args[var_name] = str(port)
69
+ else:
70
+ # Only include build args for variables that are set
71
+ value = os.getenv(var_name)
72
+ if value:
73
+ build_args[var_name] = value
74
+
75
+ return build_args
76
+
77
+
78
+ class DockerManager:
79
+ """Manages Docker operations for deployment."""
80
+
81
+ def __init__(self, require_docker: bool = True) -> None:
82
+ """Initialize Docker manager."""
83
+ self.client = None
84
+ if require_docker:
85
+ try:
86
+ from docker import DockerClient
87
+ from docker.errors import DockerException
88
+ except ImportError:
89
+ raise RuntimeError(
90
+ "Docker package not installed. Install with: pip install supervaizer[deploy]"
91
+ ) from None
92
+
93
+ try:
94
+ self.client = DockerClient.from_env()
95
+ self.client.ping() # Test connection
96
+ except DockerException as e:
97
+ log.error(f"Failed to connect to Docker: {e}")
98
+ raise RuntimeError("Docker is not running or not accessible") from e
99
+
100
+ def generate_dockerfile(
101
+ self,
102
+ output_path: Optional[Path] = None,
103
+ python_version: str = "3.12",
104
+ app_port: int = 8000,
105
+ controller_file: str = "supervaizer_control.py",
106
+ ) -> None:
107
+ """Generate a Dockerfile for Supervaizer deployment."""
108
+ if output_path is None:
109
+ output_path = Path(".deployment/Dockerfile")
110
+
111
+ # Ensure output directory exists
112
+ output_path.parent.mkdir(parents=True, exist_ok=True)
113
+
114
+ # Read template and customize it
115
+ template_path = TEMPLATE_DIR / "Dockerfile.template"
116
+ dockerfile_content = template_path.read_text()
117
+
118
+ # Replace template placeholders with actual values
119
+ dockerfile_content = dockerfile_content.replace(
120
+ "{{PYTHON_VERSION}}", python_version
121
+ )
122
+ dockerfile_content = dockerfile_content.replace("{{APP_PORT}}", str(app_port))
123
+ dockerfile_content = dockerfile_content.replace(
124
+ "{{CONTROLLER_FILE}}", controller_file
125
+ )
126
+
127
+ # Replace environment variables placeholder
128
+ env_vars = get_docker_env_vars(app_port)
129
+ env_lines = []
130
+ for var_name in env_vars.keys():
131
+ env_lines.append(f"ARG {var_name}")
132
+ env_lines.append(f"ENV {var_name}=${{{var_name}}}")
133
+ env_vars_section = "\n".join(env_lines)
134
+ dockerfile_content = dockerfile_content.replace(
135
+ "{{ENV_VARS}}", env_vars_section
136
+ )
137
+
138
+ output_path.write_text(dockerfile_content)
139
+ log.info(f"Generated Dockerfile at {output_path}")
140
+
141
+ # Copy entrypoint script to deployment directory
142
+ entrypoint_script_path = output_path.parent / "entrypoint.sh"
143
+ entrypoint_template_path = TEMPLATE_DIR / "entrypoint.sh"
144
+ if entrypoint_template_path.exists():
145
+ entrypoint_script_path.write_text(entrypoint_template_path.read_text())
146
+ # Make it executable
147
+ entrypoint_script_path.chmod(0o755)
148
+ log.info(f"Generated entrypoint script at {entrypoint_script_path}")
149
+
150
+ # Copy debug script to deployment directory
151
+ debug_script_path = output_path.parent / "debug_env.py"
152
+ debug_template_path = TEMPLATE_DIR / "debug_env.py"
153
+ if debug_template_path.exists():
154
+ debug_script_path.write_text(debug_template_path.read_text())
155
+ log.info(f"Generated debug script at {debug_script_path}")
156
+
157
+ def generate_dockerignore(self, output_path: Optional[Path] = None) -> None:
158
+ """Generate a .dockerignore file."""
159
+ if output_path is None:
160
+ output_path = Path(".deployment/.dockerignore")
161
+
162
+ # Ensure output directory exists
163
+ output_path.parent.mkdir(parents=True, exist_ok=True)
164
+
165
+ # Read template
166
+ template_path = TEMPLATE_DIR / "dockerignore.template"
167
+ dockerignore_content = template_path.read_text()
168
+
169
+ output_path.write_text(dockerignore_content)
170
+ log.info(f"Generated .dockerignore at {output_path}")
171
+
172
+ def generate_docker_compose(
173
+ self,
174
+ output_path: Optional[Path] = None,
175
+ port: int = 8000,
176
+ service_name: str = "supervaizer-dev",
177
+ environment: str = "dev",
178
+ api_key: str = "test-api-key",
179
+ rsa_key: str = "test-rsa-key",
180
+ ) -> None:
181
+ """Generate a docker-compose.yml for local testing."""
182
+ if output_path is None:
183
+ output_path = Path(".deployment/docker-compose.yml")
184
+
185
+ # Ensure output directory exists
186
+ output_path.parent.mkdir(parents=True, exist_ok=True)
187
+
188
+ # Read template and customize it
189
+ template_path = TEMPLATE_DIR / "docker-compose.yml.template"
190
+ compose_content = template_path.read_text()
191
+
192
+ # Get environment variables for build args
193
+ env_vars = get_docker_env_vars(port)
194
+
195
+ # Replace template placeholders with actual values
196
+ compose_content = compose_content.replace("{{PORT}}", str(port))
197
+ compose_content = compose_content.replace("{{SERVICE_NAME}}", service_name)
198
+ compose_content = compose_content.replace("{{ENVIRONMENT}}", environment)
199
+ compose_content = compose_content.replace("{{API_KEY}}", api_key)
200
+ compose_content = compose_content.replace("{{RSA_KEY}}", rsa_key)
201
+ compose_content = compose_content.replace(
202
+ "{{ env.SV_LOG_LEVEL | default('INFO') }}", "INFO"
203
+ )
204
+
205
+ # Replace environment variable placeholders for build args
206
+ compose_content = compose_content.replace(
207
+ "{{WORKSPACE_ID}}", env_vars.get("SUPERVAIZE_WORKSPACE_ID", "")
208
+ )
209
+ compose_content = compose_content.replace(
210
+ "{{API_URL}}", env_vars.get("SUPERVAIZE_API_URL", "")
211
+ )
212
+ compose_content = compose_content.replace(
213
+ "{{PUBLIC_URL}}", env_vars.get("SUPERVAIZER_PUBLIC_URL", "")
214
+ )
215
+
216
+ output_path.write_text(compose_content)
217
+ log.info(f"Generated docker-compose.yml at {output_path}")
218
+
219
+ def build_image(
220
+ self,
221
+ tag: str,
222
+ context_path: Optional[Path] = None,
223
+ dockerfile_path: Optional[Path] = None,
224
+ verbose: bool = False,
225
+ build_args: Optional[dict] = None,
226
+ ) -> str:
227
+ """Build Docker image and return the image ID."""
228
+ from docker.errors import APIError, BuildError, DockerException
229
+
230
+ if self.client is None:
231
+ raise RuntimeError(
232
+ "Docker client not available. Initialize DockerManager with require_docker=True"
233
+ )
234
+
235
+ if context_path is None:
236
+ context_path = Path(".")
237
+ if dockerfile_path is None:
238
+ dockerfile_path = Path(".deployment/Dockerfile")
239
+
240
+ try:
241
+ log.info(f"Building Docker image with tag: {tag}")
242
+
243
+ # Use low-level API to get logs even when build fails
244
+ # Access the low-level API through the existing client
245
+ build_kwargs = {
246
+ "path": str(context_path),
247
+ "dockerfile": str(dockerfile_path),
248
+ "tag": tag,
249
+ "rm": True,
250
+ "forcerm": True,
251
+ "buildargs": build_args or {},
252
+ "decode": True,
253
+ }
254
+
255
+ response = [line for line in self.client.api.build(**build_kwargs)]
256
+
257
+ if not response:
258
+ raise RuntimeError("Failed to get a response from docker client")
259
+
260
+ # Process and display logs (always show output, especially errors)
261
+ image_id = None
262
+ last_error = None
263
+ for result in response:
264
+ if isinstance(result, dict):
265
+ # Check for errors first
266
+ if "error" in result or "errorDetail" in result:
267
+ error_msg = result.get("error") or result.get(
268
+ "errorDetail", {}
269
+ ).get("message", "Unknown error")
270
+ last_error = error_msg
271
+
272
+ for key, value in result.items():
273
+ if isinstance(value, str):
274
+ # Always print string values (logs, errors, etc.)
275
+ print(value, end="", flush=True)
276
+ elif key == "aux" and isinstance(value, dict):
277
+ # Extract image ID from aux data
278
+ image_id = value.get("ID")
279
+
280
+ # If we found an error, raise it
281
+ if last_error:
282
+ raise RuntimeError(f"Docker build failed: {last_error}")
283
+
284
+ # If we didn't get image ID from aux, try to get it from the tag
285
+ if image_id is None:
286
+ try:
287
+ image = self.client.images.get(tag)
288
+ image_id = image.id
289
+ except DockerException:
290
+ raise RuntimeError(
291
+ "Docker build completed but no image was returned"
292
+ )
293
+
294
+ log.info(f"Successfully built image: {image_id}")
295
+ return image_id
296
+
297
+ except (BuildError, APIError) as e:
298
+ log.error(f"Failed to build Docker image: {e}")
299
+ raise RuntimeError(f"Docker build failed: {e}") from e
300
+ except DockerException as e:
301
+ log.error(f"Failed to build Docker image: {e}")
302
+ raise RuntimeError(f"Docker build failed: {e}") from e
303
+
304
+ def tag_image(self, source_tag: str, target_tag: str) -> None:
305
+ """Tag a Docker image."""
306
+ from docker.errors import DockerException
307
+
308
+ if self.client is None:
309
+ raise RuntimeError(
310
+ "Docker client not available. Initialize DockerManager with require_docker=True"
311
+ )
312
+
313
+ try:
314
+ image = self.client.images.get(source_tag)
315
+ image.tag(target_tag)
316
+ log.info(f"Tagged {source_tag} as {target_tag}")
317
+ except DockerException as e:
318
+ log.error(f"Failed to tag image: {e}")
319
+ raise RuntimeError(f"Failed to tag image: {e}") from e
320
+
321
+ def push_image(self, tag: str) -> None:
322
+ """Push Docker image to registry."""
323
+ from docker.errors import DockerException
324
+
325
+ if self.client is None:
326
+ raise RuntimeError(
327
+ "Docker client not available. Initialize DockerManager with require_docker=True"
328
+ )
329
+
330
+ try:
331
+ log.info(f"Pushing image: {tag}")
332
+ push_logs = self.client.images.push(tag, stream=True, decode=True)
333
+
334
+ for log_line in push_logs:
335
+ if "error" in log_line:
336
+ log.error(f"Push error: {log_line}")
337
+ raise RuntimeError(f"Push failed: {log_line}")
338
+ elif "status" in log_line:
339
+ log.debug(f"Push status: {log_line['status']}")
340
+
341
+ log.info(f"Successfully pushed image: {tag}")
342
+
343
+ except DockerException as e:
344
+ log.error(f"Failed to push image: {e}")
345
+ raise RuntimeError(f"Failed to push image: {e}") from e
346
+
347
+ def get_image_digest(self, tag: str) -> Optional[str]:
348
+ """Get the digest of a Docker image."""
349
+ from docker.errors import DockerException
350
+
351
+ if self.client is None:
352
+ raise RuntimeError(
353
+ "Docker client not available. Initialize DockerManager with require_docker=True"
354
+ )
355
+
356
+ try:
357
+ image = self.client.images.get(tag)
358
+ repo_digests = image.attrs.get("RepoDigests", [])
359
+ return repo_digests[0] if repo_digests else None
360
+ except DockerException as e:
361
+ log.error(f"Failed to get image digest: {e}")
362
+ return None
363
+
364
+
365
+ def ensure_docker_running() -> bool:
366
+ """Check if Docker is running and accessible."""
367
+ try:
368
+ from docker import DockerClient
369
+ from docker.errors import DockerException
370
+
371
+ client = DockerClient.from_env()
372
+ client.ping()
373
+ return True
374
+ except ImportError:
375
+ return False
376
+ except DockerException:
377
+ return False
@@ -0,0 +1,42 @@
1
+ # Copyright (c) 2024-2025 Alain Prasquier - Supervaize.com. All rights reserved.
2
+ #
3
+ # This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
4
+ # If a copy of the MPL was not distributed with this file, you can obtain one at
5
+ # https://mozilla.org/MPL/2.0/.
6
+
7
+ """
8
+ Driver Factory
9
+
10
+ This module provides a factory for creating deployment drivers.
11
+ """
12
+
13
+ from typing import Optional
14
+
15
+ from supervaizer.deploy.drivers.base import BaseDriver
16
+ from supervaizer.deploy.drivers.cloud_run import CloudRunDriver
17
+ from supervaizer.deploy.drivers.aws_app_runner import AWSAppRunnerDriver
18
+ from supervaizer.deploy.drivers.do_app_platform import DOAppPlatformDriver
19
+
20
+
21
+ def create_driver(
22
+ platform: str, region: str, project_id: Optional[str] = None
23
+ ) -> BaseDriver:
24
+ """Create a deployment driver for the specified platform."""
25
+ platform = platform.lower()
26
+
27
+ match platform:
28
+ case "cloud-run":
29
+ if not project_id:
30
+ raise ValueError("project_id is required for Cloud Run")
31
+ return CloudRunDriver(region, project_id)
32
+ case "aws-app-runner":
33
+ return AWSAppRunnerDriver(region, project_id)
34
+ case "do-app-platform":
35
+ return DOAppPlatformDriver(region, project_id)
36
+ case _:
37
+ raise ValueError(f"Unsupported platform: {platform}")
38
+
39
+
40
+ def get_supported_platforms() -> list[str]:
41
+ """Get list of supported platforms."""
42
+ return ["cloud-run", "aws-app-runner", "do-app-platform"]
@@ -0,0 +1,39 @@
1
+ # Copyright (c) 2024-2025 Alain Prasquier - Supervaize.com. All rights reserved.
2
+ #
3
+ # This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
4
+ # If a copy of the MPL was not distributed with this file, you can obtain one at
5
+ # https://mozilla.org/MPL/2.0/.
6
+
7
+ """
8
+ Deployment Drivers
9
+
10
+ This module contains platform-specific deployment drivers.
11
+ """
12
+
13
+ from .base import BaseDriver, DeploymentPlan, DeploymentResult
14
+
15
+ # Conditional imports for platform-specific drivers
16
+ try:
17
+ from .cloud_run import CloudRunDriver
18
+
19
+ CLOUD_RUN_AVAILABLE = True
20
+ except ImportError:
21
+ CLOUD_RUN_AVAILABLE = False
22
+
23
+ try:
24
+ from .aws_app_runner import AWSAppRunnerDriver
25
+
26
+ AWS_APP_RUNNER_AVAILABLE = True
27
+ except ImportError:
28
+ AWS_APP_RUNNER_AVAILABLE = False
29
+
30
+ from .do_app_platform import DOAppPlatformDriver
31
+
32
+ __all__ = [
33
+ "BaseDriver",
34
+ "DeploymentPlan",
35
+ "DeploymentResult",
36
+ "CloudRunDriver",
37
+ "AWSAppRunnerDriver",
38
+ "DOAppPlatformDriver",
39
+ ]