feat: govern connector configurations and simulations

This commit is contained in:
2026-08-20 12:58:53 +02:00
parent 3964a2e4e7
commit 0f61fe4607
18 changed files with 3309 additions and 14 deletions
+237
View File
@@ -48,6 +48,30 @@ from govoplan_connectors.backend.schemas import (
TabularSourceResponse,
)
from govoplan_connectors.backend.feeds import ConnectorFeedProvider, feed_rows
from govoplan_connectors.backend.governed_runtime import (
GovernedConnectorError,
create_configuration,
execute_run,
list_configurations,
list_definitions,
list_runs,
review_run,
update_configuration,
upsert_definition,
)
from govoplan_connectors.backend.governed_schemas import (
ConnectorConfigurationCreateRequest,
ConnectorConfigurationItem,
ConnectorConfigurationListResponse,
ConnectorConfigurationUpdateRequest,
ConnectorDefinitionItem,
ConnectorDefinitionListResponse,
ConnectorDefinitionUpsertRequest,
ConnectorReviewRequest,
ConnectorRunItem,
ConnectorRunListResponse,
ConnectorRunRequest,
)
from govoplan_connectors.backend.recovery import (
ConnectorRecoveryError,
begin_connector_read_snapshot,
@@ -135,6 +159,26 @@ def _recovery_http_error(exc: ConnectorRecoveryError) -> HTTPException:
)
def _governed_http_error(exc: GovernedConnectorError) -> HTTPException:
if exc.code.endswith("_not_found"):
status_code = status.HTTP_404_NOT_FOUND
elif exc.code in {
"configuration_conflict",
"configuration_disabled",
"idempotency_conflict",
"local_definition_protected",
"package_definition_requires_overrides",
"run_not_reviewable",
}:
status_code = status.HTTP_409_CONFLICT
else:
status_code = status.HTTP_422_UNPROCESSABLE_CONTENT
return HTTPException(
status_code=status_code,
detail={"code": exc.code, "message": str(exc)},
)
@router.post("/feeds/preview", response_model=FeedDocumentResponse)
def api_preview_feed(
payload: FeedAcquireRequest,
@@ -609,6 +653,199 @@ def api_list_sanctions_runs(
)
@router.get(
"/governed/definitions",
response_model=ConnectorDefinitionListResponse,
)
def api_list_governed_definitions(
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> ConnectorDefinitionListResponse:
_require_any_scope(principal, READ_SCOPE, ADMIN_SCOPE)
try:
items = list_definitions(session, tenant_id=principal.tenant_id)
except GovernedConnectorError as exc:
raise _governed_http_error(exc) from exc
return ConnectorDefinitionListResponse(items=items)
@router.post(
"/governed/definitions",
response_model=ConnectorDefinitionItem,
)
def api_upsert_governed_definition(
payload: ConnectorDefinitionUpsertRequest,
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> ConnectorDefinitionItem:
_require_any_scope(principal, ADMIN_SCOPE)
try:
return upsert_definition(session, principal, payload)
except GovernedConnectorError as exc:
session.rollback()
raise _governed_http_error(exc) from exc
@router.get(
"/governed/configurations",
response_model=ConnectorConfigurationListResponse,
)
def api_list_governed_configurations(
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> ConnectorConfigurationListResponse:
_require_any_scope(principal, READ_SCOPE, ADMIN_SCOPE)
try:
items = list_configurations(session, tenant_id=principal.tenant_id)
except GovernedConnectorError as exc:
raise _governed_http_error(exc) from exc
return ConnectorConfigurationListResponse(items=items)
@router.post(
"/governed/configurations",
response_model=ConnectorConfigurationItem,
status_code=status.HTTP_201_CREATED,
)
def api_create_governed_configuration(
payload: ConnectorConfigurationCreateRequest,
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> ConnectorConfigurationItem:
_require_any_scope(principal, ADMIN_SCOPE)
try:
return create_configuration(session, principal, payload)
except GovernedConnectorError as exc:
session.rollback()
raise _governed_http_error(exc) from exc
@router.put(
"/governed/configurations/{configuration_id}",
response_model=ConnectorConfigurationItem,
)
def api_update_governed_configuration(
configuration_id: str,
payload: ConnectorConfigurationUpdateRequest,
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> ConnectorConfigurationItem:
_require_any_scope(principal, ADMIN_SCOPE)
try:
return update_configuration(
session,
principal,
configuration_id=configuration_id,
payload=payload,
)
except GovernedConnectorError as exc:
session.rollback()
raise _governed_http_error(exc) from exc
def _api_execute_governed_run(
*,
configuration_id: str,
mode: str,
payload: ConnectorRunRequest,
session: Session,
principal: ApiPrincipal,
) -> ConnectorRunItem:
_require_any_scope(principal, WRITE_SCOPE, ADMIN_SCOPE)
try:
return execute_run(
session,
principal,
configuration_id=configuration_id,
mode=mode,
payload=payload,
)
except GovernedConnectorError as exc:
session.rollback()
raise _governed_http_error(exc) from exc
@router.post(
"/governed/configurations/{configuration_id}/dry-runs",
response_model=ConnectorRunItem,
)
def api_dry_run_governed_configuration(
configuration_id: str,
payload: ConnectorRunRequest,
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> ConnectorRunItem:
return _api_execute_governed_run(
configuration_id=configuration_id,
mode="dry_run",
payload=payload,
session=session,
principal=principal,
)
@router.post(
"/governed/configurations/{configuration_id}/simulations",
response_model=ConnectorRunItem,
)
def api_simulate_governed_configuration(
configuration_id: str,
payload: ConnectorRunRequest,
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> ConnectorRunItem:
return _api_execute_governed_run(
configuration_id=configuration_id,
mode="simulation",
payload=payload,
session=session,
principal=principal,
)
@router.get(
"/governed/runs",
response_model=ConnectorRunListResponse,
)
def api_list_governed_runs(
configuration_id: str | None = Query(default=None),
review_state: str | None = Query(default=None),
limit: int = Query(default=100, ge=1, le=500),
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> ConnectorRunListResponse:
_require_any_scope(principal, READ_SCOPE, ADMIN_SCOPE)
try:
items = list_runs(
session,
tenant_id=principal.tenant_id,
configuration_id=configuration_id,
review_state=review_state,
limit=limit,
)
except GovernedConnectorError as exc:
raise _governed_http_error(exc) from exc
return ConnectorRunListResponse(items=items)
@router.post(
"/governed/runs/{run_id}/review",
response_model=ConnectorRunItem,
)
def api_review_governed_run(
run_id: str,
payload: ConnectorReviewRequest,
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> ConnectorRunItem:
_require_any_scope(principal, ADMIN_SCOPE)
try:
return review_run(session, principal, run_id=run_id, payload=payload)
except GovernedConnectorError as exc:
session.rollback()
raise _governed_http_error(exc) from exc
def _source_response(source: TabularSource) -> TabularSourceResponse:
return TabularSourceResponse(
ref=source.ref,