Loading Dockerfile +17 −36 Changes for Dockerfile: 17 added lines, 36 removed lines. Original line number Diff line number Diff line FROM ghcr.io/astral-sh/uv:0.12.19 AS uv FROM python:3.12-slim AS builder COPY --from=uv /uv /bin/uv WORKDIR /app # DB build arg: base | postgres | mongo ARG DB=base # Install exactly what uv.lock pins (same as CI), so a new upstream release # can't change the image without a lockfile change. uv verifies every package # against the hashes in uv.lock, so the trusted hosts only relax TLS checks for # networks that intercept HTTPS, not package integrity. ENV UV_COMPILE_BYTECODE=1 \ UV_LINK_MODE=copy \ UV_PYTHON_DOWNLOADS=never \ UV_HTTP_TIMEOUT=120 \ UV_INSECURE_HOST="pypi.org files.pythonhosted.org" ENV PIP_DEFAULT_TIMEOUT=120 # Dependencies first, so this layer is cached until uv.lock changes. COPY pyproject.toml uv.lock ./ RUN uv sync --locked --no-install-project COPY pyproject.toml ./ COPY README.md ./ COPY src/ ./src/ RUN python -m venv .venv && \ .venv/bin/pip install \ --no-cache-dir \ --default-timeout=120 \ --trusted-host pypi.org \ --trusted-host files.pythonhosted.org \ --upgrade pip setuptools wheel && \ if [ "$DB" = "postgres" ]; then \ .venv/bin/pip install \ --no-cache-dir \ --default-timeout=120 \ --trusted-host pypi.org \ --trusted-host files.pythonhosted.org \ ".[postgres]"; \ elif [ "$DB" = "mongo" ]; then \ .venv/bin/pip install \ --no-cache-dir \ --default-timeout=120 \ --trusted-host pypi.org \ --trusted-host files.pythonhosted.org \ ".[mongo]"; \ else \ .venv/bin/pip install \ --no-cache-dir \ --default-timeout=120 \ --trusted-host pypi.org \ --trusted-host files.pythonhosted.org \ "."; \ fi RUN uv sync --locked --no-editable FROM python:3.12-slim AS runtime WORKDIR /app COPY --from=builder /app/.venv /app/.venv COPY src/ ./src/ ENV PATH="/app/.venv/bin:$PATH" ENV PYTHONPATH="/app" ENV PYTHONUNBUFFERED=1 ENV PYTHONDONTWRITEBYTECODE=1 Loading README.md +9 −12 Changes for README.md: 9 added lines, 12 removed lines. Original line number Diff line number Diff line Loading @@ -85,25 +85,22 @@ via the `conformance` marker. #### Vendored spec pins (update this list when bumping a pin) **Edge Application Management** — [camaraproject/EdgeApplicationManagement](https://github.com/camaraproject/EdgeApplicationManagement) at commit `0a64e153e8b860f9a8b7c045fe2e840e7278dada` (API version `wip`, served under `{apiRoot}/edge-application-management/vwip`), vendored in `src/open_exposure_gateway/api/camara/edge_application_management/vwip/`: API version `0.1.0-alpha.1` (served under `{apiRoot}/edge-application-management/v0.1alpha1`), vendored in `src/open_exposure_gateway/api/camara/edge_application_management/v0_1_0_alpha_1/`: | Vendored file | Upstream file | |---|---| | `API_definitions/edge-application-management.yaml` | `code/API_definitions/edge-application-management.yaml` | | `common/CAMARA_common.yaml` | `code/common/CAMARA_common.yaml` | | `API_definitions/edge-application-management-0.1.0-alpha.1.yaml` | `code/API_definitions/edge-application-management.yaml` | To refresh or bump the pin, fetch **both files from the same commit** (a mismatched pair breaks the spec's relative `$ref`s or silently changes its meaning), from the pin folder: The `0.1.0-alpha.1` bundle is self-contained (no external `$ref`s), so it is a single vendored file — the previous `common/` pair is no longer needed. To refresh or bump the pin, fetch the release's bundled spec from the pin folder: ```sh SHA=0a64e153e8b860f9a8b7c045fe2e840e7278dada curl -sL -o API_definitions/edge-application-management.yaml \ SHA=<upstream commit for the target release> curl -sL -o API_definitions/edge-application-management-0.1.0-alpha.1.yaml \ "https://raw.githubusercontent.com/camaraproject/EdgeApplicationManagement/$SHA/code/API_definitions/edge-application-management.yaml" curl -sL -o common/CAMARA_common.yaml \ "https://raw.githubusercontent.com/camaraproject/EdgeApplicationManagement/$SHA/code/common/CAMARA_common.yaml" ``` ### Running with Docker Loading src/open_exposure_gateway/adapters/database/mappers.py +30 −0 Changes for src/open_exposure_gateway/adapters/database/mappers.py: 30 added lines, 0 removed lines. Original line number Diff line number Diff line from uuid import UUID from open_exposure_gateway.adapters.database.sql import ( AppDeploymentRow, AppInstanceRow, AppRegistrationRow, CallbackDeliveryRow, Loading @@ -7,6 +10,7 @@ from open_exposure_gateway.adapters.database.sql import ( QodSessionRow, ) from open_exposure_gateway.domain.models import ( AppDeployment, AppInstance, AppRegistration, CallbackDelivery, Loading Loading @@ -110,6 +114,32 @@ class AppInstanceMapper: ) class AppDeploymentMapper: @staticmethod def to_domain(row: AppDeploymentRow) -> AppDeployment: return AppDeployment( app_deployment_id=row.app_deployment_id, operation_id=row.operation_id, app_registration_id=row.app_registration_id, app_deployment_name=row.app_deployment_name, edge_cloud_zones=[UUID(zone_id) for zone_id in row.edge_cloud_zones], state=row.state, created_at=row.created_at, updated_at=row.updated_at, ) @staticmethod def to_row(domain: AppDeployment) -> AppDeploymentRow: return AppDeploymentRow( app_deployment_id=domain.app_deployment_id, operation_id=domain.operation_id, app_registration_id=domain.app_registration_id, app_deployment_name=domain.app_deployment_name, edge_cloud_zones=[str(zone_id) for zone_id in domain.edge_cloud_zones], state=domain.state, ) class QodSessionMapper: @staticmethod def to_domain(row: QodSessionRow) -> QodSession: Loading src/open_exposure_gateway/adapters/database/repos/app_instances.py +10 −0 Changes for src/open_exposure_gateway/adapters/database/repos/app_instances.py: 10 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -25,6 +25,16 @@ class SqlAppInstanceRepository(AppInstanceRepository): row = await self._session.scalar(stmt) return AppInstanceMapper.to_domain(row) if row is not None else None async def list_by_operation_id(self, operation_id: UUID) -> list[AppInstance]: stmt = select(AppInstanceRow).where(AppInstanceRow.operation_id == operation_id) rows = await self._session.scalars(stmt) return [AppInstanceMapper.to_domain(row) for row in rows] async def list_by_app_deployment_id(self, app_deployment_id: UUID) -> list[AppInstance]: stmt = select(AppInstanceRow).where(AppInstanceRow.app_deployment_id == app_deployment_id) rows = await self._session.scalars(stmt) return [AppInstanceMapper.to_domain(row) for row in rows] async def exists_for_app_registration(self, app_registration_id: UUID) -> bool: stmt = select(AppInstanceRow.app_instance_id).where( AppInstanceRow.app_registration_id == app_registration_id, Loading src/open_exposure_gateway/adapters/database/repos/deployments.py 0 → 100644 +46 −0 Changes for src/open_exposure_gateway/adapters/database/repos/deployments.py: 46 added lines, 0 removed lines. Original line number Diff line number Diff line from uuid import UUID from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from open_exposure_gateway.adapters.database.mappers import AppDeploymentMapper from open_exposure_gateway.adapters.database.sql import AppDeploymentRow from open_exposure_gateway.domain.models import AppDeployment from open_exposure_gateway.ports.database.deployments import AppDeploymentRepository class SqlAppDeploymentRepository(AppDeploymentRepository): def __init__(self, session: AsyncSession) -> None: self._session = session async def get_by_id(self, app_deployment_id: UUID) -> AppDeployment | None: stmt = select(AppDeploymentRow).where( AppDeploymentRow.app_deployment_id == app_deployment_id ) row = await self._session.scalar(stmt) return AppDeploymentMapper.to_domain(row) if row is not None else None async def get_by_operation_id(self, operation_id: UUID) -> AppDeployment | None: stmt = select(AppDeploymentRow).where(AppDeploymentRow.operation_id == operation_id) row = await self._session.scalar(stmt) return AppDeploymentMapper.to_domain(row) if row is not None else None async def list_by_app_registration_id(self, app_registration_id: UUID) -> list[AppDeployment]: stmt = select(AppDeploymentRow).where( AppDeploymentRow.app_registration_id == app_registration_id ) rows = await self._session.scalars(stmt) return [AppDeploymentMapper.to_domain(row) for row in rows] async def list_all(self) -> list[AppDeployment]: stmt = select(AppDeploymentRow) rows = await self._session.scalars(stmt) return [AppDeploymentMapper.to_domain(row) for row in rows] async def save(self, app_deployment: AppDeployment) -> AppDeployment: merged = await self._session.merge(AppDeploymentMapper.to_row(app_deployment)) await self._session.flush() saved = await self.get_by_id(merged.app_deployment_id) if saved is None: raise RuntimeError("Saved app deployment could not be reloaded") return saved Loading
Dockerfile +17 −36 Changes for Dockerfile: 17 added lines, 36 removed lines. Original line number Diff line number Diff line FROM ghcr.io/astral-sh/uv:0.12.19 AS uv FROM python:3.12-slim AS builder COPY --from=uv /uv /bin/uv WORKDIR /app # DB build arg: base | postgres | mongo ARG DB=base # Install exactly what uv.lock pins (same as CI), so a new upstream release # can't change the image without a lockfile change. uv verifies every package # against the hashes in uv.lock, so the trusted hosts only relax TLS checks for # networks that intercept HTTPS, not package integrity. ENV UV_COMPILE_BYTECODE=1 \ UV_LINK_MODE=copy \ UV_PYTHON_DOWNLOADS=never \ UV_HTTP_TIMEOUT=120 \ UV_INSECURE_HOST="pypi.org files.pythonhosted.org" ENV PIP_DEFAULT_TIMEOUT=120 # Dependencies first, so this layer is cached until uv.lock changes. COPY pyproject.toml uv.lock ./ RUN uv sync --locked --no-install-project COPY pyproject.toml ./ COPY README.md ./ COPY src/ ./src/ RUN python -m venv .venv && \ .venv/bin/pip install \ --no-cache-dir \ --default-timeout=120 \ --trusted-host pypi.org \ --trusted-host files.pythonhosted.org \ --upgrade pip setuptools wheel && \ if [ "$DB" = "postgres" ]; then \ .venv/bin/pip install \ --no-cache-dir \ --default-timeout=120 \ --trusted-host pypi.org \ --trusted-host files.pythonhosted.org \ ".[postgres]"; \ elif [ "$DB" = "mongo" ]; then \ .venv/bin/pip install \ --no-cache-dir \ --default-timeout=120 \ --trusted-host pypi.org \ --trusted-host files.pythonhosted.org \ ".[mongo]"; \ else \ .venv/bin/pip install \ --no-cache-dir \ --default-timeout=120 \ --trusted-host pypi.org \ --trusted-host files.pythonhosted.org \ "."; \ fi RUN uv sync --locked --no-editable FROM python:3.12-slim AS runtime WORKDIR /app COPY --from=builder /app/.venv /app/.venv COPY src/ ./src/ ENV PATH="/app/.venv/bin:$PATH" ENV PYTHONPATH="/app" ENV PYTHONUNBUFFERED=1 ENV PYTHONDONTWRITEBYTECODE=1 Loading
README.md +9 −12 Changes for README.md: 9 added lines, 12 removed lines. Original line number Diff line number Diff line Loading @@ -85,25 +85,22 @@ via the `conformance` marker. #### Vendored spec pins (update this list when bumping a pin) **Edge Application Management** — [camaraproject/EdgeApplicationManagement](https://github.com/camaraproject/EdgeApplicationManagement) at commit `0a64e153e8b860f9a8b7c045fe2e840e7278dada` (API version `wip`, served under `{apiRoot}/edge-application-management/vwip`), vendored in `src/open_exposure_gateway/api/camara/edge_application_management/vwip/`: API version `0.1.0-alpha.1` (served under `{apiRoot}/edge-application-management/v0.1alpha1`), vendored in `src/open_exposure_gateway/api/camara/edge_application_management/v0_1_0_alpha_1/`: | Vendored file | Upstream file | |---|---| | `API_definitions/edge-application-management.yaml` | `code/API_definitions/edge-application-management.yaml` | | `common/CAMARA_common.yaml` | `code/common/CAMARA_common.yaml` | | `API_definitions/edge-application-management-0.1.0-alpha.1.yaml` | `code/API_definitions/edge-application-management.yaml` | To refresh or bump the pin, fetch **both files from the same commit** (a mismatched pair breaks the spec's relative `$ref`s or silently changes its meaning), from the pin folder: The `0.1.0-alpha.1` bundle is self-contained (no external `$ref`s), so it is a single vendored file — the previous `common/` pair is no longer needed. To refresh or bump the pin, fetch the release's bundled spec from the pin folder: ```sh SHA=0a64e153e8b860f9a8b7c045fe2e840e7278dada curl -sL -o API_definitions/edge-application-management.yaml \ SHA=<upstream commit for the target release> curl -sL -o API_definitions/edge-application-management-0.1.0-alpha.1.yaml \ "https://raw.githubusercontent.com/camaraproject/EdgeApplicationManagement/$SHA/code/API_definitions/edge-application-management.yaml" curl -sL -o common/CAMARA_common.yaml \ "https://raw.githubusercontent.com/camaraproject/EdgeApplicationManagement/$SHA/code/common/CAMARA_common.yaml" ``` ### Running with Docker Loading
src/open_exposure_gateway/adapters/database/mappers.py +30 −0 Changes for src/open_exposure_gateway/adapters/database/mappers.py: 30 added lines, 0 removed lines. Original line number Diff line number Diff line from uuid import UUID from open_exposure_gateway.adapters.database.sql import ( AppDeploymentRow, AppInstanceRow, AppRegistrationRow, CallbackDeliveryRow, Loading @@ -7,6 +10,7 @@ from open_exposure_gateway.adapters.database.sql import ( QodSessionRow, ) from open_exposure_gateway.domain.models import ( AppDeployment, AppInstance, AppRegistration, CallbackDelivery, Loading Loading @@ -110,6 +114,32 @@ class AppInstanceMapper: ) class AppDeploymentMapper: @staticmethod def to_domain(row: AppDeploymentRow) -> AppDeployment: return AppDeployment( app_deployment_id=row.app_deployment_id, operation_id=row.operation_id, app_registration_id=row.app_registration_id, app_deployment_name=row.app_deployment_name, edge_cloud_zones=[UUID(zone_id) for zone_id in row.edge_cloud_zones], state=row.state, created_at=row.created_at, updated_at=row.updated_at, ) @staticmethod def to_row(domain: AppDeployment) -> AppDeploymentRow: return AppDeploymentRow( app_deployment_id=domain.app_deployment_id, operation_id=domain.operation_id, app_registration_id=domain.app_registration_id, app_deployment_name=domain.app_deployment_name, edge_cloud_zones=[str(zone_id) for zone_id in domain.edge_cloud_zones], state=domain.state, ) class QodSessionMapper: @staticmethod def to_domain(row: QodSessionRow) -> QodSession: Loading
src/open_exposure_gateway/adapters/database/repos/app_instances.py +10 −0 Changes for src/open_exposure_gateway/adapters/database/repos/app_instances.py: 10 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -25,6 +25,16 @@ class SqlAppInstanceRepository(AppInstanceRepository): row = await self._session.scalar(stmt) return AppInstanceMapper.to_domain(row) if row is not None else None async def list_by_operation_id(self, operation_id: UUID) -> list[AppInstance]: stmt = select(AppInstanceRow).where(AppInstanceRow.operation_id == operation_id) rows = await self._session.scalars(stmt) return [AppInstanceMapper.to_domain(row) for row in rows] async def list_by_app_deployment_id(self, app_deployment_id: UUID) -> list[AppInstance]: stmt = select(AppInstanceRow).where(AppInstanceRow.app_deployment_id == app_deployment_id) rows = await self._session.scalars(stmt) return [AppInstanceMapper.to_domain(row) for row in rows] async def exists_for_app_registration(self, app_registration_id: UUID) -> bool: stmt = select(AppInstanceRow.app_instance_id).where( AppInstanceRow.app_registration_id == app_registration_id, Loading
src/open_exposure_gateway/adapters/database/repos/deployments.py 0 → 100644 +46 −0 Changes for src/open_exposure_gateway/adapters/database/repos/deployments.py: 46 added lines, 0 removed lines. Original line number Diff line number Diff line from uuid import UUID from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from open_exposure_gateway.adapters.database.mappers import AppDeploymentMapper from open_exposure_gateway.adapters.database.sql import AppDeploymentRow from open_exposure_gateway.domain.models import AppDeployment from open_exposure_gateway.ports.database.deployments import AppDeploymentRepository class SqlAppDeploymentRepository(AppDeploymentRepository): def __init__(self, session: AsyncSession) -> None: self._session = session async def get_by_id(self, app_deployment_id: UUID) -> AppDeployment | None: stmt = select(AppDeploymentRow).where( AppDeploymentRow.app_deployment_id == app_deployment_id ) row = await self._session.scalar(stmt) return AppDeploymentMapper.to_domain(row) if row is not None else None async def get_by_operation_id(self, operation_id: UUID) -> AppDeployment | None: stmt = select(AppDeploymentRow).where(AppDeploymentRow.operation_id == operation_id) row = await self._session.scalar(stmt) return AppDeploymentMapper.to_domain(row) if row is not None else None async def list_by_app_registration_id(self, app_registration_id: UUID) -> list[AppDeployment]: stmt = select(AppDeploymentRow).where( AppDeploymentRow.app_registration_id == app_registration_id ) rows = await self._session.scalars(stmt) return [AppDeploymentMapper.to_domain(row) for row in rows] async def list_all(self) -> list[AppDeployment]: stmt = select(AppDeploymentRow) rows = await self._session.scalars(stmt) return [AppDeploymentMapper.to_domain(row) for row in rows] async def save(self, app_deployment: AppDeployment) -> AppDeployment: merged = await self._session.merge(AppDeploymentMapper.to_row(app_deployment)) await self._session.flush() saved = await self.get_by_id(merged.app_deployment_id) if saved is None: raise RuntimeError("Saved app deployment could not be reloaded") return saved