Loading src/federation_manager/adapters/database/federation_context_repo.py +12 −0 Original line number Diff line number Diff line Loading @@ -23,6 +23,18 @@ class PostgresFederationContextRepo: .limit(1) ) async def find_active_inbound(self, partner_id: UUID) -> FederationContext | None: return await self._one( select(contexts) .where( contexts.c.partner_op_id == partner_id, contexts.c.direction == "inbound", contexts.c.status == "available", ) .order_by(contexts.c.created_at.desc()) .limit(1) ) async def find_inbound( self, partner_id: UUID, federation_context_id: str ) -> FederationContext | None: Loading src/federation_manager/api/errors.py +11 −0 Original line number Diff line number Diff line Loading @@ -9,6 +9,7 @@ from federation_manager.domain.errors import ( AgreementExpired, AgreementViolation, AuthenticationFailed, FederationAlreadyExists, FederationContextMissing, FederationContextUnknown, NoRouteMatched, Loading Loading @@ -170,6 +171,16 @@ def register_exception_handlers(app: FastAPI) -> None: request.url.path, ) @app.exception_handler(FederationAlreadyExists) async def _federation_exists(request: Request, exc: FederationAlreadyExists) -> JSONResponse: return problem( 409, "federation-exists", "Federation Already Exists", "An active federation context already exists with this partner operator.", request.url.path, ) @app.exception_handler(FederationContextUnknown) async def _context_unknown(request: Request, exc: FederationContextUnknown) -> JSONResponse: return problem( Loading src/federation_manager/api/ewbi/v1/management.py +23 −2 Original line number Diff line number Diff line from typing import Annotated from fastapi import APIRouter, Depends, Path from fastapi import APIRouter, Depends, Path, Response from federation_manager.api.errors import EWBI_ERROR_RESPONSES from federation_manager.api.security import get_bearer_token from federation_manager.application.authentication import PartnerAuthenticator from federation_manager.application.federation import InboundFederationService from federation_manager.contracts.ewbi import ( FederationHealthInfo, FederationHealthResponse, FederationRequestData, FederationResponseData, FederationStatus, ) from federation_manager.dependencies import get_federation_context_repo, get_partner_authenticator from federation_manager.dependencies import ( get_federation_context_repo, get_inbound_federation_service, get_partner_authenticator, ) from federation_manager.domain.errors import FederationContextUnknown from federation_manager.domain.ewbi import EWBI_BASE_PATH from federation_manager.domain.ports import FederationContextRepositoryPort Loading Loading @@ -51,3 +58,17 @@ async def get_federation_health( num_of_accepted_zones="0", ) ) @router.post("/partner", operation_id="CreateFederation", responses=EWBI_ERROR_RESPONSES) async def create_federation( body: FederationRequestData, response: Response, auth: Annotated[PartnerAuthenticator, Depends(get_partner_authenticator)], service: Annotated[InboundFederationService, Depends(get_inbound_federation_service)], token: Annotated[str, Depends(get_bearer_token)], ) -> FederationResponseData: partner = await auth.authenticate(token) context, accepted = await service.create(partner, body) response.headers["Location"] = f"{EWBI_BASE_PATH}/{context.federation_context_id}/partner" return accepted src/federation_manager/application/federation.py +42 −0 Original line number Diff line number Diff line Loading @@ -11,6 +11,7 @@ from federation_manager.contracts.ewbi import ( MobileNetworkIds, ) from federation_manager.domain.errors import ( FederationAlreadyExists, FederationEstablishmentFailed, PartnerNotActive, PartnerResponseInvalid, Loading @@ -24,6 +25,7 @@ from federation_manager.domain.ports import ( ) OUTBOUND = "outbound" INBOUND = "inbound" AVAILABLE = "available" Loading @@ -38,6 +40,7 @@ class LocalOperator: mcc: str mncs: tuple[str, ...] partner_status_link: str platform_caps: tuple[str, ...] = ("serviceAPIs",) class FederationEstablishmentService: Loading Loading @@ -104,3 +107,42 @@ class FederationEstablishmentService: if await self._contexts.find_active_outbound(partner.id) is None: established.append(await self.establish(partner)) return established class InboundFederationService: def __init__( self, contexts: FederationContextRepositoryPort, local: LocalOperator, *, clock: Callable[[], datetime] = _utcnow, id_factory: Callable[[], UUID] = uuid4, context_id_factory: Callable[[], str] = lambda: uuid4().hex, ) -> None: self._contexts = contexts self._local = local self._clock = clock self._new_id = id_factory self._new_context_id = context_id_factory async def create( self, partner: PartnerOP, request: FederationRequestData ) -> tuple[FederationContext, FederationResponseData]: if await self._contexts.find_active_inbound(partner.id) is not None: raise FederationAlreadyExists(partner.id) context = FederationContext( id=self._new_id(), partner_op_id=partner.id, direction=INBOUND, federation_context_id=self._new_context_id(), status=AVAILABLE, created_at=self._clock(), status_callback_url=request.partner_status_link, ) await self._contexts.add(context) return context, FederationResponseData( federation_context_id=context.federation_context_id, platform_caps=list(self._local.platform_caps), partner_op_federation_id=self._local.federation_id, ) src/federation_manager/core/config.py +1 −0 Original line number Diff line number Diff line Loading @@ -17,6 +17,7 @@ class Settings(BaseSettings): mcc: str = "214" mncs: tuple[str, ...] = ("07",) partner_status_link: str = "https://localhost/operatorplatform/federation/v1/partner-status" platform_caps: tuple[str, ...] = ("serviceAPIs",) # ADR-0043 bootstrap path: federate with every active partner that has no outbound context. bootstrap_federation: bool = False Loading Loading
src/federation_manager/adapters/database/federation_context_repo.py +12 −0 Original line number Diff line number Diff line Loading @@ -23,6 +23,18 @@ class PostgresFederationContextRepo: .limit(1) ) async def find_active_inbound(self, partner_id: UUID) -> FederationContext | None: return await self._one( select(contexts) .where( contexts.c.partner_op_id == partner_id, contexts.c.direction == "inbound", contexts.c.status == "available", ) .order_by(contexts.c.created_at.desc()) .limit(1) ) async def find_inbound( self, partner_id: UUID, federation_context_id: str ) -> FederationContext | None: Loading
src/federation_manager/api/errors.py +11 −0 Original line number Diff line number Diff line Loading @@ -9,6 +9,7 @@ from federation_manager.domain.errors import ( AgreementExpired, AgreementViolation, AuthenticationFailed, FederationAlreadyExists, FederationContextMissing, FederationContextUnknown, NoRouteMatched, Loading Loading @@ -170,6 +171,16 @@ def register_exception_handlers(app: FastAPI) -> None: request.url.path, ) @app.exception_handler(FederationAlreadyExists) async def _federation_exists(request: Request, exc: FederationAlreadyExists) -> JSONResponse: return problem( 409, "federation-exists", "Federation Already Exists", "An active federation context already exists with this partner operator.", request.url.path, ) @app.exception_handler(FederationContextUnknown) async def _context_unknown(request: Request, exc: FederationContextUnknown) -> JSONResponse: return problem( Loading
src/federation_manager/api/ewbi/v1/management.py +23 −2 Original line number Diff line number Diff line from typing import Annotated from fastapi import APIRouter, Depends, Path from fastapi import APIRouter, Depends, Path, Response from federation_manager.api.errors import EWBI_ERROR_RESPONSES from federation_manager.api.security import get_bearer_token from federation_manager.application.authentication import PartnerAuthenticator from federation_manager.application.federation import InboundFederationService from federation_manager.contracts.ewbi import ( FederationHealthInfo, FederationHealthResponse, FederationRequestData, FederationResponseData, FederationStatus, ) from federation_manager.dependencies import get_federation_context_repo, get_partner_authenticator from federation_manager.dependencies import ( get_federation_context_repo, get_inbound_federation_service, get_partner_authenticator, ) from federation_manager.domain.errors import FederationContextUnknown from federation_manager.domain.ewbi import EWBI_BASE_PATH from federation_manager.domain.ports import FederationContextRepositoryPort Loading Loading @@ -51,3 +58,17 @@ async def get_federation_health( num_of_accepted_zones="0", ) ) @router.post("/partner", operation_id="CreateFederation", responses=EWBI_ERROR_RESPONSES) async def create_federation( body: FederationRequestData, response: Response, auth: Annotated[PartnerAuthenticator, Depends(get_partner_authenticator)], service: Annotated[InboundFederationService, Depends(get_inbound_federation_service)], token: Annotated[str, Depends(get_bearer_token)], ) -> FederationResponseData: partner = await auth.authenticate(token) context, accepted = await service.create(partner, body) response.headers["Location"] = f"{EWBI_BASE_PATH}/{context.federation_context_id}/partner" return accepted
src/federation_manager/application/federation.py +42 −0 Original line number Diff line number Diff line Loading @@ -11,6 +11,7 @@ from federation_manager.contracts.ewbi import ( MobileNetworkIds, ) from federation_manager.domain.errors import ( FederationAlreadyExists, FederationEstablishmentFailed, PartnerNotActive, PartnerResponseInvalid, Loading @@ -24,6 +25,7 @@ from federation_manager.domain.ports import ( ) OUTBOUND = "outbound" INBOUND = "inbound" AVAILABLE = "available" Loading @@ -38,6 +40,7 @@ class LocalOperator: mcc: str mncs: tuple[str, ...] partner_status_link: str platform_caps: tuple[str, ...] = ("serviceAPIs",) class FederationEstablishmentService: Loading Loading @@ -104,3 +107,42 @@ class FederationEstablishmentService: if await self._contexts.find_active_outbound(partner.id) is None: established.append(await self.establish(partner)) return established class InboundFederationService: def __init__( self, contexts: FederationContextRepositoryPort, local: LocalOperator, *, clock: Callable[[], datetime] = _utcnow, id_factory: Callable[[], UUID] = uuid4, context_id_factory: Callable[[], str] = lambda: uuid4().hex, ) -> None: self._contexts = contexts self._local = local self._clock = clock self._new_id = id_factory self._new_context_id = context_id_factory async def create( self, partner: PartnerOP, request: FederationRequestData ) -> tuple[FederationContext, FederationResponseData]: if await self._contexts.find_active_inbound(partner.id) is not None: raise FederationAlreadyExists(partner.id) context = FederationContext( id=self._new_id(), partner_op_id=partner.id, direction=INBOUND, federation_context_id=self._new_context_id(), status=AVAILABLE, created_at=self._clock(), status_callback_url=request.partner_status_link, ) await self._contexts.add(context) return context, FederationResponseData( federation_context_id=context.federation_context_id, platform_caps=list(self._local.platform_caps), partner_op_federation_id=self._local.federation_id, )
src/federation_manager/core/config.py +1 −0 Original line number Diff line number Diff line Loading @@ -17,6 +17,7 @@ class Settings(BaseSettings): mcc: str = "214" mncs: tuple[str, ...] = ("07",) partner_status_link: str = "https://localhost/operatorplatform/federation/v1/partner-status" platform_caps: tuple[str, ...] = ("serviceAPIs",) # ADR-0043 bootstrap path: federate with every active partner that has no outbound context. bootstrap_federation: bool = False Loading