Install and reconcile module workflow standards
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from sqlalchemy import case, or_, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from govoplan_core.auth import ApiPrincipal
|
||||
from govoplan_core.core.workflows import (
|
||||
WorkflowCurrentStepResolution,
|
||||
WorkflowInstanceRef,
|
||||
WorkflowStandardStartRequest,
|
||||
)
|
||||
from govoplan_workflow_engine.backend.db.models import (
|
||||
WorkflowDefinition,
|
||||
WorkflowDefinitionRevision,
|
||||
WorkflowInstance,
|
||||
WorkflowInstanceStep,
|
||||
)
|
||||
from govoplan_workflow_engine.backend.instance_service import (
|
||||
get_instance as get_workflow_instance,
|
||||
resolve_step,
|
||||
start_instance,
|
||||
)
|
||||
from govoplan_workflow_engine.backend.schemas import (
|
||||
WorkflowInstanceStartRequest,
|
||||
WorkflowStepActionRequest,
|
||||
)
|
||||
|
||||
|
||||
class SqlWorkflowOrchestrationProvider:
|
||||
def __init__(self, *, registry: object | None = None) -> None:
|
||||
self._registry = registry
|
||||
|
||||
def start_standard(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
request: WorkflowStandardStartRequest,
|
||||
) -> WorkflowInstanceRef:
|
||||
sql_session = _session(session)
|
||||
api_principal = _principal(principal)
|
||||
definition = _active_standard(
|
||||
sql_session,
|
||||
tenant_id=request.tenant_id,
|
||||
origin_module_id=request.origin_module_id,
|
||||
definition_key=request.definition_key,
|
||||
)
|
||||
instance, replayed = start_instance(
|
||||
sql_session,
|
||||
tenant_id=request.tenant_id,
|
||||
definition_id=definition.id,
|
||||
actor_id=request.actor_id,
|
||||
principal=api_principal,
|
||||
registry=self._registry,
|
||||
payload=WorkflowInstanceStartRequest(
|
||||
idempotency_key=request.idempotency_key,
|
||||
input=dict(request.input),
|
||||
correlation_id=request.correlation_id,
|
||||
),
|
||||
start_origin=request.start_origin,
|
||||
)
|
||||
return _instance_ref(sql_session, instance, replayed=replayed)
|
||||
|
||||
def resolve_current_step(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
tenant_id: str,
|
||||
instance_id: str,
|
||||
resolution: WorkflowCurrentStepResolution,
|
||||
) -> WorkflowInstanceRef:
|
||||
sql_session = _session(session)
|
||||
api_principal = _principal(principal)
|
||||
instance = get_workflow_instance(
|
||||
sql_session,
|
||||
tenant_id=tenant_id,
|
||||
instance_id=instance_id,
|
||||
)
|
||||
step_id = instance.current_step_id
|
||||
if step_id is None:
|
||||
raise ValueError("Workflow instance has no current waiting step.")
|
||||
if (
|
||||
resolution.expected_step_id is not None
|
||||
and resolution.expected_step_id != step_id
|
||||
):
|
||||
raise ValueError("Workflow current step changed; reload and retry.")
|
||||
resolved = resolve_step(
|
||||
sql_session,
|
||||
tenant_id=tenant_id,
|
||||
instance_id=instance_id,
|
||||
step_id=step_id,
|
||||
actor_id=resolution.actor_id,
|
||||
principal=api_principal,
|
||||
registry=self._registry,
|
||||
payload=WorkflowStepActionRequest(
|
||||
action=resolution.action, # type: ignore[arg-type]
|
||||
output=dict(resolution.output),
|
||||
evidence=list(resolution.evidence),
|
||||
comment=resolution.comment,
|
||||
),
|
||||
)
|
||||
return _instance_ref(sql_session, resolved)
|
||||
|
||||
def get_instance(
|
||||
self,
|
||||
session: object,
|
||||
*,
|
||||
tenant_id: str,
|
||||
instance_id: str,
|
||||
) -> WorkflowInstanceRef:
|
||||
sql_session = _session(session)
|
||||
instance = get_workflow_instance(
|
||||
sql_session,
|
||||
tenant_id=tenant_id,
|
||||
instance_id=instance_id,
|
||||
)
|
||||
return _instance_ref(sql_session, instance)
|
||||
|
||||
|
||||
def _active_standard(
|
||||
session: Session,
|
||||
*,
|
||||
tenant_id: str,
|
||||
origin_module_id: str,
|
||||
definition_key: str,
|
||||
) -> WorkflowDefinition:
|
||||
definition = session.scalar(
|
||||
select(WorkflowDefinition)
|
||||
.where(
|
||||
or_(
|
||||
WorkflowDefinition.tenant_id == tenant_id,
|
||||
WorkflowDefinition.tenant_id.is_(None),
|
||||
),
|
||||
WorkflowDefinition.standard_origin_module_id == origin_module_id,
|
||||
WorkflowDefinition.standard_definition_key == definition_key,
|
||||
WorkflowDefinition.status == "active",
|
||||
WorkflowDefinition.active_revision.is_not(None),
|
||||
WorkflowDefinition.deleted_at.is_(None),
|
||||
)
|
||||
.order_by(
|
||||
case(
|
||||
(WorkflowDefinition.tenant_id == tenant_id, 0),
|
||||
else_=1,
|
||||
),
|
||||
WorkflowDefinition.updated_at.desc(),
|
||||
)
|
||||
)
|
||||
if definition is None:
|
||||
raise ValueError(
|
||||
f"Active Workflow standard is unavailable: "
|
||||
f"{origin_module_id}/{definition_key}."
|
||||
)
|
||||
return definition
|
||||
|
||||
|
||||
def _instance_ref(
|
||||
session: Session,
|
||||
instance: WorkflowInstance,
|
||||
*,
|
||||
replayed: bool = False,
|
||||
) -> WorkflowInstanceRef:
|
||||
revision = session.get(
|
||||
WorkflowDefinitionRevision,
|
||||
instance.definition_revision_id,
|
||||
)
|
||||
if revision is None:
|
||||
raise ValueError("Workflow instance revision is unavailable.")
|
||||
step = (
|
||||
session.get(WorkflowInstanceStep, instance.current_step_id)
|
||||
if instance.current_step_id
|
||||
else None
|
||||
)
|
||||
return WorkflowInstanceRef(
|
||||
id=instance.id,
|
||||
tenant_id=instance.tenant_id,
|
||||
definition_id=instance.definition_id,
|
||||
definition_revision_id=instance.definition_revision_id,
|
||||
definition_revision=revision.revision,
|
||||
definition_hash=revision.content_hash,
|
||||
status=instance.status,
|
||||
current_step_id=instance.current_step_id,
|
||||
current_node_id=step.node_id if step is not None else None,
|
||||
replayed=replayed,
|
||||
)
|
||||
|
||||
|
||||
def _session(value: object) -> Session:
|
||||
if not isinstance(value, Session):
|
||||
raise TypeError("Workflow orchestration requires a SQLAlchemy Session.")
|
||||
return value
|
||||
|
||||
|
||||
def _principal(value: object) -> ApiPrincipal:
|
||||
if not isinstance(value, ApiPrincipal):
|
||||
raise TypeError("Workflow orchestration requires an API principal.")
|
||||
return value
|
||||
|
||||
|
||||
__all__ = ["SqlWorkflowOrchestrationProvider"]
|
||||
Reference in New Issue
Block a user