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