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,460 @@
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
+ Local Testing Command
9
+
10
+ This module provides local testing functionality using Docker Compose.
11
+ """
12
+
13
+ import os
14
+ import subprocess
15
+ import time
16
+ from pathlib import Path
17
+ from typing import Optional
18
+
19
+ import httpx
20
+ from rich.console import Console
21
+ from rich.panel import Panel
22
+ from rich.progress import Progress, SpinnerColumn, TextColumn
23
+ from rich.table import Table
24
+
25
+ from supervaizer.deploy.docker import DockerManager
26
+
27
+ console = Console()
28
+
29
+
30
+ def local_docker(
31
+ name: str,
32
+ env: str,
33
+ port: int,
34
+ generate_api_key: bool,
35
+ generate_rsa: bool,
36
+ timeout: int,
37
+ verbose: bool,
38
+ docker_files_only: bool = False,
39
+ source_dir: str = ".",
40
+ controller_file: str = "supervaizer_control.py",
41
+ ) -> None:
42
+ """Test deployment locally using Docker Compose."""
43
+ if docker_files_only:
44
+ console.print(
45
+ Panel.fit("[bold blue]Generate Docker Files Only[/]", border_style="blue")
46
+ )
47
+ else:
48
+ console.print(
49
+ Panel.fit("[bold blue]Local Docker Testing[/]", border_style="blue")
50
+ )
51
+
52
+ # Determine service name
53
+ if name is None:
54
+ name = Path(source_dir).name.lower().replace("_", "-")
55
+
56
+ service_name = f"{name}-{env}"
57
+
58
+ console.print(f"[bold]Testing service:[/] {service_name}")
59
+ console.print(f"[bold]Environment:[/] {env}")
60
+ console.print(f"[bold]Port:[/] {port}")
61
+
62
+ try:
63
+ # Step 1: Check Docker availability
64
+ console.print("\n[bold]Step 1:[/] Checking Docker availability...")
65
+ if not _check_docker_available():
66
+ console.print("[bold red]Error:[/] Docker is not available or not running")
67
+ raise RuntimeError("Docker not available")
68
+ console.print("[green]✓[/] Docker is available")
69
+
70
+ # Step 2: Generate secrets if needed
71
+ console.print("\n[bold]Step 2:[/] Setting up secrets...")
72
+ secrets = _generate_test_secrets(generate_api_key, generate_rsa)
73
+ console.print("[green]✓[/] Test secrets configured")
74
+
75
+ # Step 3: Generate deployment files
76
+ console.print("\n[bold]Step 3:[/] Generating deployment files...")
77
+ docker_manager = DockerManager(require_docker=False)
78
+ docker_manager.generate_dockerfile(
79
+ controller_file=controller_file,
80
+ app_port=port,
81
+ )
82
+ docker_manager.generate_dockerignore()
83
+ docker_manager.generate_docker_compose(
84
+ port=port,
85
+ service_name=service_name,
86
+ environment=env,
87
+ api_key=secrets.get("api_key", "test-api-key"),
88
+ rsa_key=secrets.get("rsa_private_key", "test-rsa-key"),
89
+ )
90
+ console.print("[green]✓[/] Deployment files generated")
91
+
92
+ # If docker_files_only is True, stop here
93
+ if docker_files_only:
94
+ console.print("\n[bold green]✓ Docker files generated successfully![/]")
95
+ console.print("[bold]Generated files:[/]")
96
+ console.print(" • .deployment/Dockerfile")
97
+ console.print(" • .deployment/.dockerignore")
98
+ console.print(" • .deployment/docker-compose.yml")
99
+ console.print("\n[bold]To start the services:[/]")
100
+ console.print("[dim]docker-compose -f .deployment/docker-compose.yml up[/]")
101
+ console.print("\n[bold]To debug environment variables:[/]")
102
+ console.print(
103
+ f"[dim]docker-compose -f .deployment/docker-compose.yml run --rm {service_name} python debug_env.py[/]"
104
+ )
105
+ console.print(
106
+ "\n[bold]Note:[/] Environment variables are automatically included from your host environment."
107
+ )
108
+ console.print("Make sure to set the following variables if needed:")
109
+ console.print(" • SUPERVAIZE_API_KEY")
110
+ console.print(" • SUPERVAIZE_WORKSPACE_ID")
111
+ console.print(" • SUPERVAIZE_API_URL")
112
+ console.print(" • SUPERVAIZER_PUBLIC_URL")
113
+ return
114
+
115
+ # Step 4: Build Docker image
116
+ console.print("\n[bold]Step 4:[/] Building Docker image...")
117
+ image_tag = f"{service_name}:local-test"
118
+ # Create a new DockerManager instance that requires Docker for building
119
+ build_docker_manager = DockerManager(require_docker=True)
120
+
121
+ # Get build arguments for environment variables
122
+ from supervaizer.deploy.docker import get_docker_build_args
123
+
124
+ build_args = get_docker_build_args(port)
125
+
126
+ build_docker_manager.build_image(
127
+ image_tag, verbose=verbose, build_args=build_args
128
+ )
129
+ console.print(f"[green]✓[/] Image built: {image_tag}")
130
+
131
+ # Step 5: Start services with Docker Compose
132
+ console.print("\n[bold]Step 5:[/] Starting services...")
133
+ _start_docker_compose(
134
+ service_name=service_name, port=port, secrets=secrets, verbose=verbose
135
+ )
136
+ console.print("[green]✓[/] Services started")
137
+
138
+ # Step 6: Wait for service to be ready
139
+ console.print("\n[bold]Step 6:[/] Waiting for service to be ready...")
140
+ service_url = f"http://localhost:{port}"
141
+ if _wait_for_service(service_url, timeout):
142
+ console.print("[green]✓[/] Service is ready")
143
+ else:
144
+ console.print("[bold red]Error:[/] Service failed to start within timeout")
145
+ _show_service_logs(service_name)
146
+ raise RuntimeError("Service startup timeout")
147
+
148
+ # Step 7: Run health checks
149
+ console.print("\n[bold]Step 7:[/] Running health checks...")
150
+ health_results = _run_health_checks(service_url, secrets.get("api_key"))
151
+ _display_health_results(health_results)
152
+
153
+ # Step 8: Display service information
154
+ console.print("\n[bold]Step 8:[/] Service Information")
155
+ _display_service_info(service_name, service_url, port, secrets)
156
+
157
+ console.print("\n[bold green]✓ Local testing completed successfully![/]")
158
+ console.print(f"[bold]Service URL:[/] {service_url}")
159
+ console.print(f"[bold]API Documentation:[/] {service_url}/docs")
160
+ console.print(f"[bold]ReDoc:[/] {service_url}/redoc")
161
+
162
+ except Exception as e:
163
+ console.print(f"\n[bold red]Error during local testing:[/] {e}")
164
+ _cleanup_test_resources(service_name)
165
+ raise
166
+ finally:
167
+ # Always show cleanup instructions
168
+ console.print("\n[bold]To stop the test services:[/]")
169
+ console.print("[dim]docker-compose -f .deployment/docker-compose.yml down[/]")
170
+ console.print("\n[bold]To debug environment variables:[/]")
171
+ console.print(
172
+ f"[dim]docker-compose -f .deployment/docker-compose.yml run --rm {service_name} python debug_env.py[/]"
173
+ )
174
+ console.print("\n[bold]To clean up all deployment files:[/]")
175
+ console.print("[dim]supervaizer deploy clean[/]")
176
+
177
+
178
+ def _check_docker_available() -> bool:
179
+ """Check if Docker is available and running."""
180
+ try:
181
+ result = subprocess.run(
182
+ ["docker", "version"], capture_output=True, text=True, timeout=10
183
+ )
184
+ return result.returncode == 0
185
+ except (subprocess.TimeoutExpired, FileNotFoundError):
186
+ return False
187
+
188
+
189
+ def _generate_test_secrets(generate_api_key: bool, generate_rsa: bool) -> dict:
190
+ """Generate test secrets for local testing."""
191
+ secrets = {}
192
+
193
+ if generate_api_key:
194
+ # Generate a test API key
195
+ import secrets as secrets_module
196
+
197
+ secrets["api_key"] = secrets_module.token_urlsafe(32)
198
+ else:
199
+ secrets["api_key"] = "test-api-key-local"
200
+
201
+ if generate_rsa:
202
+ # Generate a test RSA key
203
+ from cryptography.hazmat.primitives import serialization
204
+ from cryptography.hazmat.primitives.asymmetric import rsa
205
+
206
+ private_key = rsa.generate_private_key(
207
+ public_exponent=65537,
208
+ key_size=2048,
209
+ )
210
+
211
+ private_pem = private_key.private_bytes(
212
+ encoding=serialization.Encoding.PEM,
213
+ format=serialization.PrivateFormat.PKCS8,
214
+ encryption_algorithm=serialization.NoEncryption(),
215
+ )
216
+ secrets["rsa_private_key"] = private_pem.decode()
217
+ else:
218
+ secrets["rsa_private_key"] = "test-rsa-key-local"
219
+
220
+ return secrets
221
+
222
+
223
+ def _start_docker_compose(
224
+ service_name: str, port: int, secrets: dict, verbose: bool = False
225
+ ) -> None:
226
+ """Start services using Docker Compose."""
227
+ compose_file = Path(".deployment/docker-compose.yml")
228
+
229
+ if not compose_file.exists():
230
+ raise RuntimeError("Docker Compose file not found")
231
+
232
+ # Set environment variables for Docker Compose
233
+ env = os.environ.copy()
234
+ env.update(
235
+ {
236
+ "SERVICE_NAME": service_name,
237
+ "SERVICE_PORT": str(port),
238
+ "SUPERVAIZER_API_KEY": secrets["api_key"],
239
+ "SV_RSA_PRIVATE_KEY": secrets["rsa_private_key"],
240
+ "SUPERVAIZER_ENVIRONMENT": "dev",
241
+ "SUPERVAIZER_HOST": "0.0.0.0",
242
+ "SUPERVAIZER_PORT": str(port),
243
+ "SV_LOG_LEVEL": "INFO",
244
+ }
245
+ )
246
+
247
+ cmd = ["docker-compose", "-f", str(compose_file), "up", "-d"]
248
+
249
+ if verbose:
250
+ # When verbose, capture output to display it
251
+ result = subprocess.run(cmd, env=env, capture_output=True, text=True)
252
+ # Display the captured output
253
+ if result.stdout:
254
+ console.print(result.stdout)
255
+ if result.stderr:
256
+ console.print(f"[yellow]Stderr:[/] {result.stderr}")
257
+ else:
258
+ # When not verbose, capture output silently
259
+ result = subprocess.run(cmd, env=env, capture_output=True, text=True)
260
+
261
+ if result.returncode != 0:
262
+ error_msg = result.stderr if result.stderr else "Unknown error"
263
+ raise RuntimeError(f"Failed to start Docker Compose: {error_msg}")
264
+
265
+
266
+ def _wait_for_service(url: str, timeout: int) -> bool:
267
+ """Wait for service to be ready."""
268
+ start_time = time.time()
269
+
270
+ with Progress(
271
+ SpinnerColumn(),
272
+ TextColumn("[progress.description]{task.description}"),
273
+ console=console,
274
+ ) as progress:
275
+ progress.add_task("Waiting for service...", total=None)
276
+
277
+ while time.time() - start_time < timeout:
278
+ try:
279
+ response = httpx.get(f"{url}/.well-known/health", timeout=5)
280
+ if response.status_code == 200:
281
+ return True
282
+ except httpx.RequestError:
283
+ pass
284
+
285
+ time.sleep(2)
286
+
287
+ return False
288
+
289
+
290
+ def _run_health_checks(url: str, api_key: Optional[str]) -> dict:
291
+ """Run comprehensive health checks."""
292
+ results = {}
293
+
294
+ # Basic health check
295
+ try:
296
+ response = httpx.get(f"{url}/.well-known/health", timeout=10)
297
+ results["health_endpoint"] = {
298
+ "status": response.status_code,
299
+ "success": response.status_code == 200,
300
+ "response_time": response.elapsed.total_seconds(),
301
+ }
302
+ except Exception as e:
303
+ results["health_endpoint"] = {
304
+ "status": None,
305
+ "success": False,
306
+ "error": str(e),
307
+ }
308
+
309
+ # API health check (if API key available)
310
+ if api_key:
311
+ try:
312
+ headers = {"X-API-Key": api_key}
313
+ response = httpx.get(f"{url}/agents/health", headers=headers, timeout=10)
314
+ results["api_health_endpoint"] = {
315
+ "status": response.status_code,
316
+ "success": response.status_code == 200,
317
+ "response_time": response.elapsed.total_seconds(),
318
+ }
319
+ except Exception as e:
320
+ results["api_health_endpoint"] = {
321
+ "status": None,
322
+ "success": False,
323
+ "error": str(e),
324
+ }
325
+
326
+ # API documentation check
327
+ try:
328
+ response = httpx.get(f"{url}/docs", timeout=10)
329
+ results["api_docs"] = {
330
+ "status": response.status_code,
331
+ "success": response.status_code == 200,
332
+ }
333
+ except Exception as e:
334
+ results["api_docs"] = {
335
+ "status": None,
336
+ "success": False,
337
+ "error": str(e),
338
+ }
339
+
340
+ return results
341
+
342
+
343
+ def _display_health_results(results: dict) -> None:
344
+ """Display health check results in a table."""
345
+ table = Table(title="Health Check Results")
346
+ table.add_column("Endpoint", style="cyan")
347
+ table.add_column("Status", style="green")
348
+ table.add_column("Response Time", style="yellow")
349
+ table.add_column("Details", style="white")
350
+
351
+ for endpoint, result in results.items():
352
+ if result["success"]:
353
+ status = f"[green]{result['status']}[/]"
354
+ response_time = f"{result.get('response_time', 0):.3f}s"
355
+ details = "✓ OK"
356
+ else:
357
+ status = f"[red]{result.get('status', 'ERROR')}[/]"
358
+ response_time = "N/A"
359
+ details = result.get("error", "Failed")
360
+
361
+ table.add_row(
362
+ endpoint.replace("_", " ").title(), status, response_time, details
363
+ )
364
+
365
+ console.print(table)
366
+
367
+
368
+ def _display_service_info(
369
+ service_name: str, url: str, port: int, secrets: dict
370
+ ) -> None:
371
+ """Display service information."""
372
+ info_table = Table(title="Service Information")
373
+ info_table.add_column("Property", style="cyan")
374
+ info_table.add_column("Value", style="white")
375
+
376
+ info_table.add_row("Service Name", service_name)
377
+ info_table.add_row("URL", url)
378
+ info_table.add_row("Port", str(port))
379
+ info_table.add_row(
380
+ "API Key",
381
+ secrets["api_key"][:8] + "..."
382
+ if len(secrets["api_key"]) > 8
383
+ else secrets["api_key"],
384
+ )
385
+ info_table.add_row("Environment", "dev")
386
+
387
+ console.print(info_table)
388
+
389
+
390
+ def _show_service_logs(service_name: str) -> None:
391
+ """Show service logs for debugging."""
392
+ console.print("\n[bold]Service Logs:[/]")
393
+ try:
394
+ # Try docker-compose logs first
395
+ result = subprocess.run(
396
+ [
397
+ "docker-compose",
398
+ "-f",
399
+ ".deployment/docker-compose.yml",
400
+ "logs",
401
+ "--tail=100",
402
+ service_name,
403
+ ],
404
+ capture_output=True,
405
+ text=True,
406
+ )
407
+ if result.stdout:
408
+ console.print(result.stdout)
409
+ if result.stderr:
410
+ console.print(f"[yellow]Stderr:[/] {result.stderr}")
411
+
412
+ # Also try direct docker logs as fallback
413
+ if not result.stdout:
414
+ console.print("\n[bold]Trying direct docker logs:[/]")
415
+ docker_logs = subprocess.run(
416
+ [
417
+ "docker",
418
+ "logs",
419
+ f"deployment-{service_name}-1",
420
+ "--tail=100",
421
+ ],
422
+ capture_output=True,
423
+ text=True,
424
+ )
425
+ if docker_logs.stdout:
426
+ console.print(docker_logs.stdout)
427
+ if docker_logs.stderr:
428
+ console.print(f"[yellow]Stderr:[/] {docker_logs.stderr}")
429
+
430
+ # Also try to get container status
431
+ console.print("\n[bold]Container Status:[/]")
432
+ status_result = subprocess.run(
433
+ [
434
+ "docker-compose",
435
+ "-f",
436
+ ".deployment/docker-compose.yml",
437
+ "ps",
438
+ "-a",
439
+ ],
440
+ capture_output=True,
441
+ text=True,
442
+ )
443
+ if status_result.stdout:
444
+ console.print(status_result.stdout)
445
+ except Exception as e:
446
+ console.print(f"[red]Failed to get logs:[/] {e}")
447
+
448
+
449
+ def _cleanup_test_resources(service_name: str) -> None:
450
+ """Clean up test resources."""
451
+ console.print("\n[bold]Cleaning up test resources...[/]")
452
+ try:
453
+ subprocess.run(
454
+ ["docker-compose", "-f", ".deployment/docker-compose.yml", "down"],
455
+ capture_output=True,
456
+ text=True,
457
+ )
458
+ console.print("[green]✓[/] Test resources cleaned up")
459
+ except Exception as e:
460
+ console.print(f"[yellow]Warning:[/] Failed to cleanup resources: {e}")
@@ -0,0 +1,167 @@
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
+ Plan Command
9
+
10
+ Shows what changes will be made during deployment.
11
+ """
12
+
13
+ from pathlib import Path
14
+ from typing import Optional
15
+
16
+ from rich.console import Console
17
+ from rich.table import Table
18
+
19
+ from supervaizer.common import log
20
+ from supervaizer.deploy.driver_factory import create_driver, get_supported_platforms
21
+ from supervaizer.deploy.utils import get_git_sha
22
+ from supervaizer.deploy.drivers.base import DeploymentPlan
23
+
24
+ console = Console()
25
+
26
+
27
+ def plan_deployment(
28
+ platform: str,
29
+ name: Optional[str] = None,
30
+ env: str = "dev",
31
+ region: Optional[str] = None,
32
+ project_id: Optional[str] = None,
33
+ verbose: bool = False,
34
+ source_dir: Optional[Path] = None,
35
+ ) -> None:
36
+ """Plan deployment changes without applying them."""
37
+ # Validate platform
38
+ if platform not in get_supported_platforms():
39
+ console.print(f"[bold red]Error:[/] Unsupported platform: {platform}")
40
+ console.print(f"Supported platforms: {', '.join(get_supported_platforms())}")
41
+ return
42
+
43
+ # Set defaults
44
+ if not name:
45
+ name = (source_dir or Path.cwd()).name
46
+ if not region:
47
+ region = _get_default_region(platform)
48
+
49
+ console.print(f"[bold blue]Planning deployment to {platform}[/bold blue]")
50
+ console.print(f"Service name: {name}")
51
+ console.print(f"Environment: {env}")
52
+ console.print(f"Region: {region}")
53
+ if project_id:
54
+ console.print(f"Project ID: {project_id}")
55
+
56
+ try:
57
+ # Create driver
58
+ driver = create_driver(platform, region, project_id)
59
+
60
+ # Check prerequisites
61
+ prerequisites = driver.check_prerequisites()
62
+ if prerequisites:
63
+ console.print("[bold red]Prerequisites not met:[/]")
64
+ for prereq in prerequisites:
65
+ console.print(f" • {prereq}")
66
+ return
67
+
68
+ # Generate image tag
69
+ image_tag = _generate_image_tag(name, env)
70
+
71
+ # Create deployment plan
72
+ plan = driver.plan_deployment(
73
+ service_name=name,
74
+ environment=env,
75
+ image_tag=image_tag,
76
+ port=8000,
77
+ env_vars=_get_default_env_vars(env),
78
+ secrets=_get_default_secrets(name, env),
79
+ )
80
+
81
+ # Display plan
82
+ _display_plan(plan)
83
+
84
+ except Exception as e:
85
+ log.error(f"Planning failed: {e}")
86
+ console.print(f"[bold red]Planning failed:[/] {e}")
87
+
88
+
89
+ def _get_default_region(platform: str) -> str:
90
+ """Get default region for platform."""
91
+ defaults = {
92
+ "cloud-run": "us-central1",
93
+ "aws-app-runner": "us-east-1",
94
+ "do-app-platform": "nyc3",
95
+ }
96
+ return defaults.get(platform, "us-central1")
97
+
98
+
99
+ def _generate_image_tag(service_name: str, environment: str) -> str:
100
+ """Generate image tag for deployment."""
101
+ git_sha = get_git_sha()
102
+ return f"{service_name}-{environment}:{git_sha}"
103
+
104
+
105
+ def _get_default_env_vars(environment: str) -> dict[str, str]:
106
+ """Get default environment variables."""
107
+ return {
108
+ "SUPERVAIZER_ENVIRONMENT": environment,
109
+ "SUPERVAIZER_HOST": "0.0.0.0",
110
+ "SUPERVAIZER_PORT": "8000",
111
+ "SV_LOG_LEVEL": "INFO",
112
+ }
113
+
114
+
115
+ def _get_default_secrets(service_name: str, environment: str) -> dict[str, str]:
116
+ """Get default secrets for deployment."""
117
+ return {
118
+ f"{service_name}-{environment}-api-key": "placeholder-api-key",
119
+ f"{service_name}-{environment}-rsa-key": "placeholder-rsa-key",
120
+ }
121
+
122
+
123
+ def _display_plan(plan: DeploymentPlan) -> None:
124
+ """Display deployment plan."""
125
+ console.print(
126
+ f"\n[bold]Deployment Plan for {plan.service_name}-{plan.environment}[/bold]"
127
+ )
128
+ console.print(f"Platform: {plan.platform}")
129
+ console.print(f"Region: {plan.region}")
130
+ console.print(f"Target Image: {plan.target_image}")
131
+
132
+ if plan.current_image:
133
+ console.print(f"Current Image: {plan.current_image}")
134
+ if plan.current_url:
135
+ console.print(f"Current URL: {plan.current_url}")
136
+
137
+ # Display actions
138
+ if plan.actions:
139
+ table = Table(title="Actions")
140
+ table.add_column("Type", style="cyan")
141
+ table.add_column("Resource", style="magenta")
142
+ table.add_column("Action", style="green")
143
+ table.add_column("Description", style="white")
144
+
145
+ for action in plan.actions:
146
+ table.add_row(
147
+ action.resource_type.value,
148
+ action.resource_name,
149
+ action.action_type.value,
150
+ action.description,
151
+ )
152
+
153
+ console.print(table)
154
+ else:
155
+ console.print("[yellow]No actions required[/yellow]")
156
+
157
+ # Display environment variables
158
+ if plan.target_env_vars:
159
+ console.print("\n[bold]Environment Variables:[/bold]")
160
+ for key, value in plan.target_env_vars.items():
161
+ console.print(f" {key}={value}")
162
+
163
+ # Display secrets
164
+ if plan.target_secrets:
165
+ console.print("\n[bold]Secrets:[/bold]")
166
+ for key in plan.target_secrets.keys():
167
+ console.print(f" {key}=***")