refactor: retain workflow as optional editor

This commit is contained in:
2026-07-31 16:59:02 +02:00
parent 484ac43352
commit 8fd8753012
33 changed files with 450 additions and 11483 deletions
+2 -969
View File
@@ -1,970 +1,3 @@
from __future__ import annotations
"""Compatibility facade for the extracted Workflow Engine backend."""
from collections.abc import Mapping
from dataclasses import dataclass
import hashlib
import json
import re
import unicodedata
from sqlalchemy import or_, select
from sqlalchemy.orm import Session
from govoplan_core.auth import ApiPrincipal
from govoplan_core.db.base import utcnow
from govoplan_workflow.backend.db.models import (
WorkflowDefinition,
WorkflowDefinitionRevision,
)
from govoplan_workflow.backend.node_library import WORKFLOW_GRAPH_LIBRARY
from govoplan_workflow.backend.bpmn import (
BpmnDiagnostic,
BpmnInspectionError,
)
from govoplan_workflow.backend.bpmn_adapters import (
RuntimeKind,
bpmn_adapter_registry,
)
from govoplan_workflow.backend.bpmn_graph import (
BpmnGraphError,
NATIVE_BPMN_ADAPTER_ID,
NATIVE_BPMN_ADAPTER_VERSION,
canonical_bpmn_graph,
export_bpmn_graph,
import_bpmn_graph,
materialize_runtime_graph,
runtime_diagnostics,
)
from govoplan_workflow.backend.governance import (
definition_governance_payload,
require_definition_action,
)
from govoplan_workflow.backend.schemas import (
BpmnRevisionInput,
BpmnRevisionSummaryResponse,
WorkflowDefinitionCreateRequest,
WorkflowDefinitionDeriveRequest,
WorkflowDefinitionResponse,
WorkflowDefinitionRevisionResponse,
WorkflowDefinitionUpdateRequest,
WorkflowGraph,
)
from govoplan_workflow.backend.validation import validate_workflow_graph
class WorkflowError(RuntimeError):
pass
class WorkflowNotFoundError(WorkflowError):
pass
class WorkflowConflictError(WorkflowError):
pass
class WorkflowValidationError(WorkflowError):
def __init__(self, diagnostics: tuple[object, ...]) -> None:
first = diagnostics[0] if diagnostics else None
super().__init__(
str(getattr(first, "message", "Workflow definition validation failed."))
)
self.diagnostics = diagnostics
class WorkflowBpmnValidationError(WorkflowError):
def __init__(self, diagnostics: tuple[BpmnDiagnostic, ...]) -> None:
first = next(
(item for item in diagnostics if item.severity == "error"),
diagnostics[0] if diagnostics else None,
)
super().__init__(
first.message if first is not None else "BPMN validation failed."
)
self.diagnostics = diagnostics
@dataclass(frozen=True, slots=True)
class _BpmnArtifact:
xml: str
content_hash: str
adapter_id: str
adapter_version: str
runtime_kind: RuntimeKind
executable: bool
def list_definitions(
session: Session,
*,
tenant_id: str,
) -> list[WorkflowDefinition]:
return list(
session.scalars(
select(WorkflowDefinition)
.where(
or_(
WorkflowDefinition.tenant_id == tenant_id,
WorkflowDefinition.tenant_id.is_(None),
),
WorkflowDefinition.deleted_at.is_(None),
)
.order_by(
WorkflowDefinition.updated_at.desc(),
WorkflowDefinition.name.asc(),
)
)
)
def get_definition(
session: Session,
*,
tenant_id: str,
definition_id: str,
) -> WorkflowDefinition:
definition = session.scalar(
select(WorkflowDefinition).where(
WorkflowDefinition.id == definition_id,
or_(
WorkflowDefinition.tenant_id == tenant_id,
WorkflowDefinition.tenant_id.is_(None),
),
WorkflowDefinition.deleted_at.is_(None),
)
)
if definition is None:
raise WorkflowNotFoundError("Workflow definition not found.")
return definition
def get_definition_revision(
session: Session,
*,
definition: WorkflowDefinition,
revision: int | None = None,
) -> WorkflowDefinitionRevision:
revision_number = revision or definition.current_revision
item = session.scalar(
select(WorkflowDefinitionRevision).where(
WorkflowDefinitionRevision.definition_id == definition.id,
WorkflowDefinitionRevision.tenant_id == definition.tenant_id,
WorkflowDefinitionRevision.revision == revision_number,
)
)
if item is None:
raise WorkflowNotFoundError("Workflow definition revision not found.")
return item
def list_definition_revisions(
session: Session,
*,
definition: WorkflowDefinition,
) -> list[WorkflowDefinitionRevision]:
return list(
session.scalars(
select(WorkflowDefinitionRevision)
.where(
WorkflowDefinitionRevision.definition_id == definition.id,
WorkflowDefinitionRevision.tenant_id == definition.tenant_id,
)
.order_by(WorkflowDefinitionRevision.revision.desc())
)
)
def create_definition(
session: Session,
*,
tenant_id: str,
actor_id: str | None,
payload: WorkflowDefinitionCreateRequest,
) -> WorkflowDefinition:
graph, bpmn = _prepare_revision_content(
graph=payload.graph,
bpmn=payload.bpmn,
)
stored_tenant_id = (
None if payload.scope_type == "system" else tenant_id
)
scope_id = (
None
if payload.scope_type == "system"
else tenant_id
if payload.scope_type == "tenant"
else payload.scope_id
)
scope_key = (
"system"
if payload.scope_type == "system"
else f"{payload.scope_type}:{scope_id}"
)
definition = WorkflowDefinition(
tenant_id=stored_tenant_id,
scope_type=payload.scope_type,
scope_id=scope_id,
scope_key=scope_key,
definition_kind=payload.definition_kind,
inherit_to_lower_scopes=payload.inherit_to_lower_scopes,
allow_start=payload.allow_start,
allow_reuse=payload.allow_reuse,
allow_automation=payload.allow_automation,
definition_key=_available_key(
session,
scope_key=scope_key,
requested=payload.key,
name=payload.name,
),
name=payload.name.strip(),
description=_clean_optional(payload.description),
status="draft",
current_revision=1,
active_revision=None,
metadata_=dict(payload.metadata),
created_by=actor_id,
updated_by=actor_id,
)
definition.revisions.append(
_new_revision(
tenant_id=stored_tenant_id,
revision=1,
graph=graph,
bpmn=bpmn,
execution_mode=payload.execution_mode,
view_id=payload.view_id,
view_revision_id=payload.view_revision_id,
actor_id=actor_id,
)
)
session.add(definition)
session.flush()
return definition
def update_definition(
session: Session,
*,
tenant_id: str,
definition_id: str,
actor_id: str | None,
payload: WorkflowDefinitionUpdateRequest,
) -> WorkflowDefinition:
definition = get_definition(
session,
tenant_id=tenant_id,
definition_id=definition_id,
)
if payload.expected_revision != definition.current_revision:
raise WorkflowConflictError(
"Workflow definition changed on the server; "
f"expected revision {payload.expected_revision}, "
f"current revision is {definition.current_revision}."
)
if (
payload.scope_type != definition.scope_type
or payload.scope_id != definition.scope_id
and not (
definition.scope_type == "tenant"
and payload.scope_id in {None, definition.scope_id}
)
):
raise WorkflowConflictError(
"Definition scope is immutable; derive a scoped copy instead."
)
if payload.definition_kind != definition.definition_kind:
raise WorkflowConflictError(
"Definition kind is immutable; derive a flow or template instead."
)
current = get_definition_revision(session, definition=definition)
graph, bpmn = _prepare_revision_content(
graph=payload.graph,
bpmn=payload.bpmn,
)
definition.name = payload.name.strip()
definition.description = _clean_optional(payload.description)
definition.metadata_ = dict(payload.metadata)
ancestor_limits = _ancestor_governance_limits(
definition.derivation_provenance
)
definition.inherit_to_lower_scopes = (
payload.inherit_to_lower_scopes
and ancestor_limits["inherit_to_lower_scopes"]
)
definition.allow_start = (
payload.allow_start and ancestor_limits["allow_start"]
)
definition.allow_reuse = (
payload.allow_reuse and ancestor_limits["allow_reuse"]
)
definition.allow_automation = (
payload.allow_automation and ancestor_limits["allow_automation"]
)
definition.updated_by = actor_id
if not _revision_matches(
current,
graph=graph,
bpmn=bpmn,
execution_mode=payload.execution_mode,
view_id=payload.view_id,
view_revision_id=payload.view_revision_id,
):
definition.current_revision += 1
definition.revisions.append(
_new_revision(
tenant_id=definition.tenant_id,
revision=definition.current_revision,
graph=graph,
bpmn=bpmn,
execution_mode=payload.execution_mode,
view_id=payload.view_id,
view_revision_id=payload.view_revision_id,
actor_id=actor_id,
)
)
if definition.status == "active":
definition.status = "draft"
session.flush()
return definition
def derive_definition(
session: Session,
*,
tenant_id: str,
actor_id: str | None,
principal: ApiPrincipal,
registry: object | None,
source_definition_id: str,
payload: WorkflowDefinitionDeriveRequest,
) -> WorkflowDefinition:
source = get_definition(
session,
tenant_id=tenant_id,
definition_id=source_definition_id,
)
decision = require_definition_action(
source,
principal=principal,
registry=registry,
action="derive",
)
source_revision = get_definition_revision(
session,
definition=source,
revision=payload.source_revision,
)
stored_tenant_id = (
None if payload.scope_type == "system" else tenant_id
)
scope_id = (
None
if payload.scope_type == "system"
else tenant_id
if payload.scope_type == "tenant"
else payload.scope_id
)
scope_key = (
"system"
if payload.scope_type == "system"
else f"{payload.scope_type}:{scope_id}"
)
source_limits = _effective_governance_limits(
source,
decision_details=decision.details,
)
limits = {
"inherit_to_lower_scopes": (
source_limits["inherit_to_lower_scopes"]
and payload.inherit_to_lower_scopes
),
"allow_start": (
source_limits["allow_start"] and payload.allow_start
),
"allow_reuse": (
source_limits["allow_reuse"] and payload.allow_reuse
),
"allow_automation": (
source_limits["allow_automation"]
and payload.allow_automation
),
}
provenance = {
"source_ref": f"workflow-definition:{source.id}",
"source_scope": {
"scope_type": source.scope_type,
"scope_id": source.scope_id,
},
"source_definition_kind": source.definition_kind,
"source_revision": source_revision.revision,
"source_hash": source_revision.content_hash,
"source_effective_limits": limits,
"policy_decision": decision.to_dict(),
"derived_by": actor_id,
"derived_at": utcnow().isoformat(),
}
execution_mode = (
payload.execution_mode
if "execution_mode" in payload.model_fields_set
else source_revision.execution_mode
)
view_id = (
payload.view_id
if "view_id" in payload.model_fields_set
else source_revision.view_id
)
view_revision_id = (
payload.view_revision_id
if (
"view_revision_id" in payload.model_fields_set
or "view_id" in payload.model_fields_set
)
else source_revision.view_revision_id
)
if view_id is None:
view_revision_id = None
source_graph, source_bpmn = _prepare_revision_content(
graph=WorkflowGraph.model_validate(source_revision.graph),
bpmn=None,
)
definition = WorkflowDefinition(
tenant_id=stored_tenant_id,
scope_type=payload.scope_type,
scope_id=scope_id,
scope_key=scope_key,
definition_kind=payload.definition_kind,
inherit_to_lower_scopes=limits["inherit_to_lower_scopes"],
allow_start=limits["allow_start"],
allow_reuse=limits["allow_reuse"],
allow_automation=limits["allow_automation"],
derived_from_definition_id=source.id,
derived_from_revision=source_revision.revision,
derived_from_hash=source_revision.content_hash,
derivation_provenance=provenance,
definition_key=_available_key(
session,
scope_key=scope_key,
requested=payload.key,
name=payload.name,
),
name=payload.name.strip(),
description=_clean_optional(payload.description),
status="draft",
current_revision=1,
active_revision=None,
metadata_=dict(payload.metadata),
created_by=actor_id,
updated_by=actor_id,
)
definition.revisions.append(
WorkflowDefinitionRevision(
tenant_id=stored_tenant_id,
revision=1,
schema_version=source_revision.schema_version,
graph=_canonical_graph(source_graph),
content_hash=_content_hash(
source_graph,
bpmn=source_bpmn,
execution_mode=execution_mode,
view_id=view_id,
view_revision_id=view_revision_id,
),
library_id=source_revision.library_id,
library_version=source_revision.library_version,
execution_mode=execution_mode,
view_id=view_id,
view_revision_id=view_revision_id,
bpmn_xml=source_bpmn.xml,
bpmn_hash=source_bpmn.content_hash,
bpmn_adapter_id=source_bpmn.adapter_id,
bpmn_adapter_version=source_bpmn.adapter_version,
bpmn_runtime_kind=source_bpmn.runtime_kind,
bpmn_executable=source_bpmn.executable,
created_by=actor_id,
)
)
session.add(definition)
session.flush()
return definition
def activate_definition(
session: Session,
*,
tenant_id: str,
definition_id: str,
actor_id: str | None,
revision: int | None = None,
) -> WorkflowDefinition:
definition = get_definition(
session,
tenant_id=tenant_id,
definition_id=definition_id,
)
selected = get_definition_revision(
session,
definition=definition,
revision=revision,
)
if definition.definition_kind == "template":
raise WorkflowConflictError(
"Workflow templates cannot be activated or started."
)
_validated_graph(WorkflowGraph.model_validate(selected.graph))
_validate_bpmn_activation(selected)
_validate_execution_mode_activation(selected)
definition.active_revision = selected.revision
definition.status = "active"
definition.updated_by = actor_id
session.flush()
return definition
def archive_definition(
session: Session,
*,
tenant_id: str,
definition_id: str,
actor_id: str | None,
) -> WorkflowDefinition:
definition = get_definition(
session,
tenant_id=tenant_id,
definition_id=definition_id,
)
definition.status = "archived"
definition.updated_by = actor_id
session.flush()
return definition
def delete_definition(
session: Session,
*,
tenant_id: str,
definition_id: str,
actor_id: str | None,
) -> WorkflowDefinition:
definition = get_definition(
session,
tenant_id=tenant_id,
definition_id=definition_id,
)
definition.deleted_at = utcnow()
definition.updated_by = actor_id
session.flush()
return definition
def definition_response(
session: Session,
definition: WorkflowDefinition,
*,
principal: ApiPrincipal,
registry: object | None,
revision: int | None = None,
) -> WorkflowDefinitionResponse:
selected = get_definition_revision(
session,
definition=definition,
revision=revision,
)
return WorkflowDefinitionResponse(
id=definition.id,
tenant_id=definition.tenant_id,
key=definition.definition_key,
name=definition.name,
description=definition.description,
status=definition.status,
current_revision=definition.current_revision,
active_revision=definition.active_revision,
metadata=dict(definition.metadata_),
created_by=definition.created_by,
updated_by=definition.updated_by,
created_at=definition.created_at,
updated_at=definition.updated_at,
revision=revision_response(selected),
governance=definition_governance_payload(
definition,
principal=principal,
registry=registry,
),
)
def revision_response(
revision: WorkflowDefinitionRevision,
) -> WorkflowDefinitionRevisionResponse:
return WorkflowDefinitionRevisionResponse(
id=revision.id,
revision=revision.revision,
schema_version=revision.schema_version,
graph=canonical_bpmn_graph(
WorkflowGraph.model_validate(revision.graph)
),
content_hash=revision.content_hash,
library_id=revision.library_id,
library_version=revision.library_version,
execution_mode=revision.execution_mode,
view_id=revision.view_id,
view_revision_id=revision.view_revision_id,
bpmn=revision_bpmn_summary(revision),
created_by=revision.created_by,
created_at=revision.created_at,
)
def _new_revision(
*,
tenant_id: str | None,
revision: int,
graph: WorkflowGraph,
bpmn: _BpmnArtifact | None,
execution_mode: str,
view_id: str | None,
view_revision_id: str | None,
actor_id: str | None,
) -> WorkflowDefinitionRevision:
return WorkflowDefinitionRevision(
tenant_id=tenant_id,
revision=revision,
schema_version=graph.schema_version,
graph=_canonical_graph(graph),
content_hash=_content_hash(
graph,
bpmn=bpmn,
execution_mode=execution_mode,
view_id=view_id,
view_revision_id=view_revision_id,
),
library_id=WORKFLOW_GRAPH_LIBRARY.id,
library_version=WORKFLOW_GRAPH_LIBRARY.version,
execution_mode=execution_mode,
view_id=view_id,
view_revision_id=view_revision_id,
bpmn_xml=bpmn.xml if bpmn else None,
bpmn_hash=bpmn.content_hash if bpmn else None,
bpmn_adapter_id=bpmn.adapter_id if bpmn else None,
bpmn_adapter_version=bpmn.adapter_version if bpmn else None,
bpmn_runtime_kind=bpmn.runtime_kind if bpmn else None,
bpmn_executable=bpmn.executable if bpmn else None,
created_by=actor_id,
)
def revision_bpmn_summary(
revision: WorkflowDefinitionRevision,
) -> BpmnRevisionSummaryResponse | None:
if not revision.bpmn_xml:
return None
adapter_id = revision.bpmn_adapter_id or "bpmn.interchange"
adapter_version = revision.bpmn_adapter_version or "1.0.0"
adapter = bpmn_adapter_registry().resolve(adapter_id, adapter_version)
runtime_kind: RuntimeKind = "model_only"
if revision.bpmn_runtime_kind in {"model_only", "native_graph", "external"}:
runtime_kind = revision.bpmn_runtime_kind
return BpmnRevisionSummaryResponse(
content_hash=revision.bpmn_hash
or hashlib.sha256(revision.bpmn_xml.encode("utf-8")).hexdigest(),
adapter_id=adapter_id,
adapter_version=adapter_version,
runtime_kind=runtime_kind,
executable=bool(revision.bpmn_executable),
adapter_available=adapter is not None,
)
def _prepare_revision_content(
*,
graph: WorkflowGraph,
bpmn: BpmnRevisionInput | None,
) -> tuple[WorkflowGraph, _BpmnArtifact | None]:
try:
effective_graph = (
import_bpmn_graph(bpmn.xml)
if bpmn is not None
else canonical_bpmn_graph(graph)
)
except WorkflowBpmnValidationError:
raise
except (BpmnGraphError, BpmnInspectionError) as exc:
diagnostics = getattr(exc, "diagnostics", None)
raise WorkflowBpmnValidationError(
tuple(diagnostics)
if diagnostics
else (
BpmnDiagnostic(
severity="error",
code="bpmn.invalid",
message=str(exc),
),
)
) from exc
effective_graph = _validated_graph(effective_graph)
xml = export_bpmn_graph(effective_graph)
executable = not any(
item.severity == "error"
for item in runtime_diagnostics(effective_graph)
)
if executable:
try:
runtime_graph = materialize_runtime_graph(effective_graph)
except BpmnGraphError:
executable = False
else:
executable = not any(
item.severity == "error"
for item in validate_workflow_graph(runtime_graph)
)
return effective_graph, _BpmnArtifact(
xml=xml,
content_hash=hashlib.sha256(xml.encode("utf-8")).hexdigest(),
adapter_id=NATIVE_BPMN_ADAPTER_ID,
adapter_version=NATIVE_BPMN_ADAPTER_VERSION,
runtime_kind="native_graph",
executable=executable,
)
def _validate_bpmn_activation(
revision: WorkflowDefinitionRevision,
) -> None:
try:
runtime_graph = materialize_runtime_graph(
WorkflowGraph.model_validate(revision.graph)
)
except BpmnGraphError as exc:
diagnostics = getattr(exc, "diagnostics", None)
raise WorkflowBpmnValidationError(
tuple(diagnostics)
if diagnostics
else (
BpmnDiagnostic(
severity="error",
code="bpmn.activation_invalid",
message=str(exc),
),
)
) from exc
runtime_validation = validate_workflow_graph(runtime_graph)
if any(item.severity == "error" for item in runtime_validation):
raise WorkflowValidationError(runtime_validation)
def _validated_graph(graph: WorkflowGraph) -> WorkflowGraph:
try:
graph = canonical_bpmn_graph(graph)
except BpmnGraphError as exc:
raise WorkflowBpmnValidationError(exc.diagnostics) from exc
diagnostics = validate_workflow_graph(graph)
if any(item.severity == "error" for item in diagnostics):
raise WorkflowValidationError(diagnostics)
return graph
def _canonical_graph(graph: WorkflowGraph) -> dict[str, object]:
return graph.model_dump(mode="json")
def _content_hash(
graph: WorkflowGraph,
*,
bpmn: _BpmnArtifact | None = None,
execution_mode: str = "hybrid",
view_id: str | None = None,
view_revision_id: str | None = None,
) -> str:
content: dict[str, object] = {
"graph": _canonical_graph(graph),
"execution": {
"mode": execution_mode,
"view_id": view_id,
"view_revision_id": view_revision_id,
},
}
if bpmn is not None:
content["bpmn"] = {
"xml": bpmn.xml,
"adapter_id": bpmn.adapter_id,
"adapter_version": bpmn.adapter_version,
}
encoded = json.dumps(content, sort_keys=True, separators=(",", ":"))
return hashlib.sha256(encoded.encode("utf-8")).hexdigest()
def _revision_matches(
revision: WorkflowDefinitionRevision,
*,
graph: WorkflowGraph,
bpmn: _BpmnArtifact | None,
execution_mode: str,
view_id: str | None,
view_revision_id: str | None,
) -> bool:
return (
revision.graph == _canonical_graph(graph)
and revision.execution_mode == execution_mode
and revision.view_id == view_id
and revision.view_revision_id == view_revision_id
and revision.bpmn_xml == (bpmn.xml if bpmn else None)
and revision.bpmn_adapter_id
== (bpmn.adapter_id if bpmn else None)
and revision.bpmn_adapter_version
== (bpmn.adapter_version if bpmn else None)
)
def _validate_execution_mode_activation(
revision: WorkflowDefinitionRevision,
) -> None:
if revision.execution_mode != "automated":
return
try:
graph = materialize_runtime_graph(
WorkflowGraph.model_validate(revision.graph)
)
except BpmnGraphError as exc:
raise WorkflowBpmnValidationError(exc.diagnostics) from exc
human_nodes = [
node.id
for node in graph.nodes
if node.type in {"workflow.activity", "workflow.review"}
or (
node.type == "workflow.wait"
and str(node.config.get("mode") or "manual") == "manual"
)
or (
node.type == "workflow.capability"
and str(node.config.get("failure_policy") or "manual") == "manual"
)
or (
node.type == "workflow.dataflow"
and (
str(node.config.get("warning_policy") or "review") == "review"
or str(node.config.get("failure_policy") or "manual")
== "manual"
)
)
]
if human_nodes:
raise WorkflowConflictError(
"Automated workflows cannot activate with human handoff paths: "
+ ", ".join(sorted(human_nodes))
)
def _available_key(
session: Session,
*,
scope_key: str,
requested: str | None,
name: str,
) -> str:
base = _slug(requested or name)
candidate = base
suffix = 2
while session.scalar(
select(WorkflowDefinition.id).where(
WorkflowDefinition.scope_key == scope_key,
WorkflowDefinition.definition_key == candidate,
)
):
candidate = f"{base[: max(1, 120 - len(str(suffix)) - 1)]}-{suffix}"
suffix += 1
return candidate
def _slug(value: str) -> str:
normalized = unicodedata.normalize("NFKD", value)
ascii_value = normalized.encode("ascii", "ignore").decode("ascii").lower()
cleaned = re.sub(r"[^a-z0-9]+", "-", ascii_value).strip("-")
return (cleaned or "workflow")[:120]
def _clean_optional(value: str | None) -> str | None:
if value is None:
return None
cleaned = value.strip()
return cleaned or None
def _ancestor_governance_limits(
provenance: Mapping[str, object],
) -> dict[str, bool]:
raw = provenance.get("source_effective_limits")
limits = raw if isinstance(raw, Mapping) else {}
return {
key: value if isinstance((value := limits.get(key)), bool) else True
for key in (
"inherit_to_lower_scopes",
"allow_start",
"allow_reuse",
"allow_automation",
)
}
def _effective_governance_limits(
definition: WorkflowDefinition,
*,
decision_details: Mapping[str, object] | None = None,
) -> dict[str, bool]:
ancestor = _ancestor_governance_limits(
definition.derivation_provenance
)
effective = {
"inherit_to_lower_scopes": (
definition.inherit_to_lower_scopes
and ancestor["inherit_to_lower_scopes"]
),
"allow_start": (
definition.allow_start and ancestor["allow_start"]
),
"allow_reuse": (
definition.allow_reuse and ancestor["allow_reuse"]
),
"allow_automation": (
definition.allow_automation
and ancestor["allow_automation"]
),
}
policy_limits = (
decision_details.get("effective_limits")
if decision_details is not None
else None
)
if isinstance(policy_limits, Mapping):
policy_key_by_local_key = {
"inherit_to_lower_scopes": "inherit_to_lower_scopes",
"allow_start": "allow_run",
"allow_reuse": "allow_reuse",
"allow_automation": "allow_automation",
}
for local_key, policy_key in policy_key_by_local_key.items():
value = policy_limits.get(policy_key)
if isinstance(value, bool):
effective[local_key] = effective[local_key] and value
return effective
__all__ = [
"WorkflowBpmnValidationError",
"WorkflowConflictError",
"WorkflowError",
"WorkflowNotFoundError",
"WorkflowValidationError",
"activate_definition",
"archive_definition",
"create_definition",
"derive_definition",
"definition_response",
"delete_definition",
"get_definition",
"get_definition_revision",
"list_definition_revisions",
"list_definitions",
"revision_response",
"revision_bpmn_summary",
"update_definition",
]
from govoplan_workflow_engine.backend.service import * # noqa: F401,F403