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,504 @@
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
+ DigitalOcean App Platform Driver
9
+
10
+ This module implements deployment to DigitalOcean App Platform.
11
+ """
12
+
13
+ import json
14
+ import subprocess
15
+ import time
16
+ from pathlib import Path
17
+ from typing import Dict, List, Optional
18
+
19
+ from rich.console import Console
20
+
21
+ from supervaizer.common import log
22
+ from supervaizer.deploy.drivers.base import (
23
+ ActionType,
24
+ BaseDriver,
25
+ DeploymentPlan,
26
+ DeploymentResult,
27
+ ResourceAction,
28
+ ResourceType,
29
+ )
30
+
31
+ console = Console()
32
+
33
+
34
+ class DOAppPlatformDriver(BaseDriver):
35
+ """Driver for deploying to DigitalOcean App Platform."""
36
+
37
+ def __init__(self, region: str, project_id: Optional[str] = None):
38
+ """Initialize DigitalOcean App Platform driver."""
39
+ super().__init__(region, project_id)
40
+ self.project_id = project_id
41
+
42
+ def plan_deployment(
43
+ self,
44
+ service_name: str,
45
+ environment: str,
46
+ image_tag: str,
47
+ port: int = 8000,
48
+ env_vars: Optional[Dict[str, str]] = None,
49
+ secrets: Optional[Dict[str, str]] = None,
50
+ ) -> DeploymentPlan:
51
+ """Plan deployment changes without applying them."""
52
+ full_service_name = self.get_service_key(service_name, environment)
53
+
54
+ actions = []
55
+ current_image = None
56
+ current_url = None
57
+ current_status = None
58
+
59
+ # Check if app exists
60
+ try:
61
+ result = subprocess.run(
62
+ ["doctl", "apps", "get", full_service_name, "--format", "json"],
63
+ capture_output=True,
64
+ text=True,
65
+ check=True,
66
+ )
67
+ app_data = json.loads(result.stdout)
68
+ current_url = (
69
+ app_data.get("spec", {}).get("ingress", {}).get("default_route")
70
+ )
71
+ current_status = app_data.get("last_deployment_active_at")
72
+
73
+ # Check if update is needed
74
+ actions.append(
75
+ ResourceAction(
76
+ resource_type=ResourceType.SERVICE,
77
+ action_type=ActionType.UPDATE,
78
+ resource_name=full_service_name,
79
+ description=f"Update App Platform app with image {image_tag}",
80
+ )
81
+ )
82
+ except subprocess.CalledProcessError:
83
+ # App doesn't exist, need to create
84
+ actions.append(
85
+ ResourceAction(
86
+ resource_type=ResourceType.SERVICE,
87
+ action_type=ActionType.CREATE,
88
+ resource_name=full_service_name,
89
+ description=f"Create new App Platform app with image {image_tag}",
90
+ )
91
+ )
92
+
93
+ # Check registry
94
+ registry_name = f"{service_name}-{environment}"
95
+ try:
96
+ result = subprocess.run(
97
+ ["doctl", "registry", "get", registry_name, "--format", "json"],
98
+ capture_output=True,
99
+ text=True,
100
+ check=True,
101
+ )
102
+ actions.append(
103
+ ResourceAction(
104
+ resource_type=ResourceType.REGISTRY,
105
+ action_type=ActionType.NOOP,
106
+ resource_name=registry_name,
107
+ description="Container registry exists",
108
+ )
109
+ )
110
+ except subprocess.CalledProcessError:
111
+ actions.append(
112
+ ResourceAction(
113
+ resource_type=ResourceType.REGISTRY,
114
+ action_type=ActionType.CREATE,
115
+ resource_name=registry_name,
116
+ description=f"Create container registry {registry_name}",
117
+ )
118
+ )
119
+
120
+ return DeploymentPlan(
121
+ platform="do-app-platform",
122
+ service_name=service_name,
123
+ environment=environment,
124
+ region=self.region,
125
+ project_id=self.project_id,
126
+ actions=actions,
127
+ current_image=current_image,
128
+ current_url=current_url,
129
+ current_status=current_status,
130
+ target_image=image_tag,
131
+ target_port=port,
132
+ target_env_vars=env_vars or {},
133
+ target_secrets=secrets or {},
134
+ )
135
+
136
+ def deploy_service(
137
+ self,
138
+ service_name: str,
139
+ environment: str,
140
+ image_tag: str,
141
+ port: int = 8000,
142
+ env_vars: Optional[Dict[str, str]] = None,
143
+ secrets: Optional[Dict[str, str]] = None,
144
+ timeout: int = 300,
145
+ ) -> DeploymentResult:
146
+ """Deploy or update the service."""
147
+ start_time = time.time()
148
+ full_service_name = self.get_service_key(service_name, environment)
149
+
150
+ try:
151
+ # Ensure registry exists
152
+ registry_name = f"{service_name}-{environment}"
153
+ self._ensure_registry(registry_name)
154
+
155
+ # Create/update app spec
156
+ app_spec_path = self._create_app_spec(
157
+ full_service_name,
158
+ registry_name,
159
+ image_tag,
160
+ port,
161
+ env_vars or {},
162
+ secrets or {},
163
+ )
164
+
165
+ # Deploy app
166
+ service_url = self._deploy_app(full_service_name, app_spec_path, timeout)
167
+
168
+ # Set SUPERVAIZER_PUBLIC_URL
169
+ if service_url:
170
+ self._set_public_url(full_service_name, service_url, app_spec_path)
171
+
172
+ # Verify health
173
+ health_status = (
174
+ "healthy"
175
+ if service_url and self.verify_health(service_url)
176
+ else "unhealthy"
177
+ )
178
+
179
+ deployment_time = time.time() - start_time
180
+
181
+ return DeploymentResult(
182
+ success=True,
183
+ service_url=service_url,
184
+ service_id=full_service_name,
185
+ status="running",
186
+ health_status=health_status,
187
+ deployment_time=deployment_time,
188
+ )
189
+
190
+ except Exception as e:
191
+ log.error(f"Deployment failed: {e}")
192
+ return DeploymentResult(
193
+ success=False,
194
+ error_message=str(e),
195
+ error_details={"exception_type": type(e).__name__},
196
+ )
197
+
198
+ def destroy_service(
199
+ self,
200
+ service_name: str,
201
+ environment: str,
202
+ keep_secrets: bool = False,
203
+ ) -> DeploymentResult:
204
+ """Destroy the service and cleanup resources."""
205
+ full_service_name = self.get_service_key(service_name, environment)
206
+
207
+ try:
208
+ # Delete app
209
+ subprocess.run(
210
+ ["doctl", "apps", "delete", full_service_name, "--force"], check=True
211
+ )
212
+ log.info(f"Deleted App Platform app: {full_service_name}")
213
+
214
+ # Delete registry
215
+ registry_name = f"{service_name}-{environment}"
216
+ try:
217
+ subprocess.run(
218
+ ["doctl", "registry", "delete", registry_name, "--force"],
219
+ check=True,
220
+ )
221
+ log.info(f"Deleted container registry: {registry_name}")
222
+ except subprocess.CalledProcessError:
223
+ log.warning(f"Failed to delete registry {registry_name}")
224
+
225
+ return DeploymentResult(
226
+ success=True,
227
+ status="deleted",
228
+ )
229
+
230
+ except subprocess.CalledProcessError as e:
231
+ log.error(f"Failed to destroy service: {e}")
232
+ return DeploymentResult(
233
+ success=False,
234
+ error_message=str(e),
235
+ )
236
+
237
+ def get_service_status(
238
+ self,
239
+ service_name: str,
240
+ environment: str,
241
+ ) -> DeploymentResult:
242
+ """Get current service status and health."""
243
+ full_service_name = self.get_service_key(service_name, environment)
244
+
245
+ try:
246
+ result = subprocess.run(
247
+ ["doctl", "apps", "get", full_service_name, "--format", "json"],
248
+ capture_output=True,
249
+ text=True,
250
+ check=True,
251
+ )
252
+ app_data = json.loads(result.stdout)
253
+
254
+ service_url = (
255
+ app_data.get("spec", {}).get("ingress", {}).get("default_route")
256
+ )
257
+ status = app_data.get("last_deployment_active_at", "unknown")
258
+
259
+ # Check health
260
+ health_status = "unknown"
261
+ if service_url:
262
+ health_status = (
263
+ "healthy" if self.verify_health(service_url) else "unhealthy"
264
+ )
265
+
266
+ return DeploymentResult(
267
+ success=True,
268
+ service_url=service_url,
269
+ service_id=full_service_name,
270
+ status=status,
271
+ health_status=health_status,
272
+ )
273
+
274
+ except subprocess.CalledProcessError:
275
+ return DeploymentResult(
276
+ success=False,
277
+ status="not_found",
278
+ error_message="App not found",
279
+ )
280
+
281
+ def verify_health(self, service_url: str, timeout: int = 60) -> bool:
282
+ """Verify service health by checking the health endpoint."""
283
+ return self.verify_health_enhanced(service_url, timeout=timeout)
284
+
285
+ def check_prerequisites(self) -> List[str]:
286
+ """Check prerequisites and return list of missing requirements."""
287
+ errors = []
288
+
289
+ # Check doctl CLI
290
+ try:
291
+ result = subprocess.run(
292
+ ["doctl", "version"], capture_output=True, text=True, check=True
293
+ )
294
+ log.debug(f"doctl version: {result.stdout}")
295
+ except (subprocess.CalledProcessError, FileNotFoundError):
296
+ errors.append("doctl CLI not found or not working")
297
+
298
+ # Check authentication
299
+ try:
300
+ result = subprocess.run(
301
+ ["doctl", "account", "get", "--output", "json"],
302
+ capture_output=True,
303
+ text=True,
304
+ check=True,
305
+ )
306
+ account_data = json.loads(result.stdout)
307
+ if not account_data.get("email"):
308
+ errors.append("doctl authentication not configured")
309
+ except (subprocess.CalledProcessError, json.JSONDecodeError):
310
+ errors.append("doctl authentication check failed")
311
+
312
+ return errors
313
+
314
+ def _ensure_registry(self, registry_name: str) -> None:
315
+ """Ensure container registry exists."""
316
+ try:
317
+ subprocess.run(
318
+ ["doctl", "registry", "get", registry_name],
319
+ capture_output=True,
320
+ check=True,
321
+ )
322
+ log.info(f"Container registry {registry_name} exists")
323
+ except subprocess.CalledProcessError:
324
+ # Create registry
325
+ subprocess.run(
326
+ ["doctl", "registry", "create", registry_name, "--region", self.region],
327
+ check=True,
328
+ )
329
+ log.info(f"Created container registry: {registry_name}")
330
+
331
+ def _create_app_spec(
332
+ self,
333
+ app_name: str,
334
+ registry_name: str,
335
+ image_tag: str,
336
+ port: int,
337
+ env_vars: Dict[str, str],
338
+ secrets: Dict[str, str],
339
+ ) -> Path:
340
+ """Create App Platform specification file."""
341
+ # Build environment variables
342
+ env_vars_list = []
343
+ for key, value in env_vars.items():
344
+ env_vars_list.append({"key": key, "value": value})
345
+
346
+ # Build secret references
347
+ secret_refs = []
348
+ for secret_name in secrets.keys():
349
+ secret_refs.append(
350
+ {
351
+ "key": secret_name,
352
+ "scope": "RUN_TIME",
353
+ "type": "SECRET",
354
+ }
355
+ )
356
+
357
+ # App spec
358
+ app_spec = {
359
+ "name": app_name,
360
+ "services": [
361
+ {
362
+ "name": "web",
363
+ "source_dir": "/",
364
+ "github": {
365
+ "repo": "supervaizer",
366
+ "branch": "main",
367
+ "deploy_on_push": False,
368
+ },
369
+ "dockerfile_path": "Dockerfile",
370
+ "http_port": port,
371
+ "instance_count": 1,
372
+ "instance_size_slug": "basic-xxs",
373
+ "routes": [{"path": "/"}],
374
+ "health_check": {
375
+ "http_path": "/.well-known/health",
376
+ "initial_delay_seconds": 10,
377
+ "period_seconds": 10,
378
+ "timeout_seconds": 5,
379
+ "success_threshold": 1,
380
+ "failure_threshold": 3,
381
+ },
382
+ "envs": env_vars_list + secret_refs,
383
+ }
384
+ ],
385
+ "region": self.region,
386
+ }
387
+
388
+ # Write spec to file
389
+ spec_path = Path(".deployment") / "do-app-spec.yaml"
390
+ spec_path.parent.mkdir(exist_ok=True)
391
+
392
+ import yaml # type: ignore[import-untyped]
393
+
394
+ with open(spec_path, "w") as f:
395
+ yaml.dump(app_spec, f, default_flow_style=False)
396
+
397
+ log.info(f"Created app spec at {spec_path}")
398
+ return spec_path
399
+
400
+ def _deploy_app(
401
+ self, app_name: str, spec_path: Path, timeout: int
402
+ ) -> Optional[str]:
403
+ """Deploy app using doctl."""
404
+ try:
405
+ # Check if app exists
406
+ try:
407
+ subprocess.run(
408
+ ["doctl", "apps", "get", app_name], capture_output=True, check=True
409
+ )
410
+ # App exists, update it
411
+ subprocess.run(
412
+ ["doctl", "apps", "update", app_name, "--spec", str(spec_path)],
413
+ capture_output=True,
414
+ text=True,
415
+ check=True,
416
+ )
417
+ log.info(f"Updated App Platform app: {app_name}")
418
+ except subprocess.CalledProcessError:
419
+ # App doesn't exist, create it
420
+ subprocess.run(
421
+ ["doctl", "apps", "create", "--spec", str(spec_path)],
422
+ capture_output=True,
423
+ text=True,
424
+ check=True,
425
+ )
426
+ log.info(f"Created App Platform app: {app_name}")
427
+
428
+ # Wait for deployment to complete
429
+ return self._wait_for_deployment(app_name, timeout)
430
+
431
+ except subprocess.CalledProcessError as e:
432
+ log.error(f"Failed to deploy app: {e}")
433
+ raise RuntimeError(f"App deployment failed: {e}") from e
434
+
435
+ def _wait_for_deployment(self, app_name: str, timeout: int) -> Optional[str]:
436
+ """Wait for deployment to complete and return URL."""
437
+ start_time = time.time()
438
+
439
+ while time.time() - start_time < timeout:
440
+ try:
441
+ result = subprocess.run(
442
+ ["doctl", "apps", "get", app_name, "--format", "json"],
443
+ capture_output=True,
444
+ text=True,
445
+ check=True,
446
+ )
447
+ app_data = json.loads(result.stdout)
448
+
449
+ # Check if deployment is active
450
+ if app_data.get("last_deployment_active_at"):
451
+ service_url = (
452
+ app_data.get("spec", {}).get("ingress", {}).get("default_route")
453
+ )
454
+ if service_url:
455
+ log.info(f"App deployed at: {service_url}")
456
+ return service_url
457
+
458
+ # Check for failed deployment
459
+ if app_data.get("last_deployment_created_at") and not app_data.get(
460
+ "last_deployment_active_at"
461
+ ):
462
+ raise RuntimeError("Deployment failed")
463
+
464
+ except Exception as e:
465
+ log.debug(f"Waiting for deployment: {e}")
466
+
467
+ time.sleep(10)
468
+
469
+ raise TimeoutError(f"Deployment did not complete within {timeout} seconds")
470
+
471
+ def _set_public_url(self, app_name: str, public_url: str, spec_path: Path) -> None:
472
+ """Set SUPERVAIZER_PUBLIC_URL environment variable."""
473
+ try:
474
+ # Read current spec
475
+ import yaml
476
+
477
+ with open(spec_path, "r") as f:
478
+ spec = yaml.safe_load(f)
479
+
480
+ # Update environment variables
481
+ envs = spec["services"][0].get("envs", [])
482
+
483
+ # Remove existing SUPERVAIZER_PUBLIC_URL
484
+ envs = [env for env in envs if env.get("key") != "SUPERVAIZER_PUBLIC_URL"]
485
+
486
+ # Add the public URL
487
+ envs.append({"key": "SUPERVAIZER_PUBLIC_URL", "value": public_url})
488
+
489
+ spec["services"][0]["envs"] = envs
490
+
491
+ # Write updated spec
492
+ with open(spec_path, "w") as f:
493
+ yaml.dump(spec, f, default_flow_style=False)
494
+
495
+ # Update app
496
+ subprocess.run(
497
+ ["doctl", "apps", "update", app_name, "--spec", str(spec_path)],
498
+ check=True,
499
+ )
500
+
501
+ log.info(f"Set SUPERVAIZER_PUBLIC_URL to {public_url}")
502
+
503
+ except Exception as e:
504
+ log.error(f"Failed to set SUPERVAIZER_PUBLIC_URL: {e}")