feat(records): implement native eAkte vertical
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from govoplan_core.auth import ApiPrincipal, get_api_principal, has_scope
|
||||
from govoplan_core.core.records import RecordFilingRequest, RecordSourceLocator
|
||||
from govoplan_core.db.session import get_session
|
||||
from govoplan_records.backend.manifest import ADMIN_SCOPE, READ_SCOPE, WRITE_SCOPE
|
||||
from govoplan_records.backend.schemas import (
|
||||
FilePlanNodeWriteRequest,
|
||||
RecordCatalogResponse,
|
||||
RecordClassWriteRequest,
|
||||
RecordCreateRequest,
|
||||
RecordDetailResponse,
|
||||
RecordItemCreateRequest,
|
||||
RecordListResponse,
|
||||
RecordSourceProviderResponse,
|
||||
RecordUpdateRequest,
|
||||
RecordVolumeCreateRequest,
|
||||
)
|
||||
from govoplan_records.backend.service import (
|
||||
RecordConflictError,
|
||||
RecordNotFoundError,
|
||||
RecordSourceUnavailableError,
|
||||
RecordStoreError,
|
||||
SqlRecordRegistry,
|
||||
)
|
||||
|
||||
|
||||
def create_router(registry: object | None = None) -> APIRouter:
|
||||
router = APIRouter(prefix="/records", tags=["records"])
|
||||
records = SqlRecordRegistry(registry)
|
||||
|
||||
@router.get("/catalog", response_model=RecordCatalogResponse)
|
||||
def api_catalog(
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> RecordCatalogResponse:
|
||||
_require(principal, READ_SCOPE)
|
||||
return RecordCatalogResponse(**records.catalog(session, principal))
|
||||
|
||||
@router.post(
|
||||
"/catalog/file-plan",
|
||||
response_model=dict[str, Any],
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
def api_write_file_plan_node(
|
||||
payload: FilePlanNodeWriteRequest,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> dict[str, Any]:
|
||||
_require(principal, ADMIN_SCOPE)
|
||||
return _write(
|
||||
session,
|
||||
lambda: records.write_file_plan_node(
|
||||
session, principal, payload=payload.model_dump(mode="python")
|
||||
),
|
||||
)
|
||||
|
||||
@router.post(
|
||||
"/catalog/classes",
|
||||
response_model=dict[str, Any],
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
def api_write_record_class(
|
||||
payload: RecordClassWriteRequest,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> dict[str, Any]:
|
||||
_require(principal, ADMIN_SCOPE)
|
||||
return _write(
|
||||
session,
|
||||
lambda: records.write_record_class(
|
||||
session, principal, payload=payload.model_dump(mode="python")
|
||||
),
|
||||
)
|
||||
|
||||
@router.get("/sources", response_model=RecordSourceProviderResponse)
|
||||
def api_source_providers(
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> RecordSourceProviderResponse:
|
||||
_require(principal, WRITE_SCOPE)
|
||||
return RecordSourceProviderResponse(
|
||||
providers=records.source_providers(session, principal)
|
||||
)
|
||||
|
||||
@router.get("", response_model=RecordListResponse)
|
||||
def api_list_records(
|
||||
query: str | None = Query(default=None, max_length=500),
|
||||
record_state: str | None = Query(default=None, alias="state", max_length=40),
|
||||
class_id: str | None = Query(default=None, max_length=255),
|
||||
file_plan_node_id: str | None = Query(default=None, max_length=255),
|
||||
offset: int = Query(default=0, ge=0),
|
||||
limit: int = Query(default=100, ge=1, le=200),
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> RecordListResponse:
|
||||
_require(principal, READ_SCOPE)
|
||||
items, total = records.list_records(
|
||||
session,
|
||||
principal,
|
||||
query=query,
|
||||
state=record_state,
|
||||
class_id=class_id,
|
||||
file_plan_node_id=file_plan_node_id,
|
||||
offset=offset,
|
||||
limit=limit,
|
||||
)
|
||||
return RecordListResponse(
|
||||
records=items, total=total, offset=offset, limit=limit
|
||||
)
|
||||
|
||||
@router.post("", response_model=dict[str, Any], status_code=status.HTTP_201_CREATED)
|
||||
def api_create_record(
|
||||
payload: RecordCreateRequest,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> dict[str, Any]:
|
||||
_require(principal, WRITE_SCOPE)
|
||||
return _write(
|
||||
session,
|
||||
lambda: records.create_record(
|
||||
session, principal, payload=payload.model_dump(mode="python")
|
||||
),
|
||||
)
|
||||
|
||||
@router.get("/{record_id}", response_model=RecordDetailResponse)
|
||||
def api_get_record(
|
||||
record_id: str,
|
||||
revision: int | None = Query(default=None, ge=1),
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> RecordDetailResponse:
|
||||
_require(principal, READ_SCOPE)
|
||||
try:
|
||||
return RecordDetailResponse(
|
||||
**records.get_record(
|
||||
session, principal, record_id=record_id, revision=revision
|
||||
)
|
||||
)
|
||||
except RecordStoreError as exc:
|
||||
raise _http_error(exc) from exc
|
||||
|
||||
@router.patch("/{record_id}", response_model=dict[str, Any])
|
||||
def api_update_record(
|
||||
record_id: str,
|
||||
payload: RecordUpdateRequest,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> dict[str, Any]:
|
||||
_require(principal, WRITE_SCOPE)
|
||||
return _write(
|
||||
session,
|
||||
lambda: records.update_record(
|
||||
session,
|
||||
principal,
|
||||
record_id=record_id,
|
||||
payload=payload.model_dump(mode="python", exclude_unset=True),
|
||||
),
|
||||
)
|
||||
|
||||
@router.post(
|
||||
"/{record_id}/volumes",
|
||||
response_model=dict[str, Any],
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
def api_create_volume(
|
||||
record_id: str,
|
||||
payload: RecordVolumeCreateRequest,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> dict[str, Any]:
|
||||
_require(principal, WRITE_SCOPE)
|
||||
return _write(
|
||||
session,
|
||||
lambda: records.create_volume(
|
||||
session,
|
||||
principal,
|
||||
record_id=record_id,
|
||||
payload=payload.model_dump(mode="python"),
|
||||
),
|
||||
)
|
||||
|
||||
@router.post(
|
||||
"/{record_id}/items",
|
||||
response_model=dict[str, Any],
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
def api_file_item(
|
||||
record_id: str,
|
||||
payload: RecordItemCreateRequest,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> dict[str, Any]:
|
||||
_require(principal, WRITE_SCOPE)
|
||||
request = RecordFilingRequest(
|
||||
tenant_id=principal.tenant_id,
|
||||
record_id=record_id,
|
||||
source=RecordSourceLocator(
|
||||
tenant_id=principal.tenant_id,
|
||||
source_module=payload.source.source_module,
|
||||
resource_type=payload.source.resource_type,
|
||||
resource_id=payload.source.resource_id,
|
||||
source_revision=payload.source.source_revision,
|
||||
metadata=payload.source.metadata,
|
||||
),
|
||||
purpose=payload.purpose,
|
||||
filing_reason=payload.filing_reason,
|
||||
idempotency_key=payload.idempotency_key,
|
||||
volume_id=payload.volume_id,
|
||||
relationship=payload.relationship,
|
||||
institutional_context=payload.institutional_context,
|
||||
metadata=payload.metadata,
|
||||
)
|
||||
|
||||
def operation() -> dict[str, Any]:
|
||||
result = records.file(session, principal, request=request)
|
||||
return {
|
||||
"record_id": result.record_id,
|
||||
"item_id": result.item_id,
|
||||
"sequence": result.sequence,
|
||||
"filed_at": result.filed_at,
|
||||
"replayed": result.replayed,
|
||||
"source": {
|
||||
"source_module": result.source.locator.source_module,
|
||||
"resource_type": result.source.locator.resource_type,
|
||||
"resource_id": result.source.locator.resource_id,
|
||||
"source_revision": result.source.locator.source_revision,
|
||||
"label": result.source.label,
|
||||
},
|
||||
}
|
||||
|
||||
return _write(session, operation)
|
||||
|
||||
return router
|
||||
|
||||
|
||||
def _require(principal: ApiPrincipal, scope: str) -> None:
|
||||
if not has_scope(principal, scope):
|
||||
raise HTTPException(status_code=403, detail=f"Missing scope: {scope}")
|
||||
|
||||
|
||||
def _write(session: Session, operation):
|
||||
try:
|
||||
result = operation()
|
||||
session.commit()
|
||||
return result
|
||||
except (RecordStoreError, IntegrityError) as exc:
|
||||
session.rollback()
|
||||
if isinstance(exc, IntegrityError):
|
||||
raise HTTPException(
|
||||
status_code=409, detail="The record write conflicts with existing data."
|
||||
) from exc
|
||||
raise _http_error(exc) from exc
|
||||
|
||||
|
||||
def _http_error(exc: RecordStoreError) -> HTTPException:
|
||||
if isinstance(exc, RecordNotFoundError):
|
||||
code = 404
|
||||
elif isinstance(exc, RecordConflictError):
|
||||
code = 409
|
||||
elif isinstance(exc, RecordSourceUnavailableError):
|
||||
code = 503
|
||||
else:
|
||||
code = 422
|
||||
return HTTPException(status_code=code, detail=str(exc))
|
||||
|
||||
|
||||
__all__ = ["create_router"]
|
||||
Reference in New Issue
Block a user