from collections.abc import Mapping from dataclasses import dataclass, field from datetime import UTC, datetime, timedelta from pathlib import Path import docker from docker import DockerClient from adapter.config.loader import load_config from adapter.config.model import AppConfig from adapter.docker.runtime import DockerSandboxRuntime from adapter.observability.factory import build_observability from adapter.observability.runtime import ObservabilityRuntime from adapter.sandbox.reconciliation import SandboxSessionReconciler from repository.sandbox_lock import ProcessLocalSandboxLifecycleLocker from repository.sandbox_session import InMemorySandboxSessionRepository from usecase.interface import Clock from usecase.sandbox import CleanupExpiredSandboxes, CreateSandbox, DeleteSandbox @dataclass(frozen=True, slots=True) class AppRepositories: sandbox_session: InMemorySandboxSessionRepository @dataclass(frozen=True, slots=True) class AppUsecases: create_sandbox: CreateSandbox cleanup_expired_sandboxes: CleanupExpiredSandboxes delete_sandbox: DeleteSandbox @dataclass(slots=True) class AppContainer: config: AppConfig observability: ObservabilityRuntime repositories: AppRepositories usecases: AppUsecases sandbox_reconciler: SandboxSessionReconciler = field(repr=False) _docker_client: DockerClient = field(repr=False) _is_shutdown: bool = field(default=False, init=False, repr=False) def shutdown(self) -> None: if self._is_shutdown: return self._is_shutdown = True errors: list[Exception] = [] for action in (self._docker_client.close, self.observability.shutdown): try: action() except Exception as exc: errors.append(exc) if errors: raise ExceptionGroup('shutdown failed', errors) class SystemClock(Clock): def now(self) -> datetime: return datetime.now(tz=UTC) def build_container( config_path: Path | str | None = None, env_path: Path | str | None = None, environ: Mapping[str, str] | None = None, config: AppConfig | None = None, ) -> AppContainer: app_config = config if app_config is None: app_config = load_config( config_path=config_path, env_path=env_path, environ=environ, ) observability = build_observability(app_config) clock = SystemClock() docker_client = docker.DockerClient(base_url=app_config.docker.base_url) sandbox_repository = InMemorySandboxSessionRepository() sandbox_locker = ProcessLocalSandboxLifecycleLocker() sandbox_runtime = DockerSandboxRuntime( app_config.sandbox, docker_client, observability.metrics, observability.tracer, ) sandbox_reconciler = SandboxSessionReconciler( state_source=sandbox_runtime, registry=sandbox_repository, logger=observability.logger, metrics=observability.metrics, tracer=observability.tracer, ) repositories = AppRepositories(sandbox_session=sandbox_repository) usecases = AppUsecases( create_sandbox=CreateSandbox( repository=sandbox_repository, locker=sandbox_locker, runtime=sandbox_runtime, clock=clock, logger=observability.logger, metrics=observability.metrics, tracer=observability.tracer, ttl=timedelta(seconds=app_config.sandbox.ttl_seconds), ), cleanup_expired_sandboxes=CleanupExpiredSandboxes( repository=sandbox_repository, locker=sandbox_locker, runtime=sandbox_runtime, clock=clock, logger=observability.logger, metrics=observability.metrics, tracer=observability.tracer, ), delete_sandbox=DeleteSandbox( repository=sandbox_repository, locker=sandbox_locker, runtime=sandbox_runtime, logger=observability.logger, metrics=observability.metrics, tracer=observability.tracer, ), ) return AppContainer( config=app_config, observability=observability, repositories=repositories, usecases=usecases, sandbox_reconciler=sandbox_reconciler, _docker_client=docker_client, )