camunda-python 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- camunda/__init__.py +10 -0
- camunda/api/__init__.py +14 -0
- camunda/api/app.py +80 -0
- camunda/api/deps.py +17 -0
- camunda/api/errors.py +84 -0
- camunda/api/pagination.py +74 -0
- camunda/api/routers/__init__.py +5 -0
- camunda/api/routers/decision.py +57 -0
- camunda/api/routers/deployment.py +139 -0
- camunda/api/routers/history.py +128 -0
- camunda/api/routers/process_definition.py +49 -0
- camunda/api/routers/process_instance.py +106 -0
- camunda/api/routers/task.py +92 -0
- camunda/api/schemas.py +200 -0
- camunda/common/__init__.py +19 -0
- camunda/common/clock.py +30 -0
- camunda/common/exceptions.py +34 -0
- camunda/common/idgen.py +23 -0
- camunda/common/timers.py +106 -0
- camunda/dmn/__init__.py +5 -0
- camunda/dmn/engine.py +219 -0
- camunda/dmn/feel.py +392 -0
- camunda/engine/__init__.py +9 -0
- camunda/engine/behavior.py +51 -0
- camunda/engine/expression.py +126 -0
- camunda/engine/process_engine.py +3237 -0
- camunda/job/__init__.py +9 -0
- camunda/job/executor.py +136 -0
- camunda/model/__init__.py +48 -0
- camunda/model/bpmn.py +326 -0
- camunda/model/dmn.py +101 -0
- camunda/model/execution.py +121 -0
- camunda/model/job.py +88 -0
- camunda/model/task.py +33 -0
- camunda/model/variable.py +35 -0
- camunda/parser/__init__.py +5 -0
- camunda/parser/bpmn_parser.py +646 -0
- camunda/parser/dmn_parser.py +225 -0
- camunda/persistence/__init__.py +21 -0
- camunda/persistence/entities.py +202 -0
- camunda/persistence/store.py +721 -0
- camunda_python-0.1.0.dist-info/METADATA +377 -0
- camunda_python-0.1.0.dist-info/RECORD +46 -0
- camunda_python-0.1.0.dist-info/WHEEL +5 -0
- camunda_python-0.1.0.dist-info/licenses/LICENSE +200 -0
- camunda_python-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
"""BPMN 2.0 XML 解析器(lxml)。
|
|
2
|
+
|
|
3
|
+
职责(对齐 Camunda bpmn-model 的解析部分):
|
|
4
|
+
1. 解析 XML -> BpmnModel(含多个 Process)
|
|
5
|
+
2. 每个 Process 内:先收集 sequenceFlow,再按 tag 分派 flowNode 类型
|
|
6
|
+
3. 处理 camunda 扩展属性:delegateExpression / class(serviceTask 实现解析)
|
|
7
|
+
4. 校验:节点入边/出边引用存在、流程至少一个 startEvent
|
|
8
|
+
|
|
9
|
+
命名空间处理策略:只用 localName 分派(BPMN 语义元素),属性同时接受
|
|
10
|
+
camunda:xxx 与自带命名空间前缀。extensionElements 仅 serviceTask 需要,
|
|
11
|
+
取 camunda:class / camunda:delegateExpression。
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import Dict, Optional
|
|
17
|
+
|
|
18
|
+
from lxml import etree
|
|
19
|
+
|
|
20
|
+
from camunda.common.exceptions import DeploymentException
|
|
21
|
+
from camunda.common.timers import parse_iso_duration
|
|
22
|
+
from camunda.model.bpmn import (
|
|
23
|
+
BpmnModel,
|
|
24
|
+
BoundaryEvent,
|
|
25
|
+
BusinessRuleTask,
|
|
26
|
+
EndEvent,
|
|
27
|
+
MultiInstance,
|
|
28
|
+
Process,
|
|
29
|
+
SequenceFlow,
|
|
30
|
+
FLOW_NODE_TYPES,
|
|
31
|
+
FlowNode,
|
|
32
|
+
IntermediateCatchEvent,
|
|
33
|
+
IntermediateThrowEvent,
|
|
34
|
+
ServiceTask,
|
|
35
|
+
StartEvent,
|
|
36
|
+
SubProcess,
|
|
37
|
+
TimerDefinition,
|
|
38
|
+
UserTask,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# BPMN 默认命名空间(用于无前缀 tag 匹配,localName 分派时其实不需要,保留常量以文档化)
|
|
42
|
+
BPMN_NS = "http://www.omg.org/spec/BPMN/20100524/MODEL"
|
|
43
|
+
CAMUNDA_NS = "http://camunda.org/schema/1.0/bpmn"
|
|
44
|
+
|
|
45
|
+
# timerEventDefinition 子元素名 -> kind
|
|
46
|
+
_TIMER_KIND = {
|
|
47
|
+
"timeDuration": "duration",
|
|
48
|
+
"timeDate": "date",
|
|
49
|
+
"timeCycle": "cycle",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _local(tag: str) -> str:
|
|
54
|
+
"""lxml tag 形如 {ns}localName -> localName。"""
|
|
55
|
+
return tag.rsplit("}", 1)[-1]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def parse_bpmn_xml(xml_text: str, source_name: Optional[str] = None) -> BpmnModel:
|
|
59
|
+
"""解析 BPMN 2.0 XML 文本 -> BpmnModel。失败抛 DeploymentException。"""
|
|
60
|
+
try:
|
|
61
|
+
root = etree.fromstring(xml_text.encode("utf-8"))
|
|
62
|
+
except etree.XMLSyntaxError as e:
|
|
63
|
+
raise DeploymentException(f"BPMN XML 语法错误: {e}") from e
|
|
64
|
+
|
|
65
|
+
model = BpmnModel(source_name=source_name, source_xml=xml_text)
|
|
66
|
+
|
|
67
|
+
# 顶层事件声明收集:<definitions> 下的 <error id errorCode> / <message id name> /
|
|
68
|
+
# <signal id name>(errorEventDefinition errorRef / messageEventDefinition messageRef /
|
|
69
|
+
# signalEventDefinition signalRef 解析期回填,模型不保留声明表——事件槽内联
|
|
70
|
+
# code/name,运行时零查找)
|
|
71
|
+
error_by_id: Dict[str, str] = {}
|
|
72
|
+
message_by_id: Dict[str, str] = {}
|
|
73
|
+
signal_by_id: Dict[str, str] = {}
|
|
74
|
+
for el in root.iter():
|
|
75
|
+
if not isinstance(el.tag, str): # 跳过注释/PI 节点
|
|
76
|
+
continue
|
|
77
|
+
ln = _local(el.tag)
|
|
78
|
+
if ln == "error":
|
|
79
|
+
error_by_id[el.get("id", "")] = el.get("errorCode") or el.get("id") or ""
|
|
80
|
+
elif ln == "message":
|
|
81
|
+
message_by_id[el.get("id", "")] = el.get("name") or el.get("id") or ""
|
|
82
|
+
elif ln == "signal":
|
|
83
|
+
signal_by_id[el.get("id", "")] = el.get("name") or el.get("id") or ""
|
|
84
|
+
|
|
85
|
+
# 顶层按出现顺序找 <process>(注意可能嵌套在 collaboration 外或内,直接遍历即可)
|
|
86
|
+
for process_el in root.iter():
|
|
87
|
+
if not isinstance(process_el.tag, str): # 跳过注释/PI 节点
|
|
88
|
+
continue
|
|
89
|
+
if _local(process_el.tag) != "process":
|
|
90
|
+
continue
|
|
91
|
+
model.processes.append(
|
|
92
|
+
_parse_process(process_el, error_by_id, message_by_id, signal_by_id)
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
if not model.processes:
|
|
96
|
+
raise DeploymentException("BPMN 文件中未找到任何 <process> 元素")
|
|
97
|
+
|
|
98
|
+
return model
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def parse_bpmn_file(path: str) -> BpmnModel:
|
|
102
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
103
|
+
return parse_bpmn_xml(f.read(), source_name=path)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# ---------------------------------------------------------------------------
|
|
107
|
+
# Process 解析
|
|
108
|
+
# ---------------------------------------------------------------------------
|
|
109
|
+
def _parse_process(
|
|
110
|
+
process_el, error_by_id=None, message_by_id=None, signal_by_id=None
|
|
111
|
+
) -> Process:
|
|
112
|
+
proc = Process(
|
|
113
|
+
id=process_el.get("id") or process_el.get("name") or "process",
|
|
114
|
+
name=process_el.get("name"),
|
|
115
|
+
is_executable=(process_el.get("isExecutable", "true").lower() == "true"),
|
|
116
|
+
)
|
|
117
|
+
_fill_container(
|
|
118
|
+
process_el,
|
|
119
|
+
proc,
|
|
120
|
+
error_by_id=error_by_id,
|
|
121
|
+
message_by_id=message_by_id,
|
|
122
|
+
signal_by_id=signal_by_id,
|
|
123
|
+
)
|
|
124
|
+
proc.__post_init__() # 重算 start_events
|
|
125
|
+
return proc
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _fill_container(
|
|
129
|
+
el,
|
|
130
|
+
proc: Process,
|
|
131
|
+
require_start: bool = True,
|
|
132
|
+
error_by_id: Optional[Dict[str, str]] = None,
|
|
133
|
+
message_by_id: Optional[Dict[str, str]] = None,
|
|
134
|
+
signal_by_id: Optional[Dict[str, str]] = None,
|
|
135
|
+
) -> None:
|
|
136
|
+
"""把一个 BPMN 容器元素(<process> 或 <subProcess>)的节点/连线解析进 Process。
|
|
137
|
+
|
|
138
|
+
四遍结构(M4-2a 起容器递归):
|
|
139
|
+
1. sequenceFlow 先行(节点只存引用 id)
|
|
140
|
+
2. flowNode / subProcess:subProcess 先登记为父容器节点,再递归解析其内部容器
|
|
141
|
+
3. incoming/outgoing 连线挂接(同容器校验:跨容器引用自然报错)
|
|
142
|
+
4. 边界事件按 attachedToRef 归属到宿主
|
|
143
|
+
require_start=False 用于事件子流程(内部 start 是事件触发入口,独立校验)。
|
|
144
|
+
结束调 __post_init__ 重算 start_events。
|
|
145
|
+
"""
|
|
146
|
+
# 第一遍:sequenceFlow(连线必须先于节点存在,节点只存引用 id)
|
|
147
|
+
for child in el:
|
|
148
|
+
if not isinstance(child.tag, str): # 跳过注释/PI 节点
|
|
149
|
+
continue
|
|
150
|
+
if _local(child.tag) == "sequenceFlow":
|
|
151
|
+
flow = _parse_sequence_flow(child)
|
|
152
|
+
proc.sequence_flows[flow.id] = flow
|
|
153
|
+
|
|
154
|
+
# 第二遍:flowNode 家族(task/event/gateway/subProcess 等)
|
|
155
|
+
for child in el:
|
|
156
|
+
if not isinstance(child.tag, str):
|
|
157
|
+
continue
|
|
158
|
+
node_type = _local(child.tag)
|
|
159
|
+
if node_type == "subProcess":
|
|
160
|
+
sub = _parse_sub_process(child, error_by_id, message_by_id, signal_by_id)
|
|
161
|
+
proc.flow_nodes[sub.id] = sub
|
|
162
|
+
elif node_type in FLOW_NODE_TYPES:
|
|
163
|
+
node = _parse_flow_node(child, node_type, error_by_id, message_by_id, signal_by_id)
|
|
164
|
+
proc.flow_nodes[node.id] = node
|
|
165
|
+
|
|
166
|
+
# 第三遍:把连线引用挂到节点上(incoming/outgoing 子元素按 XML 顺序)
|
|
167
|
+
_wire_flows(proc)
|
|
168
|
+
# 第四遍:边界事件按 attachedToRef 归属到宿主活动
|
|
169
|
+
_attach_boundaries(proc)
|
|
170
|
+
|
|
171
|
+
# 校验 + 重算 start_events
|
|
172
|
+
_validate_process(proc, require_start=require_start)
|
|
173
|
+
proc.__post_init__()
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _parse_sub_process(
|
|
177
|
+
el, error_by_id=None, message_by_id=None, signal_by_id=None
|
|
178
|
+
) -> SubProcess:
|
|
179
|
+
"""解析 <subProcess>:父容器登记节点 + 递归解析内部容器。
|
|
180
|
+
|
|
181
|
+
通用属性(id/name/camunda:asyncBefore 等)走 _parse_flow_node;内部子元素
|
|
182
|
+
(内部 sequenceFlow / flowNode / 嵌套 subProcess / boundary 归属)递归进
|
|
183
|
+
_fill_container。事件子流程(triggeredByEvent=true):内部 start 事件驱动
|
|
184
|
+
(无普通 startEvent),解析后做事件子流程专项校验(_validate_event_subprocess)。
|
|
185
|
+
"""
|
|
186
|
+
node = _parse_flow_node(el, "subProcess", error_by_id, message_by_id, signal_by_id)
|
|
187
|
+
assert isinstance(node, SubProcess)
|
|
188
|
+
node.triggered_by_event = el.get("triggeredByEvent", "false").lower() == "true"
|
|
189
|
+
inner = Process(id=f"{node.id}::inner", name=node.name, is_executable=True)
|
|
190
|
+
_fill_container(
|
|
191
|
+
el,
|
|
192
|
+
inner,
|
|
193
|
+
require_start=not node.triggered_by_event,
|
|
194
|
+
error_by_id=error_by_id,
|
|
195
|
+
message_by_id=message_by_id,
|
|
196
|
+
signal_by_id=signal_by_id,
|
|
197
|
+
)
|
|
198
|
+
node.process = inner
|
|
199
|
+
if node.triggered_by_event:
|
|
200
|
+
_validate_event_subprocess(node, inner)
|
|
201
|
+
return node
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _validate_event_subprocess(sub: SubProcess, inner: Process) -> None:
|
|
205
|
+
"""事件子流程容器规范校验(triggeredByEvent=true,M4-2b)。
|
|
206
|
+
|
|
207
|
+
事件子流程不参与 sequenceFlow 流转(无 incoming/outgoing),内部 startEvent
|
|
208
|
+
是唯一触发入口,因此:
|
|
209
|
+
- 至少一个 startEvent;
|
|
210
|
+
- 每个 startEvent 必须恰好带一个事件定义(timer / error / message / signal)——
|
|
211
|
+
none start 无法触发事件子流程;
|
|
212
|
+
- error start 强制 isInterrupting=true(BPMN 规范:错误事件只能中断式);
|
|
213
|
+
- startEvent 不得有入边(事件子流程不与 sequenceFlow 相连)。
|
|
214
|
+
message/signal start 解析与运行时订阅 M4-2d 落地(correlate_message /
|
|
215
|
+
throw_signal 触发,中断/非中断随 isInterrupting)。
|
|
216
|
+
"""
|
|
217
|
+
starts = [n for n in inner.flow_nodes.values() if isinstance(n, StartEvent)]
|
|
218
|
+
if not starts:
|
|
219
|
+
raise DeploymentException(
|
|
220
|
+
f"事件子流程 {sub.id!r} 缺少 startEvent(事件触发入口)"
|
|
221
|
+
)
|
|
222
|
+
for st in starts:
|
|
223
|
+
has_event = (
|
|
224
|
+
st.timer is not None
|
|
225
|
+
or st.error_code is not None
|
|
226
|
+
or st.message_name is not None
|
|
227
|
+
or st.signal_name is not None
|
|
228
|
+
)
|
|
229
|
+
if not has_event:
|
|
230
|
+
raise DeploymentException(
|
|
231
|
+
f"事件子流程 {sub.id!r} 的 startEvent {st.id!r} 缺少事件定义"
|
|
232
|
+
"(none start 不能触发事件子流程)"
|
|
233
|
+
)
|
|
234
|
+
if st.error_code is not None and not st.is_interrupting:
|
|
235
|
+
raise DeploymentException(
|
|
236
|
+
f"事件子流程 {sub.id!r} 的 error start {st.id!r} 声明 "
|
|
237
|
+
"isInterrupting=false:BPMN 规范错误事件只支持中断式"
|
|
238
|
+
)
|
|
239
|
+
if st.incoming:
|
|
240
|
+
raise DeploymentException(
|
|
241
|
+
f"事件子流程 {sub.id!r} 的 startEvent {st.id!r} 有入边:"
|
|
242
|
+
"事件子流程不参与 sequenceFlow 流转"
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _parse_sequence_flow(el) -> SequenceFlow:
|
|
247
|
+
flow = SequenceFlow(
|
|
248
|
+
id=el.get("id", ""),
|
|
249
|
+
source_ref=el.get("sourceRef", ""),
|
|
250
|
+
target_ref=el.get("targetRef", ""),
|
|
251
|
+
name=el.get("name"),
|
|
252
|
+
)
|
|
253
|
+
# conditionExpression 是 sequenceFlow 的子元素:<conditionExpression xsi:type="tFormalExpression">${...}</...>
|
|
254
|
+
for child in el:
|
|
255
|
+
if not isinstance(child.tag, str):
|
|
256
|
+
continue
|
|
257
|
+
if _local(child.tag) == "conditionExpression":
|
|
258
|
+
text = (child.text or "").strip()
|
|
259
|
+
if text:
|
|
260
|
+
flow.condition_expression = text
|
|
261
|
+
return flow
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _parse_flow_node(
|
|
265
|
+
el, node_type: str, error_by_id=None, message_by_id=None, signal_by_id=None
|
|
266
|
+
) -> FlowNode:
|
|
267
|
+
cls = FLOW_NODE_TYPES[node_type]
|
|
268
|
+
node_id = el.get("id", "")
|
|
269
|
+
node = cls(
|
|
270
|
+
id=node_id,
|
|
271
|
+
name=el.get("name"),
|
|
272
|
+
default_flow=el.get("default"),
|
|
273
|
+
)
|
|
274
|
+
# 收集 camunda 命名空间直接属性(如 camunda:class / camunda:delegateExpression
|
|
275
|
+
# 常直接写在元素属性上,也可能藏在 extensionElements 子元素里——两者都收)
|
|
276
|
+
camunda_attrs: Dict[str, Optional[str]] = {}
|
|
277
|
+
for attr_name, attr_val in el.attrib.items():
|
|
278
|
+
if "}" in attr_name and attr_name.rsplit("}", 1)[0].strip("{}") == CAMUNDA_NS:
|
|
279
|
+
camunda_attrs[attr_name.rsplit("}", 1)[-1]] = attr_val.strip()
|
|
280
|
+
|
|
281
|
+
# async continuation 标志(camunda:asyncBefore / asyncAfter="true")
|
|
282
|
+
if "asyncBefore" in camunda_attrs:
|
|
283
|
+
node.async_before = camunda_attrs["asyncBefore"].lower() == "true"
|
|
284
|
+
if "asyncAfter" in camunda_attrs:
|
|
285
|
+
node.async_after = camunda_attrs["asyncAfter"].lower() == "true"
|
|
286
|
+
|
|
287
|
+
# 解析子元素:incoming/outgoing 由 _wire_flows 回填,此处仅处理 extensionElements
|
|
288
|
+
_parse_node_children(el, node)
|
|
289
|
+
|
|
290
|
+
# boundaryEvent: attachedToRef / cancelActivity(事件定义解析走下面事件共通路径)
|
|
291
|
+
if isinstance(node, BoundaryEvent):
|
|
292
|
+
node.attached_to = el.get("attachedToRef")
|
|
293
|
+
node.cancel_activity = el.get("cancelActivity", "true").lower() != "false"
|
|
294
|
+
|
|
295
|
+
# startEvent: isInterrupting(事件子流程中断/非中断标志;普通流程恒 true)
|
|
296
|
+
if isinstance(node, StartEvent):
|
|
297
|
+
node.is_interrupting = el.get("isInterrupting", "true").lower() != "false"
|
|
298
|
+
|
|
299
|
+
# 事件类节点:事件定义统一解析(timer M3 / error、message M4-2b)
|
|
300
|
+
if isinstance(
|
|
301
|
+
node,
|
|
302
|
+
(StartEvent, IntermediateCatchEvent, BoundaryEvent, EndEvent, IntermediateThrowEvent),
|
|
303
|
+
):
|
|
304
|
+
timer, error_code, message_name, signal_name = _parse_event_definitions(
|
|
305
|
+
el, error_by_id, message_by_id, signal_by_id
|
|
306
|
+
)
|
|
307
|
+
defined = [x is not None for x in (timer, error_code, message_name, signal_name)]
|
|
308
|
+
if sum(defined) > 1:
|
|
309
|
+
raise DeploymentException(
|
|
310
|
+
f"节点 {node.id!r} 携带多个事件定义(timer/error/message/signal 互斥)"
|
|
311
|
+
)
|
|
312
|
+
if isinstance(node, (StartEvent, IntermediateCatchEvent, BoundaryEvent)):
|
|
313
|
+
node.timer = timer
|
|
314
|
+
node.error_code = error_code
|
|
315
|
+
node.message_name = message_name
|
|
316
|
+
node.signal_name = signal_name
|
|
317
|
+
elif isinstance(node, (EndEvent, IntermediateThrowEvent)):
|
|
318
|
+
# throw 类事件(M4-2d):end/中间抛出支持 error/message/signal;
|
|
319
|
+
# timer throw 无意义(拒绝)
|
|
320
|
+
node.error_code = error_code
|
|
321
|
+
node.message_name = message_name
|
|
322
|
+
node.signal_name = signal_name
|
|
323
|
+
if timer is not None:
|
|
324
|
+
raise DeploymentException(
|
|
325
|
+
f"{type(node).__name__} {node.id!r} 不支持 timer throw 事件"
|
|
326
|
+
"(文档化差异)"
|
|
327
|
+
)
|
|
328
|
+
else: # IntermediateCatchEvent / BoundaryEvent 之外的类型不会进入此分支
|
|
329
|
+
raise DeploymentException(
|
|
330
|
+
f"节点 {node.id!r} 类型 {type(node).__name__} 不支持事件定义"
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
# serviceTask: 解析实现引用(属性优先级高于扩展子元素)
|
|
334
|
+
if isinstance(node, ServiceTask):
|
|
335
|
+
ref = (
|
|
336
|
+
camunda_attrs.get("delegateExpression")
|
|
337
|
+
or camunda_attrs.get("class")
|
|
338
|
+
or node.extension.get("delegateExpression")
|
|
339
|
+
or node.extension.get("class")
|
|
340
|
+
)
|
|
341
|
+
node.implementation_ref = _resolve_impl_ref(ref)
|
|
342
|
+
|
|
343
|
+
# businessRuleTask(M5):DMN 决策引用必填;结果变量缺省 "result"
|
|
344
|
+
if isinstance(node, BusinessRuleTask):
|
|
345
|
+
ref = camunda_attrs.get("decisionRef") or node.extension.get("decisionRef")
|
|
346
|
+
if not ref:
|
|
347
|
+
raise DeploymentException(
|
|
348
|
+
f"businessRuleTask {node.id!r} 缺少 camunda:decisionRef"
|
|
349
|
+
)
|
|
350
|
+
node.decision_ref = ref
|
|
351
|
+
node.result_variable = (
|
|
352
|
+
camunda_attrs.get("resultVariable")
|
|
353
|
+
or node.extension.get("resultVariable")
|
|
354
|
+
or "result"
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
# 其余 camunda 属性并入 extension(供未来里程碑使用)
|
|
358
|
+
if camunda_attrs:
|
|
359
|
+
node.extension.update(camunda_attrs)
|
|
360
|
+
return node
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _parse_node_children(el, node: FlowNode) -> None:
|
|
364
|
+
"""解析节点子元素:incoming/outgoing 引用在 _wire_flows 处理;
|
|
365
|
+
这里处理 extensionElements 里的简单 camunda:xxx 扩展与 multiInstanceLoopCharacteristics。"""
|
|
366
|
+
camunda_attrs: Dict[str, Optional[str]] = {}
|
|
367
|
+
|
|
368
|
+
# 遍历子元素(XML 顺序)
|
|
369
|
+
for child in el:
|
|
370
|
+
if not isinstance(child.tag, str):
|
|
371
|
+
continue
|
|
372
|
+
lname = _local(child.tag)
|
|
373
|
+
# 注意:incoming/outgoing 引用由 _wire_flows 统一从 sequenceFlow 回填,
|
|
374
|
+
# 这里不收集,避免与连线挂接逻辑重复。
|
|
375
|
+
if lname in ("incoming", "outgoing"):
|
|
376
|
+
continue
|
|
377
|
+
if lname == "conditionExpression":
|
|
378
|
+
# 条件表达式挂在 sequenceFlow 上,此处不会出现;防御性忽略
|
|
379
|
+
pass
|
|
380
|
+
elif lname == "multiInstanceLoopCharacteristics":
|
|
381
|
+
# 多实例循环特征(M4-2c):宿主白名单 userTask / serviceTask /
|
|
382
|
+
# subProcess;其余类型(事件/网关等)部署即报错(文档化差异)
|
|
383
|
+
if not isinstance(node, (UserTask, ServiceTask, SubProcess)):
|
|
384
|
+
raise DeploymentException(
|
|
385
|
+
f"节点 {node.id!r} 声明 multiInstanceLoopCharacteristics:M4-2c "
|
|
386
|
+
f"仅支持 userTask / serviceTask / subProcess 宿主,"
|
|
387
|
+
f"{type(node).__name__} 不支持(文档化差异)"
|
|
388
|
+
)
|
|
389
|
+
node.multi_instance = _parse_multi_instance(child)
|
|
390
|
+
elif lname == "extensionElements":
|
|
391
|
+
_collect_camunda_extension(child, camunda_attrs)
|
|
392
|
+
|
|
393
|
+
if camunda_attrs:
|
|
394
|
+
node.extension.update(camunda_attrs)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _parse_multi_instance(el) -> MultiInstance:
|
|
398
|
+
"""解析 bpmn:multiInstanceLoopCharacteristics 子元素。
|
|
399
|
+
|
|
400
|
+
XML 结构:
|
|
401
|
+
<bpmn:multiInstanceLoopCharacteristics isSequential="false"
|
|
402
|
+
camunda:collection="${reviewers}" camunda:elementVariable="reviewer">
|
|
403
|
+
<bpmn:loopCardinality xsi:type="bpmn:tFormalExpression">3</...>
|
|
404
|
+
<bpmn:completionCondition xsi:type="bpmn:tFormalExpression">
|
|
405
|
+
${nrOfCompletedInstances >= 2}</...>
|
|
406
|
+
</bpmn:multiInstanceLoopCharacteristics>
|
|
407
|
+
|
|
408
|
+
表达式一律保留原文(含 ${}),求值由引擎在运行时做。collection 与
|
|
409
|
+
loopCardinality 至少其一(同时提供 collection 优先),否则部署报错。
|
|
410
|
+
"""
|
|
411
|
+
camunda_attrs: Dict[str, Optional[str]] = {}
|
|
412
|
+
for attr_name, attr_val in el.attrib.items():
|
|
413
|
+
if "}" in attr_name and attr_name.rsplit("}", 1)[0].strip("{}") == CAMUNDA_NS:
|
|
414
|
+
camunda_attrs[attr_name.rsplit("}", 1)[-1]] = attr_val.strip()
|
|
415
|
+
|
|
416
|
+
sequential = el.get("isSequential", "false").strip().lower() == "true"
|
|
417
|
+
collection = camunda_attrs.get("collection")
|
|
418
|
+
element_variable = camunda_attrs.get("elementVariable")
|
|
419
|
+
cardinality: Optional[str] = None
|
|
420
|
+
completion: Optional[str] = None
|
|
421
|
+
for child in el:
|
|
422
|
+
if not isinstance(child.tag, str):
|
|
423
|
+
continue
|
|
424
|
+
lname = _local(child.tag)
|
|
425
|
+
if lname == "loopCardinality":
|
|
426
|
+
cardinality = (child.text or "").strip()
|
|
427
|
+
elif lname == "completionCondition":
|
|
428
|
+
completion = (child.text or "").strip()
|
|
429
|
+
if collection is None and cardinality is None:
|
|
430
|
+
raise DeploymentException(
|
|
431
|
+
"multiInstanceLoopCharacteristics 必须提供 camunda:collection 或 "
|
|
432
|
+
"loopCardinality(M4-2c),当前两者皆缺"
|
|
433
|
+
)
|
|
434
|
+
if element_variable is not None and collection is None:
|
|
435
|
+
raise DeploymentException(
|
|
436
|
+
"camunda:elementVariable 仅与 camunda:collection 配合使用"
|
|
437
|
+
)
|
|
438
|
+
return MultiInstance(
|
|
439
|
+
sequential=sequential,
|
|
440
|
+
collection_expr=collection,
|
|
441
|
+
loop_cardinality_expr=cardinality,
|
|
442
|
+
element_variable=element_variable,
|
|
443
|
+
completion_condition_expr=completion,
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def _collect_camunda_extension(extension_el, out: Dict[str, Optional[str]]) -> None:
|
|
448
|
+
"""从 extensionElements 里收集 camunda:* 子元素的文本值。
|
|
449
|
+
|
|
450
|
+
camunda:properties / camunda:inputOutput 等复杂结构 M1 不解析,只收集简单标量。
|
|
451
|
+
"""
|
|
452
|
+
for child in extension_el:
|
|
453
|
+
lname = _local(child.tag)
|
|
454
|
+
# 跳过复杂容器(properties/inputOutput/connector/field/script...),M4 再支持
|
|
455
|
+
if lname in ("properties", "inputOutput", "connector", "field", "script"):
|
|
456
|
+
continue
|
|
457
|
+
text = (child.text or "").strip()
|
|
458
|
+
# 只保留有文本的简单扩展,如 <camunda:failedJobRetryTimeCycle>PT5M</...>
|
|
459
|
+
if text:
|
|
460
|
+
out[lname] = text
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _parse_timer_definition(el) -> Optional[TimerDefinition]:
|
|
464
|
+
"""读事件元素里的 timerEventDefinition。
|
|
465
|
+
|
|
466
|
+
XML 结构:
|
|
467
|
+
<bpmn:intermediateCatchEvent id="wait">
|
|
468
|
+
<bpmn:timerEventDefinition>
|
|
469
|
+
<bpmn:timeDuration xsi:type="bpmn:tFormalExpression">PT30S</...>
|
|
470
|
+
duration 在解析期算好 delay_seconds(运行时免重解析),非法时长部署期即报错。
|
|
471
|
+
"""
|
|
472
|
+
for child in el:
|
|
473
|
+
if not isinstance(child.tag, str):
|
|
474
|
+
continue
|
|
475
|
+
if _local(child.tag) != "timerEventDefinition":
|
|
476
|
+
continue
|
|
477
|
+
for sub in child:
|
|
478
|
+
if not isinstance(sub.tag, str):
|
|
479
|
+
continue
|
|
480
|
+
kind = _TIMER_KIND.get(_local(sub.tag))
|
|
481
|
+
if kind is None:
|
|
482
|
+
continue
|
|
483
|
+
text = (sub.text or "").strip()
|
|
484
|
+
if not text:
|
|
485
|
+
continue
|
|
486
|
+
if kind == "duration":
|
|
487
|
+
try:
|
|
488
|
+
delay = parse_iso_duration(text)
|
|
489
|
+
except ValueError as e:
|
|
490
|
+
raise DeploymentException(
|
|
491
|
+
f"timer timeDuration 非法 {text!r}: {e}"
|
|
492
|
+
) from e
|
|
493
|
+
return TimerDefinition(kind="duration", value=text, delay_seconds=delay)
|
|
494
|
+
return TimerDefinition(kind=kind, value=text)
|
|
495
|
+
return None
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def _parse_event_definitions(el, error_by_id=None, message_by_id=None, signal_by_id=None):
|
|
499
|
+
"""解析事件元素的全部事件定义 -> (timer, error_code, message_name, signal_name)。
|
|
500
|
+
|
|
501
|
+
- timerEventDefinition -> _parse_timer_definition(M3)
|
|
502
|
+
- errorEventDefinition -> errorRef 关联顶层 <error errorCode>,解析期回填 code
|
|
503
|
+
(兼容直接在定义上写 errorCode 的建模工具;无 code 部署即报错)
|
|
504
|
+
- messageEventDefinition -> messageRef 关联顶层 <message name>,回填 name
|
|
505
|
+
(兼容 messageEventDefinition 直接写 name)
|
|
506
|
+
- signalEventDefinition -> signalRef 关联顶层 <signal name>,回填 name
|
|
507
|
+
(兼容 signalEventDefinition 直接写 name)(M4-2d)
|
|
508
|
+
事件元素至多一个事件定义(互斥校验在调用方)。
|
|
509
|
+
"""
|
|
510
|
+
timer = _parse_timer_definition(el)
|
|
511
|
+
error_code = None
|
|
512
|
+
message_name = None
|
|
513
|
+
signal_name = None
|
|
514
|
+
for child in el:
|
|
515
|
+
if not isinstance(child.tag, str):
|
|
516
|
+
continue
|
|
517
|
+
ln = _local(child.tag)
|
|
518
|
+
if ln == "errorEventDefinition":
|
|
519
|
+
ref = child.get("errorRef")
|
|
520
|
+
code = None
|
|
521
|
+
if ref:
|
|
522
|
+
code = (error_by_id or {}).get(ref)
|
|
523
|
+
if code is None:
|
|
524
|
+
raise DeploymentException(
|
|
525
|
+
f"errorEventDefinition 引用未知 error 声明: {ref!r}"
|
|
526
|
+
)
|
|
527
|
+
code = code or child.get("errorCode")
|
|
528
|
+
if not code:
|
|
529
|
+
raise DeploymentException(
|
|
530
|
+
"errorEventDefinition 缺少可解析的 errorCode(无 errorRef 指向 "
|
|
531
|
+
"的顶层声明,也未直接声明 errorCode)"
|
|
532
|
+
)
|
|
533
|
+
error_code = code
|
|
534
|
+
elif ln == "messageEventDefinition":
|
|
535
|
+
ref = child.get("messageRef")
|
|
536
|
+
name = None
|
|
537
|
+
if ref:
|
|
538
|
+
name = (message_by_id or {}).get(ref)
|
|
539
|
+
if name is None:
|
|
540
|
+
raise DeploymentException(
|
|
541
|
+
f"messageEventDefinition 引用未知 message 声明: {ref!r}"
|
|
542
|
+
)
|
|
543
|
+
name = name or child.get("name")
|
|
544
|
+
if not name:
|
|
545
|
+
raise DeploymentException(
|
|
546
|
+
"messageEventDefinition 缺少可解析的 message name(无 messageRef "
|
|
547
|
+
"指向的顶层声明,也未直接声明 name)"
|
|
548
|
+
)
|
|
549
|
+
message_name = name
|
|
550
|
+
elif ln == "signalEventDefinition":
|
|
551
|
+
ref = child.get("signalRef")
|
|
552
|
+
name = None
|
|
553
|
+
if ref:
|
|
554
|
+
name = (signal_by_id or {}).get(ref)
|
|
555
|
+
if name is None:
|
|
556
|
+
raise DeploymentException(
|
|
557
|
+
f"signalEventDefinition 引用未知 signal 声明: {ref!r}"
|
|
558
|
+
)
|
|
559
|
+
name = name or child.get("name")
|
|
560
|
+
if not name:
|
|
561
|
+
raise DeploymentException(
|
|
562
|
+
"signalEventDefinition 缺少可解析的 signal name(无 signalRef "
|
|
563
|
+
"指向的顶层声明,也未直接声明 name)"
|
|
564
|
+
)
|
|
565
|
+
signal_name = name
|
|
566
|
+
return timer, error_code, message_name, signal_name
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def _resolve_impl_ref(ref: Optional[str]) -> Optional[str]:
|
|
570
|
+
"""把 camunda 实现引用转成注册名。
|
|
571
|
+
|
|
572
|
+
- delegateExpression="${myBean}" -> "myBean"
|
|
573
|
+
- class="com.example.MyDelegate" -> "MyDelegate"(短名;M1 用短名注册 Python 可调用)
|
|
574
|
+
"""
|
|
575
|
+
if not ref:
|
|
576
|
+
return None
|
|
577
|
+
ref = ref.strip()
|
|
578
|
+
if ref.startswith("${") and ref.endswith("}"):
|
|
579
|
+
return ref[2:-1].strip()
|
|
580
|
+
if "." in ref:
|
|
581
|
+
return ref.rsplit(".", 1)[-1]
|
|
582
|
+
return ref
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
# ---------------------------------------------------------------------------
|
|
586
|
+
# 连线挂接与校验
|
|
587
|
+
# ---------------------------------------------------------------------------
|
|
588
|
+
def _wire_flows(proc: Process) -> None:
|
|
589
|
+
"""校验所有 sequenceFlow 的 source/target 存在于 flow_nodes,并回填节点出/入边。"""
|
|
590
|
+
for flow in proc.sequence_flows.values():
|
|
591
|
+
src = proc.flow_nodes.get(flow.source_ref)
|
|
592
|
+
tgt = proc.flow_nodes.get(flow.target_ref)
|
|
593
|
+
if src is None:
|
|
594
|
+
raise DeploymentException(
|
|
595
|
+
f"sequenceFlow {flow.id!r} 的 sourceRef {flow.source_ref!r} 不存在于 process {proc.id!r}"
|
|
596
|
+
)
|
|
597
|
+
if tgt is None:
|
|
598
|
+
raise DeploymentException(
|
|
599
|
+
f"sequenceFlow {flow.id!r} 的 targetRef {flow.target_ref!r} 不存在于 process {proc.id!r}"
|
|
600
|
+
)
|
|
601
|
+
src.outgoing.append(flow.id)
|
|
602
|
+
tgt.incoming.append(flow.id)
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def _attach_boundaries(proc: Process) -> None:
|
|
606
|
+
"""把 boundaryEvent 挂到宿主活动(attachedToRef 回填 flow_nodes[host].boundary_events)。
|
|
607
|
+
|
|
608
|
+
边界事件不入主流转(无 incoming),仅出边参与流转;触发由引擎在宿主
|
|
609
|
+
等待期间调度(见 engine timer-boundary job)。BPMN 约束:宿主必须是活动
|
|
610
|
+
或事件(不能是 start/end/boundary/网关),M4-1 引擎进一步限制为有等待点
|
|
611
|
+
的活动(userTask / asyncBefore 节点),运行时明确报错。
|
|
612
|
+
"""
|
|
613
|
+
for node in proc.flow_nodes.values():
|
|
614
|
+
if not isinstance(node, BoundaryEvent):
|
|
615
|
+
continue
|
|
616
|
+
if not node.attached_to:
|
|
617
|
+
raise DeploymentException(
|
|
618
|
+
f"boundaryEvent {node.id!r} 缺少 attachedToRef(未指定宿主活动)"
|
|
619
|
+
)
|
|
620
|
+
host = proc.flow_nodes.get(node.attached_to)
|
|
621
|
+
if host is None:
|
|
622
|
+
raise DeploymentException(
|
|
623
|
+
f"boundaryEvent {node.id!r} 的 attachedToRef {node.attached_to!r} 不存在于 process {proc.id!r}"
|
|
624
|
+
)
|
|
625
|
+
if isinstance(host, (StartEvent, EndEvent, BoundaryEvent)):
|
|
626
|
+
raise DeploymentException(
|
|
627
|
+
f"boundaryEvent {node.id!r} 不能挂在 {type(host).__name__} {host.id!r} 上"
|
|
628
|
+
)
|
|
629
|
+
host.boundary_events.append(node.id)
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def _validate_process(proc: Process, require_start: bool = True) -> None:
|
|
633
|
+
"""容器结构校验。
|
|
634
|
+
|
|
635
|
+
require_start=False 用于事件子流程等「内部没有普通 startEvent」的容器
|
|
636
|
+
(M4-2b 前宽容;运行时语义由引擎明确报错)。
|
|
637
|
+
"""
|
|
638
|
+
if not proc.flow_nodes:
|
|
639
|
+
raise DeploymentException(f"process {proc.id!r} 没有任何 flowNode")
|
|
640
|
+
|
|
641
|
+
if require_start and not any(
|
|
642
|
+
isinstance(n, StartEvent) for n in proc.flow_nodes.values()
|
|
643
|
+
):
|
|
644
|
+
raise DeploymentException(f"process {proc.id!r} 缺少 startEvent")
|
|
645
|
+
|
|
646
|
+
# 排他网关出边必须有 default 或条件(宽松校验:不强制,运行时无条件时随机走第一条 -> 引擎内决定)
|