Loading .gitlab-ci.yml +1 −0 Changes for .gitlab-ci.yml: 1 added line, 0 removed lines. Original line number Diff line number Diff line Loading @@ -23,6 +23,7 @@ stages: variables: UV_CACHE_DIR: "$CI_PROJECT_DIR/.cache/uv" GIT_STRATEGY: clone type: stage: type Loading .pre-commit-config.yaml +1 −1 Changes for .pre-commit-config.yaml: 1 added line, 1 removed line. Original line number Diff line number Diff line Loading @@ -23,4 +23,4 @@ repos: - id: mypy files: ^src/open_exposure_gateway/|^tests/ args: [--strict, --ignore-missing-imports, --cache-dir, .cache/mypy] additional_dependencies: ["fastapi[standard]>=0.135.1", "pydantic>=2.0", "pydantic-settings>=2.0", "httpx>=0.27", "pytest>=9.0.2", "sqlalchemy>=2.0.48", "pytest-asyncio>=0.24", "testcontainers>=4.0.0"] #TODO, always add necessary additional_dependencies: ["fastapi[standard]>=0.135.1", "pydantic>=2.0", "pydantic-settings>=2.0", "httpx>=0.27", "pytest>=9.0.2", "sqlalchemy>=2.0.48", "pytest-asyncio>=0.24", "testcontainers>=4.0.0", "types-PyYAML>=6.0.1"] pyproject.toml +3 −0 Changes for pyproject.toml: 3 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -32,9 +32,11 @@ dev = [ "pytest>=9.0.2", "pytest-asyncio>=0.24", "pytest-cov>=6.0.0", "pyyaml>=6.0.1", "ruff>=0.15.6", "schemathesis>=4.22", "testcontainers>=4.0.0", "types-PyYAML>=6.0.1", ] [tool.setuptools.packages.find] Loading @@ -58,6 +60,7 @@ python_version = "3.12" cache_dir = ".cache/mypy" strict = true ignore_missing_imports = true mypy_path = "src" [[tool.mypy.overrides]] module = "tests.conformance.*" Loading src/open_exposure_gateway/adapters/database/repos/app_instances.py +25 −1 Changes for src/open_exposure_gateway/adapters/database/repos/app_instances.py: 25 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -5,9 +5,11 @@ from sqlalchemy.ext.asyncio import AsyncSession from open_exposure_gateway.adapters.database.mappers import AppInstanceMapper from open_exposure_gateway.adapters.database.sql import AppInstanceRow from open_exposure_gateway.domain.models import AppInstance from open_exposure_gateway.domain.models import AppInstance, AppInstanceState from open_exposure_gateway.ports.database.instances import AppInstanceRepository _TERMINAL_STATES = (AppInstanceState.FAILED, AppInstanceState.TERMINATED) class SqlAppInstanceRepository(AppInstanceRepository): def __init__(self, session: AsyncSession) -> None: Loading @@ -18,6 +20,28 @@ class SqlAppInstanceRepository(AppInstanceRepository): row = await self._session.scalar(stmt) return AppInstanceMapper.to_domain(row) if row is not None else None async def get_by_operation_id(self, operation_id: UUID) -> AppInstance | None: stmt = select(AppInstanceRow).where(AppInstanceRow.operation_id == operation_id) row = await self._session.scalar(stmt) return AppInstanceMapper.to_domain(row) if row is not None else None 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, AppInstanceRow.state.notin_(_TERMINAL_STATES), ) row = await self._session.scalar(stmt.limit(1)) return row is not None async def exists_in_zone(self, app_registration_id: UUID, edge_cloud_zone_id: UUID) -> bool: stmt = select(AppInstanceRow.app_instance_id).where( AppInstanceRow.app_registration_id == app_registration_id, AppInstanceRow.edge_cloud_zone_id == edge_cloud_zone_id, AppInstanceRow.state.notin_(_TERMINAL_STATES), ) row = await self._session.scalar(stmt.limit(1)) return row is not None async def save(self, app_instance: AppInstance) -> AppInstance: merged = await self._session.merge(AppInstanceMapper.to_row(app_instance)) await self._session.flush() Loading src/open_exposure_gateway/adapters/database/repos/app_registrations.py +18 −3 Changes for src/open_exposure_gateway/adapters/database/repos/app_registrations.py: 18 added lines, 3 removed lines. Original line number Diff line number Diff line from uuid import UUID from sqlalchemy import select from sqlalchemy import select, update from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import AsyncSession from open_exposure_gateway.adapters.database.mappers import AppRegistrationMapper from open_exposure_gateway.adapters.database.sql import AppRegistrationRow from open_exposure_gateway.adapters.errors import DuplicateAppRegistrationError from open_exposure_gateway.domain.models import AppRegistration from open_exposure_gateway.domain.models import AppRegistration, AppRegistrationStatus from open_exposure_gateway.ports.database.registration import AppRegistrationRepository _UNIQUE_VIOLATION = "23505" Loading @@ -25,7 +25,10 @@ class SqlAppRegistrationRepository(AppRegistrationRepository): return AppRegistrationMapper.to_domain(row) if row is not None else None async def get_by_app_id(self, app_id: UUID) -> AppRegistration | None: stmt = select(AppRegistrationRow).where(AppRegistrationRow.app_id == app_id) stmt = select(AppRegistrationRow).where( AppRegistrationRow.app_id == app_id, AppRegistrationRow.status != AppRegistrationStatus.DELETED, ) row = await self._session.scalar(stmt) return AppRegistrationMapper.to_domain(row) if row is not None else None Loading @@ -41,3 +44,15 @@ class SqlAppRegistrationRepository(AppRegistrationRepository): if saved is None: raise RuntimeError("Saved app registration could not be reloaded") return saved async def delete_by_app_id(self, app_id: UUID) -> None: """Soft-delete: flip status to DELETED, keep the row.""" stmt = ( update(AppRegistrationRow) .where( AppRegistrationRow.app_id == app_id, AppRegistrationRow.status != AppRegistrationStatus.DELETED, ) .values(status=AppRegistrationStatus.DELETED) ) await self._session.execute(stmt) Loading
.gitlab-ci.yml +1 −0 Changes for .gitlab-ci.yml: 1 added line, 0 removed lines. Original line number Diff line number Diff line Loading @@ -23,6 +23,7 @@ stages: variables: UV_CACHE_DIR: "$CI_PROJECT_DIR/.cache/uv" GIT_STRATEGY: clone type: stage: type Loading
.pre-commit-config.yaml +1 −1 Changes for .pre-commit-config.yaml: 1 added line, 1 removed line. Original line number Diff line number Diff line Loading @@ -23,4 +23,4 @@ repos: - id: mypy files: ^src/open_exposure_gateway/|^tests/ args: [--strict, --ignore-missing-imports, --cache-dir, .cache/mypy] additional_dependencies: ["fastapi[standard]>=0.135.1", "pydantic>=2.0", "pydantic-settings>=2.0", "httpx>=0.27", "pytest>=9.0.2", "sqlalchemy>=2.0.48", "pytest-asyncio>=0.24", "testcontainers>=4.0.0"] #TODO, always add necessary additional_dependencies: ["fastapi[standard]>=0.135.1", "pydantic>=2.0", "pydantic-settings>=2.0", "httpx>=0.27", "pytest>=9.0.2", "sqlalchemy>=2.0.48", "pytest-asyncio>=0.24", "testcontainers>=4.0.0", "types-PyYAML>=6.0.1"]
pyproject.toml +3 −0 Changes for pyproject.toml: 3 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -32,9 +32,11 @@ dev = [ "pytest>=9.0.2", "pytest-asyncio>=0.24", "pytest-cov>=6.0.0", "pyyaml>=6.0.1", "ruff>=0.15.6", "schemathesis>=4.22", "testcontainers>=4.0.0", "types-PyYAML>=6.0.1", ] [tool.setuptools.packages.find] Loading @@ -58,6 +60,7 @@ python_version = "3.12" cache_dir = ".cache/mypy" strict = true ignore_missing_imports = true mypy_path = "src" [[tool.mypy.overrides]] module = "tests.conformance.*" Loading
src/open_exposure_gateway/adapters/database/repos/app_instances.py +25 −1 Changes for src/open_exposure_gateway/adapters/database/repos/app_instances.py: 25 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -5,9 +5,11 @@ from sqlalchemy.ext.asyncio import AsyncSession from open_exposure_gateway.adapters.database.mappers import AppInstanceMapper from open_exposure_gateway.adapters.database.sql import AppInstanceRow from open_exposure_gateway.domain.models import AppInstance from open_exposure_gateway.domain.models import AppInstance, AppInstanceState from open_exposure_gateway.ports.database.instances import AppInstanceRepository _TERMINAL_STATES = (AppInstanceState.FAILED, AppInstanceState.TERMINATED) class SqlAppInstanceRepository(AppInstanceRepository): def __init__(self, session: AsyncSession) -> None: Loading @@ -18,6 +20,28 @@ class SqlAppInstanceRepository(AppInstanceRepository): row = await self._session.scalar(stmt) return AppInstanceMapper.to_domain(row) if row is not None else None async def get_by_operation_id(self, operation_id: UUID) -> AppInstance | None: stmt = select(AppInstanceRow).where(AppInstanceRow.operation_id == operation_id) row = await self._session.scalar(stmt) return AppInstanceMapper.to_domain(row) if row is not None else None 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, AppInstanceRow.state.notin_(_TERMINAL_STATES), ) row = await self._session.scalar(stmt.limit(1)) return row is not None async def exists_in_zone(self, app_registration_id: UUID, edge_cloud_zone_id: UUID) -> bool: stmt = select(AppInstanceRow.app_instance_id).where( AppInstanceRow.app_registration_id == app_registration_id, AppInstanceRow.edge_cloud_zone_id == edge_cloud_zone_id, AppInstanceRow.state.notin_(_TERMINAL_STATES), ) row = await self._session.scalar(stmt.limit(1)) return row is not None async def save(self, app_instance: AppInstance) -> AppInstance: merged = await self._session.merge(AppInstanceMapper.to_row(app_instance)) await self._session.flush() Loading
src/open_exposure_gateway/adapters/database/repos/app_registrations.py +18 −3 Changes for src/open_exposure_gateway/adapters/database/repos/app_registrations.py: 18 added lines, 3 removed lines. Original line number Diff line number Diff line from uuid import UUID from sqlalchemy import select from sqlalchemy import select, update from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import AsyncSession from open_exposure_gateway.adapters.database.mappers import AppRegistrationMapper from open_exposure_gateway.adapters.database.sql import AppRegistrationRow from open_exposure_gateway.adapters.errors import DuplicateAppRegistrationError from open_exposure_gateway.domain.models import AppRegistration from open_exposure_gateway.domain.models import AppRegistration, AppRegistrationStatus from open_exposure_gateway.ports.database.registration import AppRegistrationRepository _UNIQUE_VIOLATION = "23505" Loading @@ -25,7 +25,10 @@ class SqlAppRegistrationRepository(AppRegistrationRepository): return AppRegistrationMapper.to_domain(row) if row is not None else None async def get_by_app_id(self, app_id: UUID) -> AppRegistration | None: stmt = select(AppRegistrationRow).where(AppRegistrationRow.app_id == app_id) stmt = select(AppRegistrationRow).where( AppRegistrationRow.app_id == app_id, AppRegistrationRow.status != AppRegistrationStatus.DELETED, ) row = await self._session.scalar(stmt) return AppRegistrationMapper.to_domain(row) if row is not None else None Loading @@ -41,3 +44,15 @@ class SqlAppRegistrationRepository(AppRegistrationRepository): if saved is None: raise RuntimeError("Saved app registration could not be reloaded") return saved async def delete_by_app_id(self, app_id: UUID) -> None: """Soft-delete: flip status to DELETED, keep the row.""" stmt = ( update(AppRegistrationRow) .where( AppRegistrationRow.app_id == app_id, AppRegistrationRow.status != AppRegistrationStatus.DELETED, ) .values(status=AppRegistrationStatus.DELETED) ) await self._session.execute(stmt)