Loading src/srm/adapters/database/repos/topology.py +15 −1 Original line number Diff line number Diff line Loading @@ -59,6 +59,14 @@ class SqlZoneRepository(ZoneRepository): return ZoneMapper.to_domain(row) if row is not None else None async def list_active_resource_zones(self) -> list[Zone]: return await self.list_resource_zones(state=ZoneState.ACTIVE) async def list_resource_zones( self, *, state: ZoneState | None = None, region: str | None = None, ) -> list[Zone]: stmt = ( select(ZoneRow) .options( Loading @@ -66,9 +74,15 @@ class SqlZoneRepository(ZoneRepository): .selectinload(DomainRow.capabilities) .selectinload(CapabilityRow.control_path_bindings) ) .where(ZoneRow.kind == ZoneKind.RESOURCE, ZoneRow.state == ZoneState.ACTIVE) .where(ZoneRow.kind == ZoneKind.RESOURCE) .order_by(ZoneRow.ref, ZoneRow.id) ) if state is not None: stmt = stmt.where(ZoneRow.state == state) if region is not None: stmt = stmt.where( ZoneRow.zone_metadata["location"]["region"].astext == region, ) rows = (await self._session.scalars(stmt)).all() return [ZoneMapper.to_domain(row) for row in rows] Loading src/srm/api/dependencies.py +16 −0 Original line number Diff line number Diff line Loading @@ -4,12 +4,15 @@ from fastapi import Depends, Request from sqlalchemy.ext.asyncio import AsyncSession from srm.adapters.database.repos.catalog import SqlServiceSpecificationRepository from srm.adapters.database.repos.topology import SqlZoneRepository from srm.app_state import AppState from srm.application.use_cases.catalog import ( CreateServiceSpecificationUseCase, DeleteServiceSpecificationUseCase, GetServiceSpecificationUseCase, ) from srm.application.use_cases.topology import ListZonesUseCase from srm.config import get_settings def get_app_state(request: Request) -> AppState: Loading Loading @@ -47,6 +50,14 @@ def get_delete_service_specification_use_case( return DeleteServiceSpecificationUseCase(SqlServiceSpecificationRepository(session)) def get_list_zones_use_case(session: SessionDep) -> ListZonesUseCase: return ListZonesUseCase(SqlZoneRepository(session)) def get_zone_provider_name() -> str: return get_settings().zone_provider_name CreateServiceSpecificationUseCaseDep = Annotated[ CreateServiceSpecificationUseCase, Depends(get_create_service_specification_use_case), Loading @@ -59,3 +70,8 @@ DeleteServiceSpecificationUseCaseDep = Annotated[ DeleteServiceSpecificationUseCase, Depends(get_delete_service_specification_use_case), ] ListZonesUseCaseDep = Annotated[ ListZonesUseCase, Depends(get_list_zones_use_case), ] ZoneProviderNameDep = Annotated[str, Depends(get_zone_provider_name)] src/srm/api/rest/router.py +53 −1 Original line number Diff line number Diff line from typing import Annotated from uuid import UUID import structlog from fastapi import APIRouter, HTTPException, status from fastapi import APIRouter, Header, HTTPException, Response, status from srm.adapters.errors import DuplicateServiceSpecificationError, ServiceSpecificationInUseError from srm.api.dependencies import ( CreateServiceSpecificationUseCaseDep, DeleteServiceSpecificationUseCaseDep, GetServiceSpecificationUseCaseDep, ListZonesUseCaseDep, ZoneProviderNameDep, ) from srm.api.rest.schemas import ( CreateServiceSpecificationRequest, Loading @@ -17,7 +20,10 @@ from srm.api.rest.schemas import ( ServiceCapabilityRequirementResponseSchema, ServiceDeploymentUnitCreateSchema, ServiceSpecificationCreateSchema, ZoneDomainSummaryResponse, ZoneResponse, ) from srm.application.use_cases.topology import ListZonesCommand from srm.domain.models.canonical_parameters.parameters import ( CapabilityParameters, CapabilityTarget, Loading @@ -30,6 +36,7 @@ from srm.domain.models.catalog import ( ServiceDeploymentUnit, ServiceSpecification, ) from srm.domain.models.topology import DomainKind, Zone, ZoneState internal = APIRouter(prefix="/internal", tags=["Internal"]) logger: structlog.BoundLogger = structlog.get_logger(__name__) Loading Loading @@ -134,6 +141,33 @@ def _build_get_response(saved: ServiceSpecification) -> GetServiceSpecificationR ) def _zone_metadata(zone: Zone, provider_name: str) -> dict[str, object]: metadata = dict(zone.metadata) if metadata.get("provider") is None: metadata["provider"] = provider_name return metadata def _build_zone_response(zone: Zone, provider_name: str) -> ZoneResponse: return ZoneResponse( id=zone.id, name=zone.name, state=zone.state.value, metadata=_zone_metadata(zone, provider_name), domains=[ ZoneDomainSummaryResponse( id=domain.id, name=domain.name, kind=domain.kind.value, state=domain.state.value, metadata=domain.metadata, ) for domain in zone.domains if domain.kind in {DomainKind.COMPUTE, DomainKind.NETWORK} ], ) @internal.post( "/catalog/service-specifications", status_code=status.HTTP_201_CREATED, Loading Loading @@ -201,6 +235,24 @@ async def get_service_specification( return _build_get_response(specification) @internal.get("/zones") async def list_zones( use_case: ListZonesUseCaseDep, provider_name: ZoneProviderNameDep, response: Response, state: ZoneState | None = None, region: str | None = None, x_correlator: Annotated[str | None, Header()] = None, ) -> list[ZoneResponse]: logger.info("list_zones_requested", state=state, region=region) zones = await use_case.execute(ListZonesCommand(state=state, region=region)) if x_correlator is not None: response.headers["x-correlator"] = x_correlator return [_build_zone_response(zone, provider_name) for zone in zones] @internal.delete( "/catalog/service-specifications/{id}", status_code=status.HTTP_204_NO_CONTENT, Loading src/srm/api/rest/schemas.py +16 −0 Original line number Diff line number Diff line Loading @@ -87,5 +87,21 @@ class GetServiceSpecificationResponse(DomainModel): service_capability_requirements: list[ServiceCapabilityRequirementResponseSchema] class ZoneDomainSummaryResponse(DomainModel): id: UUID name: str kind: str state: str metadata: dict[str, Any] = Field(default_factory=dict) class ZoneResponse(DomainModel): id: UUID name: str state: str metadata: dict[str, Any] domains: list[ZoneDomainSummaryResponse] = Field(default_factory=list) class ErrorResponse(DomainModel): detail: str src/srm/application/use_cases/topology.py 0 → 100644 +21 −0 Original line number Diff line number Diff line from dataclasses import dataclass from srm.domain.models.topology import Zone, ZoneState from srm.domain.ports.database.topology import ZoneRepository @dataclass(frozen=True, slots=True) class ListZonesCommand: state: ZoneState | None = None region: str | None = None class ListZonesUseCase: def __init__(self, zones: ZoneRepository) -> None: self._zones = zones async def execute(self, command: ListZonesCommand) -> list[Zone]: return await self._zones.list_resource_zones( state=command.state, region=command.region, ) Loading
src/srm/adapters/database/repos/topology.py +15 −1 Original line number Diff line number Diff line Loading @@ -59,6 +59,14 @@ class SqlZoneRepository(ZoneRepository): return ZoneMapper.to_domain(row) if row is not None else None async def list_active_resource_zones(self) -> list[Zone]: return await self.list_resource_zones(state=ZoneState.ACTIVE) async def list_resource_zones( self, *, state: ZoneState | None = None, region: str | None = None, ) -> list[Zone]: stmt = ( select(ZoneRow) .options( Loading @@ -66,9 +74,15 @@ class SqlZoneRepository(ZoneRepository): .selectinload(DomainRow.capabilities) .selectinload(CapabilityRow.control_path_bindings) ) .where(ZoneRow.kind == ZoneKind.RESOURCE, ZoneRow.state == ZoneState.ACTIVE) .where(ZoneRow.kind == ZoneKind.RESOURCE) .order_by(ZoneRow.ref, ZoneRow.id) ) if state is not None: stmt = stmt.where(ZoneRow.state == state) if region is not None: stmt = stmt.where( ZoneRow.zone_metadata["location"]["region"].astext == region, ) rows = (await self._session.scalars(stmt)).all() return [ZoneMapper.to_domain(row) for row in rows] Loading
src/srm/api/dependencies.py +16 −0 Original line number Diff line number Diff line Loading @@ -4,12 +4,15 @@ from fastapi import Depends, Request from sqlalchemy.ext.asyncio import AsyncSession from srm.adapters.database.repos.catalog import SqlServiceSpecificationRepository from srm.adapters.database.repos.topology import SqlZoneRepository from srm.app_state import AppState from srm.application.use_cases.catalog import ( CreateServiceSpecificationUseCase, DeleteServiceSpecificationUseCase, GetServiceSpecificationUseCase, ) from srm.application.use_cases.topology import ListZonesUseCase from srm.config import get_settings def get_app_state(request: Request) -> AppState: Loading Loading @@ -47,6 +50,14 @@ def get_delete_service_specification_use_case( return DeleteServiceSpecificationUseCase(SqlServiceSpecificationRepository(session)) def get_list_zones_use_case(session: SessionDep) -> ListZonesUseCase: return ListZonesUseCase(SqlZoneRepository(session)) def get_zone_provider_name() -> str: return get_settings().zone_provider_name CreateServiceSpecificationUseCaseDep = Annotated[ CreateServiceSpecificationUseCase, Depends(get_create_service_specification_use_case), Loading @@ -59,3 +70,8 @@ DeleteServiceSpecificationUseCaseDep = Annotated[ DeleteServiceSpecificationUseCase, Depends(get_delete_service_specification_use_case), ] ListZonesUseCaseDep = Annotated[ ListZonesUseCase, Depends(get_list_zones_use_case), ] ZoneProviderNameDep = Annotated[str, Depends(get_zone_provider_name)]
src/srm/api/rest/router.py +53 −1 Original line number Diff line number Diff line from typing import Annotated from uuid import UUID import structlog from fastapi import APIRouter, HTTPException, status from fastapi import APIRouter, Header, HTTPException, Response, status from srm.adapters.errors import DuplicateServiceSpecificationError, ServiceSpecificationInUseError from srm.api.dependencies import ( CreateServiceSpecificationUseCaseDep, DeleteServiceSpecificationUseCaseDep, GetServiceSpecificationUseCaseDep, ListZonesUseCaseDep, ZoneProviderNameDep, ) from srm.api.rest.schemas import ( CreateServiceSpecificationRequest, Loading @@ -17,7 +20,10 @@ from srm.api.rest.schemas import ( ServiceCapabilityRequirementResponseSchema, ServiceDeploymentUnitCreateSchema, ServiceSpecificationCreateSchema, ZoneDomainSummaryResponse, ZoneResponse, ) from srm.application.use_cases.topology import ListZonesCommand from srm.domain.models.canonical_parameters.parameters import ( CapabilityParameters, CapabilityTarget, Loading @@ -30,6 +36,7 @@ from srm.domain.models.catalog import ( ServiceDeploymentUnit, ServiceSpecification, ) from srm.domain.models.topology import DomainKind, Zone, ZoneState internal = APIRouter(prefix="/internal", tags=["Internal"]) logger: structlog.BoundLogger = structlog.get_logger(__name__) Loading Loading @@ -134,6 +141,33 @@ def _build_get_response(saved: ServiceSpecification) -> GetServiceSpecificationR ) def _zone_metadata(zone: Zone, provider_name: str) -> dict[str, object]: metadata = dict(zone.metadata) if metadata.get("provider") is None: metadata["provider"] = provider_name return metadata def _build_zone_response(zone: Zone, provider_name: str) -> ZoneResponse: return ZoneResponse( id=zone.id, name=zone.name, state=zone.state.value, metadata=_zone_metadata(zone, provider_name), domains=[ ZoneDomainSummaryResponse( id=domain.id, name=domain.name, kind=domain.kind.value, state=domain.state.value, metadata=domain.metadata, ) for domain in zone.domains if domain.kind in {DomainKind.COMPUTE, DomainKind.NETWORK} ], ) @internal.post( "/catalog/service-specifications", status_code=status.HTTP_201_CREATED, Loading Loading @@ -201,6 +235,24 @@ async def get_service_specification( return _build_get_response(specification) @internal.get("/zones") async def list_zones( use_case: ListZonesUseCaseDep, provider_name: ZoneProviderNameDep, response: Response, state: ZoneState | None = None, region: str | None = None, x_correlator: Annotated[str | None, Header()] = None, ) -> list[ZoneResponse]: logger.info("list_zones_requested", state=state, region=region) zones = await use_case.execute(ListZonesCommand(state=state, region=region)) if x_correlator is not None: response.headers["x-correlator"] = x_correlator return [_build_zone_response(zone, provider_name) for zone in zones] @internal.delete( "/catalog/service-specifications/{id}", status_code=status.HTTP_204_NO_CONTENT, Loading
src/srm/api/rest/schemas.py +16 −0 Original line number Diff line number Diff line Loading @@ -87,5 +87,21 @@ class GetServiceSpecificationResponse(DomainModel): service_capability_requirements: list[ServiceCapabilityRequirementResponseSchema] class ZoneDomainSummaryResponse(DomainModel): id: UUID name: str kind: str state: str metadata: dict[str, Any] = Field(default_factory=dict) class ZoneResponse(DomainModel): id: UUID name: str state: str metadata: dict[str, Any] domains: list[ZoneDomainSummaryResponse] = Field(default_factory=list) class ErrorResponse(DomainModel): detail: str
src/srm/application/use_cases/topology.py 0 → 100644 +21 −0 Original line number Diff line number Diff line from dataclasses import dataclass from srm.domain.models.topology import Zone, ZoneState from srm.domain.ports.database.topology import ZoneRepository @dataclass(frozen=True, slots=True) class ListZonesCommand: state: ZoneState | None = None region: str | None = None class ListZonesUseCase: def __init__(self, zones: ZoneRepository) -> None: self._zones = zones async def execute(self, command: ListZonesCommand) -> list[Zone]: return await self._zones.list_resource_zones( state=command.state, region=command.region, )