Implement native BPMN workflows and guided modes

This commit is contained in:
2026-07-31 02:48:57 +02:00
parent c505e81006
commit f4974b4949
40 changed files with 8203 additions and 489 deletions
+59 -24
View File
@@ -3,7 +3,7 @@ from __future__ import annotations
from collections import Counter
from dataclasses import dataclass
from typing import Literal
from xml.etree.ElementTree import ParseError
from xml.etree.ElementTree import Element, ParseError
from defusedxml.ElementTree import fromstring
from defusedxml.common import DefusedXmlException
@@ -27,33 +27,45 @@ NATIVE_EXECUTION_ELEMENTS = frozenset(
{
"startEvent",
"endEvent",
"task",
"manualTask",
"userTask",
"serviceTask",
"businessRuleTask",
"receiveTask",
"sendTask",
"exclusiveGateway",
"parallelGateway",
"sequenceFlow",
"receiveTask",
"intermediateCatchEvent",
"intermediateThrowEvent",
"sequenceFlow",
}
)
NATIVE_MAPPING_ELEMENTS = NATIVE_EXECUTION_ELEMENTS | frozenset(
{
"task",
"definitions",
"process",
"collaboration",
"documentation",
"extensionElements",
"incoming",
"outgoing",
"conditionExpression",
"scriptTask",
"serviceTask",
"businessRuleTask",
"receiveTask",
"sendTask",
"callActivity",
"subProcess",
"transaction",
"adHocSubProcess",
"exclusiveGateway",
"parallelGateway",
"inclusiveGateway",
"eventBasedGateway",
"complexGateway",
"boundaryEvent",
"eventSubProcess",
"intermediateCatchEvent",
"intermediateThrowEvent",
"dataObject",
"dataObjectReference",
"dataStoreReference",
@@ -64,6 +76,16 @@ NATIVE_MAPPING_ELEMENTS = NATIVE_EXECUTION_ELEMENTS | frozenset(
"textAnnotation",
"association",
"group",
"dataInputAssociation",
"dataOutputAssociation",
"choreography",
"choreographyTask",
"callChoreography",
"subChoreography",
"conversation",
"callConversation",
"subConversation",
"conversationLink",
}
)
@@ -116,7 +138,7 @@ def bpmn_support_level(element_type: str) -> SupportLevel:
return "interchange_only"
def inspect_bpmn_xml(xml: str) -> BpmnInspection:
def parse_bpmn_xml(xml: str) -> Element:
encoded = xml.encode("utf-8")
if not encoded:
raise BpmnInspectionError("BPMN XML is empty")
@@ -134,6 +156,15 @@ def inspect_bpmn_xml(xml: str) -> BpmnInspection:
raise BpmnInspectionError(
"BPMN document root must be bpmn:definitions in the BPMN 2.0 model namespace"
)
if sum(1 for _item in root.iter()) > MAX_BPMN_ELEMENTS:
raise BpmnInspectionError(
f"BPMN document exceeds the {MAX_BPMN_ELEMENTS}-element inspection limit"
)
return root
def inspect_bpmn_xml(xml: str) -> BpmnInspection:
root = parse_bpmn_xml(xml)
diagnostics: list[BpmnDiagnostic] = []
elements: list[BpmnElementInventoryItem] = []
@@ -149,10 +180,6 @@ def inspect_bpmn_xml(xml: str) -> BpmnInspection:
while stack:
element, parent_type, parent_id = stack.pop()
visited += 1
if visited > MAX_BPMN_ELEMENTS:
raise BpmnInspectionError(
f"BPMN document exceeds the {MAX_BPMN_ELEMENTS}-element inspection limit"
)
element_namespace, element_type = _qualified_name(element.tag)
element_id = _bounded_attribute(element.attrib.get("id"), 255)
if element_namespace == BPMN_MODEL_NAMESPACE:
@@ -268,17 +295,24 @@ def _collect_references(
attributes: dict[str, str],
references: list[tuple[str, str | None, str]],
) -> None:
fields: tuple[str, ...]
if element_type == "sequenceFlow":
fields = ("sourceRef", "targetRef")
elif element_type == "messageFlow":
fields = ("sourceRef", "targetRef", "messageRef")
elif element_type == "participant":
fields = ("processRef",)
elif element_type == "lane":
fields = ("partitionElementRef",)
else:
fields = ()
fields_by_element = {
"sequenceFlow": ("sourceRef", "targetRef"),
"messageFlow": ("sourceRef", "targetRef", "messageRef"),
"association": ("sourceRef", "targetRef"),
"participant": ("processRef",),
"lane": ("partitionElementRef",),
"boundaryEvent": ("attachedToRef",),
"dataInputAssociation": ("sourceRef", "targetRef"),
"dataOutputAssociation": ("sourceRef", "targetRef"),
"dataObjectReference": ("dataObjectRef",),
"dataStoreReference": ("dataStoreRef",),
"messageEventDefinition": ("messageRef", "operationRef"),
"signalEventDefinition": ("signalRef",),
"errorEventDefinition": ("errorRef",),
"escalationEventDefinition": ("escalationRef",),
"compensateEventDefinition": ("activityRef",),
}
fields = fields_by_element.get(element_type, ())
for field in fields:
value = attributes.get(field)
if value:
@@ -306,4 +340,5 @@ __all__ = [
"NATIVE_MAPPING_ELEMENTS",
"bpmn_support_level",
"inspect_bpmn_xml",
"parse_bpmn_xml",
]