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