decomposer: generate files for Create a production-suitable Dockerfile and minimal Docker Compose configuration for packaging and running the completed FastAPI endpoint monitoring service.

This commit is contained in:
2026-08-09 15:10:46 +00:00
parent aec31dd90e
commit 789d63ad9f

View File

@@ -0,0 +1,75 @@
from __future__ import annotations
import hashlib
import json
from pathlib import Path
from typing import Any
from .render import PIN_RE, compose, dockerfile, dockerignore, validate_module
ARTIFACT_NAMES = ("Dockerfile", "compose.yaml", ".dockerignore")
def _configuration(spec: dict[str, Any]) -> tuple[str, int, str | None]:
cfg = spec.get("container", {})
module = cfg.get("startup_module", spec.get("startup_module", "app.main:app"))
port = cfg.get("port", spec.get("port", 8000))
health = cfg.get("health_endpoint", spec.get("health_endpoint"))
validate_module(module)
if not isinstance(port, int) or isinstance(port, bool) or not 1 <= port <= 65535:
raise ValueError("port must be an integer from 1 through 65535")
if health is not None and (not isinstance(health, str) or not health.startswith("/")):
raise ValueError("health_endpoint must be an absolute path")
return module, port, health
def _validate_existing_service(root: Path, module: str, health: str | None) -> None:
mod, _ = validate_module(module)
source = root.joinpath(*mod.split(".")).with_suffix(".py")
if not source.is_file():
raise ValueError(f"startup module does not exist: {source.relative_to(root)}")
lock = root / "requirements.lock"
if not lock.is_file():
raise ValueError("existing service must provide requirements.lock")
entries = [line.strip() for line in lock.read_text().splitlines()
if line.strip() and not line.lstrip().startswith("#")]
invalid = [entry for entry in entries if not PIN_RE.fullmatch(entry)]
if not entries or invalid:
raise ValueError("requirements.lock must contain only exact name==version pins")
if health:
texts = "\n".join(p.read_text(errors="ignore") for p in root.rglob("*.py"))
if f'"{health}"' not in texts and f"'{health}'" not in texts:
raise ValueError("configured health_endpoint was not found in Python routes")
def containerize(payload: dict[str, Any], service_dir: str | Path) -> dict[str, Any]:
required = ("service_specification", "target_management_implementation",
"endpoint_check_implementation", "current_status_implementation")
missing = [key for key in required if key not in payload]
if missing:
raise ValueError(f"missing contract keys: {', '.join(missing)}")
root = Path(service_dir).resolve()
module, port, health = _configuration(payload["service_specification"])
_validate_existing_service(root, module, health)
rendered = {
"Dockerfile": dockerfile(module, port),
"compose.yaml": compose(port, health),
".dockerignore": dockerignore(),
}
for name, content in rendered.items():
(root / name).write_text(content)
result = {key: payload[key] for key in required}
result["container_artifacts"] = {
"service_directory": str(root),
"startup": f"python -m uvicorn {module} --host 0.0.0.0 --port {port} --proxy-headers",
"dependency_lock": "requirements.lock (exact pins; installed with --no-deps)",
"non_root_user": "10001:10001",
"health_endpoint": health,
"files": {name: {"sha256": hashlib.sha256(content.encode()).hexdigest(),
"content": content} for name, content in rendered.items()},
}
return result
def dumps(result: dict[str, Any]) -> str:
return json.dumps(result, indent=2, sort_keys=True) + "\n"