feat: implement governed datasource catalogue metadata

This commit is contained in:
2026-08-01 17:48:28 +02:00
parent 8da8035693
commit 98d93e707f
14 changed files with 1382 additions and 12 deletions
@@ -9,6 +9,7 @@ from govoplan_core.core.datasources import (
DatasourceAccessError,
DatasourceDescriptor,
DatasourceError,
DatasourceGovernance,
DatasourceMaterialization,
DatasourceNotFoundError,
DatasourceOrigin,
@@ -23,6 +24,8 @@ from govoplan_datasources.backend.runtime import get_registry
from govoplan_datasources.backend.schemas import (
DatasourceFieldResponse,
DatasourceFreezeRequest,
DatasourceGovernancePayload,
DatasourceGovernanceUpdateRequest,
DatasourceListResponse,
DatasourceMaterializationListResponse,
DatasourceMaterializationResponse,
@@ -128,6 +131,7 @@ def api_register_origin(
source_name=payload.source_name,
mode=payload.mode,
description=payload.description,
governance=_governance(payload.governance),
)
except DatasourceError as exc:
raise _http_error(exc) from exc
@@ -204,6 +208,7 @@ def api_create_stage(
"source_format": payload.format,
},
metadata=payload.metadata,
governance=_governance(payload.governance),
),
)
except DatasourceError as exc:
@@ -270,6 +275,13 @@ def api_promote_stage(
def api_list_datasources(
query: str = Query(default="", max_length=200),
limit: int = Query(default=100, ge=1, le=100),
authority_mode: str | None = Query(default=None, max_length=40),
classification: str | None = Query(default=None, max_length=80),
publication_state: str | None = Query(default=None, max_length=50),
owner_ref: str | None = Query(default=None, max_length=500),
responsible_organization_ref: str | None = Query(default=None, max_length=500),
affected_ref: str | None = Query(default=None, max_length=1000),
dependency_ref: str | None = Query(default=None, max_length=1000),
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> DatasourceListResponse:
@@ -280,6 +292,13 @@ def api_list_datasources(
principal,
query=query,
limit=limit,
authority_mode=authority_mode,
classification=classification,
publication_state=publication_state,
owner_ref=owner_ref,
responsible_organization_ref=responsible_organization_ref,
affected_ref=affected_ref,
dependency_ref=dependency_ref,
)
except DatasourceError as exc:
raise _http_error(exc) from exc
@@ -311,6 +330,39 @@ def api_get_datasource(
return _datasource_response(item)
@router.patch(
"/{datasource_id}/governance",
response_model=DatasourceResponse,
)
def api_update_datasource_governance(
datasource_id: str,
payload: DatasourceGovernanceUpdateRequest,
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> DatasourceResponse:
_require_any_scope(principal, SOURCE_WRITE_SCOPE, ADMIN_SCOPE)
try:
item = _provider().update_datasource_governance(
session,
principal,
datasource_ref=f"datasource:{datasource_id}",
governance=_governance(payload.governance)
or DatasourceGovernance(),
)
except DatasourceError as exc:
raise _http_error(exc) from exc
_audit(
session,
principal,
action="datasources.governance.updated",
object_type="datasource",
object_id=item.ref,
details={"governance": item.governance.to_dict()},
)
session.commit()
return _datasource_response(item)
@router.get(
"/{datasource_id}/preview",
response_model=DatasourcePreviewResponse,
@@ -509,6 +561,7 @@ def _datasource_response(item: DatasourceDescriptor) -> DatasourceResponse:
capabilities=list(item.capabilities),
provenance=dict(item.provenance),
metadata=dict(item.metadata),
governance=item.governance.to_dict(),
)
@@ -539,6 +592,7 @@ def _materialization_response(
created_at=item.created_at.isoformat() if item.created_at else None,
provenance=dict(item.provenance),
metadata=dict(item.metadata),
governance=item.governance.to_dict(),
)
@@ -569,6 +623,7 @@ def _stage_response(item: DatasourceStage) -> DatasourceStageResponse:
promoted_materialization_ref=item.promoted_materialization_ref,
provenance=dict(item.provenance),
metadata=dict(item.metadata),
governance=item.governance.to_dict(),
)
@@ -600,6 +655,14 @@ def _origin_response(item: DatasourceOrigin) -> DatasourceOriginResponse:
)
def _governance(
payload: DatasourceGovernancePayload | None,
) -> DatasourceGovernance | None:
if payload is None:
return None
return DatasourceGovernance.from_mapping(payload.model_dump())
def _audit(
session: Session,
principal: ApiPrincipal,