Commit b13d7fab authored by Paris Stentoumis's avatar Paris Stentoumis
Browse files

Service Specification Endpoints with simpler test ci command

parent 44933cb6
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -49,7 +49,7 @@ test:
  services:
    - docker:dind
  script:
    - pytest tests/unit tests/integration tests/api --cov=src/srm --cov-branch --cov-report=term-missing || [ $? -eq 5 ] #bypass no tests found error.
    - pytest --cov=src/srm --cov-branch --cov-report=term-missing
  coverage: '/TOTAL.+ ([0-9]{1,3}(?:\.[0-9]+)?%)/'

build-and-push:
+60 −2
Original line number Diff line number Diff line
@@ -3,7 +3,7 @@ from __future__ import annotations
from uuid import UUID

import structlog
from sqlalchemy import select
from sqlalchemy import delete, select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
@@ -16,14 +16,17 @@ from srm.adapters.database.mappers import (
from srm.adapters.database.sql import (
    ServiceCapabilityRequirementRow,
    ServiceDeploymentUnitRow,
    ServiceInstanceRow,
    ServiceSpecificationRow,
)
from srm.adapters.errors import DuplicateServiceSpecificationError
from srm.adapters.errors import DuplicateServiceSpecificationError, ServiceSpecificationInUseError
from srm.domain.models.catalog import (
    ServiceCapabilityRequirement,
    ServiceDeploymentUnit,
    ServiceSpecification,
)
from srm.domain.models.catalog.enums import ServiceSpecificationState
from srm.domain.models.runtime_inventory.enums import ServiceInstanceState
from srm.domain.ports.database.catalog import (
    ServiceCapabilityRequirementRepository,
    ServiceDeploymentUnitRepository,
@@ -47,6 +50,13 @@ def _log_integrity_error(exc: IntegrityError, *, entity: str, operation: str) ->
    )


def _link_requirement_deployment_unit_rows(row: ServiceSpecificationRow) -> None:
    deployment_units_by_id = {unit.id: unit for unit in row.deployment_units}
    for requirement in row.capability_requirements:
        if requirement.deployment_unit_id in deployment_units_by_id:
            requirement.deployment_unit = deployment_units_by_id[requirement.deployment_unit_id]


class SqlServiceSpecificationRepository(ServiceSpecificationRepository):
    def __init__(self, session: AsyncSession) -> None:
        self._session = session
@@ -65,6 +75,7 @@ class SqlServiceSpecificationRepository(ServiceSpecificationRepository):

    async def create(self, service_specification: ServiceSpecification) -> ServiceSpecification:
        row = ServiceSpecificationMapper.to_row(service_specification)
        _link_requirement_deployment_unit_rows(row)
        self._session.add(row)
        try:
            await self._session.flush()
@@ -79,6 +90,53 @@ class SqlServiceSpecificationRepository(ServiceSpecificationRepository):
            raise RuntimeError("Created service specification could not be reloaded")
        return saved

    async def delete_or_retire(self, id: UUID) -> bool:
        row = await self._session.get(ServiceSpecificationRow, id)
        if row is None:
            return False

        any_instance_id = await self._session.scalar(
            select(ServiceInstanceRow.id)
            .where(ServiceInstanceRow.service_specification_id == id)
            .limit(1)
        )
        if any_instance_id is None:
            await self._session.execute(
                delete(ServiceCapabilityRequirementRow).where(
                    ServiceCapabilityRequirementRow.service_specification_id == id
                )
            )
            await self._session.execute(
                delete(ServiceDeploymentUnitRow).where(
                    ServiceDeploymentUnitRow.service_specification_id == id
                )
            )
            await self._session.execute(
                delete(ServiceSpecificationRow).where(ServiceSpecificationRow.id == id)
            )
            await self._session.flush()
            return True

        live_instance_id = await self._session.scalar(
            select(ServiceInstanceRow.id)
            .where(ServiceInstanceRow.service_specification_id == id)
            .where(
                ServiceInstanceRow.state.not_in(
                    [
                        ServiceInstanceState.TERMINATED,
                        ServiceInstanceState.FAILED,
                    ]
                )
            )
            .limit(1)
        )
        if live_instance_id is not None:
            raise ServiceSpecificationInUseError()

        row.state = ServiceSpecificationState.RETIRED
        await self._session.flush()
        return True


class SqlServiceDeploymentUnitRepository(ServiceDeploymentUnitRepository):
    def __init__(self, session: AsyncSession) -> None:
+3 −0
Original line number Diff line number Diff line
@@ -227,6 +227,9 @@ class ServiceCapabilityRequirementRow(AuditedMixin, Base):
    deployment_unit_id: Mapped[UUID | None] = mapped_column(
        ForeignKey("service_deployment_unit.id")
    )
    deployment_unit: Mapped["ServiceDeploymentUnitRow | None"] = relationship(
        foreign_keys=[deployment_unit_id]
    )
    ref: Mapped[str] = mapped_column(String(255), nullable=False)
    capability_kind: Mapped[CapabilityKind] = mapped_column(
        _enum_type(CapabilityKind), nullable=False
+5 −0
Original line number Diff line number Diff line
@@ -10,3 +10,8 @@ class DuplicateEntryError(Exception):

class DuplicateServiceSpecificationError(DuplicateEntryError):
    entity_name: ClassVar[str] = "Service Specification"


class ServiceSpecificationInUseError(Exception):
    def __init__(self) -> None:
        super().__init__("Service specification has live service instances.")
+38 −0
Original line number Diff line number Diff line
@@ -3,7 +3,13 @@ from typing import Annotated, AsyncGenerator, cast
from fastapi import Depends, Request
from sqlalchemy.ext.asyncio import AsyncSession

from srm.adapters.database.repos.catalog import SqlServiceSpecificationRepository
from srm.app_state import AppState
from srm.application.use_cases.catalog import (
    CreateServiceSpecificationUseCase,
    DeleteServiceSpecificationUseCase,
    GetServiceSpecificationUseCase,
)


def get_app_state(request: Request) -> AppState:
@@ -21,3 +27,35 @@ async def get_session(request: Request) -> AsyncGenerator[AsyncSession, None]:


SessionDep = Annotated[AsyncSession, Depends(get_session)]


def get_create_service_specification_use_case(
    session: SessionDep,
) -> CreateServiceSpecificationUseCase:
    return CreateServiceSpecificationUseCase(SqlServiceSpecificationRepository(session))


def get_service_specification_use_case(
    session: SessionDep,
) -> GetServiceSpecificationUseCase:
    return GetServiceSpecificationUseCase(SqlServiceSpecificationRepository(session))


def get_delete_service_specification_use_case(
    session: SessionDep,
) -> DeleteServiceSpecificationUseCase:
    return DeleteServiceSpecificationUseCase(SqlServiceSpecificationRepository(session))


CreateServiceSpecificationUseCaseDep = Annotated[
    CreateServiceSpecificationUseCase,
    Depends(get_create_service_specification_use_case),
]
GetServiceSpecificationUseCaseDep = Annotated[
    GetServiceSpecificationUseCase,
    Depends(get_service_specification_use_case),
]
DeleteServiceSpecificationUseCaseDep = Annotated[
    DeleteServiceSpecificationUseCase,
    Depends(get_delete_service_specification_use_case),
]
Loading