Initialize governed Dataflow module
This commit is contained in:
264
src/govoplan_dataflow/backend/router.py
Normal file
264
src/govoplan_dataflow/backend/router.py
Normal file
@@ -0,0 +1,264 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from govoplan_core.audit.logging import audit_event
|
||||
from govoplan_core.auth import ApiPrincipal, get_api_principal, has_scope
|
||||
from govoplan_core.db.session import get_session
|
||||
from govoplan_dataflow.backend.manifest import ADMIN_SCOPE, READ_SCOPE, RUN_SCOPE, WRITE_SCOPE
|
||||
from govoplan_dataflow.backend.schemas import (
|
||||
PipelineCreateRequest,
|
||||
PipelineDeleteResponse,
|
||||
PipelineDraftRequest,
|
||||
PipelineListResponse,
|
||||
PipelinePreviewRequest,
|
||||
PipelinePreviewResponse,
|
||||
PipelineResponse,
|
||||
PipelineSqlResponse,
|
||||
PipelineUpdateRequest,
|
||||
PipelineValidationResponse,
|
||||
)
|
||||
from govoplan_dataflow.backend.service import (
|
||||
DataflowConflictError,
|
||||
DataflowError,
|
||||
DataflowNotFoundError,
|
||||
DataflowValidationError,
|
||||
compile_sql_draft,
|
||||
create_pipeline,
|
||||
delete_pipeline,
|
||||
get_pipeline,
|
||||
list_pipelines,
|
||||
pipeline_response,
|
||||
preview_pipeline,
|
||||
render_graph_sql,
|
||||
update_pipeline,
|
||||
validate_draft,
|
||||
)
|
||||
|
||||
|
||||
router = APIRouter(prefix="/dataflow", tags=["dataflow"])
|
||||
|
||||
|
||||
def _require_any_scope(principal: ApiPrincipal, *scopes: str) -> None:
|
||||
if any(has_scope(principal, scope) for scope in scopes):
|
||||
return
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=f"Missing one of the required scopes: {', '.join(scopes)}",
|
||||
)
|
||||
|
||||
|
||||
def _actor_id(principal: ApiPrincipal) -> str | None:
|
||||
return (
|
||||
principal.account_id
|
||||
or principal.membership_id
|
||||
or getattr(principal.user, "id", None)
|
||||
or principal.identity_id
|
||||
)
|
||||
|
||||
|
||||
def _http_error(exc: DataflowError) -> HTTPException:
|
||||
if isinstance(exc, DataflowNotFoundError):
|
||||
return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc))
|
||||
if isinstance(exc, DataflowConflictError):
|
||||
return HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc))
|
||||
if isinstance(exc, DataflowValidationError):
|
||||
return HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
||||
detail={
|
||||
"message": str(exc),
|
||||
"diagnostics": [item.model_dump(mode="json") for item in exc.diagnostics],
|
||||
},
|
||||
)
|
||||
return HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc))
|
||||
|
||||
|
||||
@router.get("/pipelines", response_model=PipelineListResponse)
|
||||
def api_list_pipelines(
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> PipelineListResponse:
|
||||
_require_any_scope(principal, READ_SCOPE, ADMIN_SCOPE)
|
||||
pipelines = list_pipelines(session, tenant_id=principal.tenant_id)
|
||||
return PipelineListResponse(
|
||||
pipelines=[pipeline_response(session, pipeline) for pipeline in pipelines]
|
||||
)
|
||||
|
||||
|
||||
@router.post("/pipelines", response_model=PipelineResponse, status_code=status.HTTP_201_CREATED)
|
||||
def api_create_pipeline(
|
||||
payload: PipelineCreateRequest,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> PipelineResponse:
|
||||
_require_any_scope(principal, WRITE_SCOPE, ADMIN_SCOPE)
|
||||
try:
|
||||
pipeline = create_pipeline(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
actor_id=_actor_id(principal),
|
||||
payload=payload,
|
||||
)
|
||||
except DataflowError as exc:
|
||||
raise _http_error(exc) from exc
|
||||
audit_event(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
user_id=getattr(principal.user, "id", None),
|
||||
api_key_id=principal.api_key_id,
|
||||
action="dataflow.pipeline.created",
|
||||
object_type="dataflow_pipeline",
|
||||
object_id=pipeline.id,
|
||||
details={"revision": pipeline.current_revision, "status": pipeline.status},
|
||||
)
|
||||
response = pipeline_response(session, pipeline)
|
||||
session.commit()
|
||||
return response
|
||||
|
||||
|
||||
@router.get("/pipelines/{pipeline_id}", response_model=PipelineResponse)
|
||||
def api_get_pipeline(
|
||||
pipeline_id: str,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> PipelineResponse:
|
||||
_require_any_scope(principal, READ_SCOPE, ADMIN_SCOPE)
|
||||
try:
|
||||
pipeline = get_pipeline(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
pipeline_id=pipeline_id,
|
||||
)
|
||||
return pipeline_response(session, pipeline)
|
||||
except DataflowError as exc:
|
||||
raise _http_error(exc) from exc
|
||||
|
||||
|
||||
@router.put("/pipelines/{pipeline_id}", response_model=PipelineResponse)
|
||||
def api_update_pipeline(
|
||||
pipeline_id: str,
|
||||
payload: PipelineUpdateRequest,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> PipelineResponse:
|
||||
_require_any_scope(principal, WRITE_SCOPE, ADMIN_SCOPE)
|
||||
try:
|
||||
pipeline = update_pipeline(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
pipeline_id=pipeline_id,
|
||||
actor_id=_actor_id(principal),
|
||||
payload=payload,
|
||||
)
|
||||
except DataflowError as exc:
|
||||
raise _http_error(exc) from exc
|
||||
audit_event(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
user_id=getattr(principal.user, "id", None),
|
||||
api_key_id=principal.api_key_id,
|
||||
action="dataflow.pipeline.updated",
|
||||
object_type="dataflow_pipeline",
|
||||
object_id=pipeline.id,
|
||||
details={"revision": pipeline.current_revision, "status": pipeline.status},
|
||||
)
|
||||
response = pipeline_response(session, pipeline)
|
||||
session.commit()
|
||||
return response
|
||||
|
||||
|
||||
@router.delete("/pipelines/{pipeline_id}", response_model=PipelineDeleteResponse)
|
||||
def api_delete_pipeline(
|
||||
pipeline_id: str,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> PipelineDeleteResponse:
|
||||
_require_any_scope(principal, WRITE_SCOPE, ADMIN_SCOPE)
|
||||
try:
|
||||
pipeline = delete_pipeline(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
pipeline_id=pipeline_id,
|
||||
actor_id=_actor_id(principal),
|
||||
)
|
||||
except DataflowError as exc:
|
||||
raise _http_error(exc) from exc
|
||||
audit_event(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
user_id=getattr(principal.user, "id", None),
|
||||
api_key_id=principal.api_key_id,
|
||||
action="dataflow.pipeline.deleted",
|
||||
object_type="dataflow_pipeline",
|
||||
object_id=pipeline.id,
|
||||
details={"revision": pipeline.current_revision},
|
||||
)
|
||||
session.commit()
|
||||
return PipelineDeleteResponse(deleted=True, pipeline_id=pipeline.id)
|
||||
|
||||
|
||||
@router.post("/validate", response_model=PipelineValidationResponse)
|
||||
def api_validate_pipeline(
|
||||
payload: PipelineDraftRequest,
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> PipelineValidationResponse:
|
||||
_require_any_scope(principal, READ_SCOPE, WRITE_SCOPE, RUN_SCOPE, ADMIN_SCOPE)
|
||||
return validate_draft(payload)
|
||||
|
||||
|
||||
@router.post("/sql/compile", response_model=PipelineSqlResponse)
|
||||
def api_compile_pipeline_sql(
|
||||
payload: PipelineDraftRequest,
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> PipelineSqlResponse:
|
||||
_require_any_scope(principal, READ_SCOPE, WRITE_SCOPE, RUN_SCOPE, ADMIN_SCOPE)
|
||||
return compile_sql_draft(payload)
|
||||
|
||||
|
||||
@router.post("/sql/render", response_model=PipelineSqlResponse)
|
||||
def api_render_pipeline_sql(
|
||||
payload: PipelineDraftRequest,
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> PipelineSqlResponse:
|
||||
_require_any_scope(principal, READ_SCOPE, WRITE_SCOPE, RUN_SCOPE, ADMIN_SCOPE)
|
||||
return render_graph_sql(payload)
|
||||
|
||||
|
||||
@router.post("/preview", response_model=PipelinePreviewResponse)
|
||||
def api_preview_pipeline(
|
||||
payload: PipelinePreviewRequest,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> PipelinePreviewResponse:
|
||||
_require_any_scope(principal, RUN_SCOPE, ADMIN_SCOPE)
|
||||
try:
|
||||
response = preview_pipeline(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
actor_id=_actor_id(principal),
|
||||
payload=payload,
|
||||
)
|
||||
except DataflowError as exc:
|
||||
raise _http_error(exc) from exc
|
||||
if response.pipeline_id:
|
||||
audit_event(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
user_id=getattr(principal.user, "id", None),
|
||||
api_key_id=principal.api_key_id,
|
||||
action="dataflow.pipeline.previewed",
|
||||
object_type="dataflow_pipeline",
|
||||
object_id=response.pipeline_id,
|
||||
details={
|
||||
"revision": response.revision,
|
||||
"run_id": response.run_id,
|
||||
"status": response.status,
|
||||
"output_row_count": response.total_rows,
|
||||
},
|
||||
)
|
||||
session.commit()
|
||||
return response
|
||||
|
||||
|
||||
__all__ = ["router"]
|
||||
Reference in New Issue
Block a user