Commit 566fab3c authored by George Papathanail's avatar George Papathanail
Browse files

Merge branch 'refactor/edge-apps' into 'develop'

Refactor/edge apps - Complete POST appinstance and DELETE /appinstances/{appInstanceId} async lifecycle

See merge request !22
parents ac43e55f bec5b473
Loading
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -23,6 +23,7 @@ stages:

variables:
  UV_CACHE_DIR: "$CI_PROJECT_DIR/.cache/uv"
  GIT_STRATEGY: clone

type:
  stage: type
+1 −1
Original line number Diff line number Diff line
@@ -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"]
+3 −0
Original line number Diff line number Diff line
@@ -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]
@@ -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.*"
+25 −1
Original line number Diff line number Diff line
@@ -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:
@@ -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()
+18 −3
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"
@@ -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

@@ -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