from __future__ import annotations from fastapi import APIRouter, Depends from fastapi import Query from app.api.dependencies import get_policy_service, get_repair_service from app.schemas.policy import ( PlacementDecisionBackendSummary, PlacementDecisionResponse, PlacementPolicyListResponse, PlacementPolicyMutationResponse, PlacementPolicySummary, UpsertPlacementPolicyRequest, ) from app.services.policy_service import PolicyService from app.services.repair_service import RepairService router = APIRouter() def _policy_to_summary(policy) -> PlacementPolicySummary: return PlacementPolicySummary( id=policy.id, file_class=policy.file_class, require_local=policy.require_local, stable_replica_count=policy.stable_replica_count, opportunistic_replica_count=policy.opportunistic_replica_count, preferred_backend_ids=PolicyService._decode_backend_ids(policy.preferred_backend_ids_json), excluded_backend_ids=PolicyService._decode_backend_ids(policy.excluded_backend_ids_json), max_non_local_size_bytes=policy.max_non_local_size_bytes, created_at=policy.created_at, updated_at=policy.updated_at, ) @router.get("/placement", response_model=PlacementPolicyListResponse) async def list_placement_policies( service: PolicyService = Depends(get_policy_service), ) -> PlacementPolicyListResponse: policies = await service.list_policies() return PlacementPolicyListResponse(items=[_policy_to_summary(policy) for policy in policies]) @router.get("/placement/preview", response_model=PlacementDecisionResponse) async def preview_placement_decision( mime_type: str | None = Query(default=None), size_bytes: int | None = Query(default=None, ge=0), service: PolicyService = Depends(get_policy_service), ) -> PlacementDecisionResponse: decision = await service.get_placement_decision(mime_type, size_bytes=size_bytes) return PlacementDecisionResponse( file_class=decision.file_class, mime_type=mime_type, size_bytes=size_bytes, non_local_allowed=decision.non_local_allowed, policy=_policy_to_summary(decision.policy), selected_backends=[ PlacementDecisionBackendSummary.model_validate(backend, from_attributes=True) for backend in decision.backends ], ) @router.put("/placement/{file_class}", response_model=PlacementPolicyMutationResponse) async def upsert_placement_policy( file_class: str, payload: UpsertPlacementPolicyRequest, service: PolicyService = Depends(get_policy_service), repair_service: RepairService = Depends(get_repair_service), ) -> PlacementPolicyMutationResponse: policy = await service.upsert_policy( file_class=file_class, require_local=payload.require_local, stable_replica_count=payload.stable_replica_count, opportunistic_replica_count=payload.opportunistic_replica_count, preferred_backend_ids=payload.preferred_backend_ids, excluded_backend_ids=payload.excluded_backend_ids, max_non_local_size_bytes=payload.max_non_local_size_bytes, ) enqueued_jobs = await repair_service.enqueue_full_reconcile() return PlacementPolicyMutationResponse(policy=_policy_to_summary(policy), reconcile_enqueued_jobs=enqueued_jobs)