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
camunda/job/__init__.py
ADDED
camunda/job/executor.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""JobExecutor:后台轮询线程(对齐 Camunda JobExecutor 职责,M3 + M7)。
|
|
2
|
+
|
|
3
|
+
职责:周期性调用 engine.execute_due_jobs(),让到期作业(timer-start /
|
|
4
|
+
timer-catch / async-continuation)被自动执行 —— 成功删除/续排、失败重试
|
|
5
|
+
顺延、死信停排都由引擎侧语义保证,这里只做「唤醒与节奏」。
|
|
6
|
+
|
|
7
|
+
设计要点:
|
|
8
|
+
- tick() 单步方法:一次立即执行当前到期作业(手动触发 / 测试拨钟后用,
|
|
9
|
+
不依赖线程)。
|
|
10
|
+
- start()/shutdown() 管理后台线程;轮询间隔可配(默认 1s)。
|
|
11
|
+
- 空闲等待用 Event.wait —— shutdown 即时响应,不留 sleep 尾巴。
|
|
12
|
+
- 引擎锁在 execute_due_jobs 内部(RLock),轮询线程与用户命令天然互斥。
|
|
13
|
+
- M7:多 JobExecutor / 多进程场景下,JobExecutor 自动分配唯一 lock_owner
|
|
14
|
+
(hostname-pid-uuid8),通过 store CAS lease 跨节点抢锁(详见
|
|
15
|
+
Store.acquire_due_jobs 与 ProcessEngine._execute_due_jobs_db)。无 store
|
|
16
|
+
时仍走单进程内存路径(向后兼容)。
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import logging
|
|
22
|
+
import os
|
|
23
|
+
import socket
|
|
24
|
+
import threading
|
|
25
|
+
import uuid
|
|
26
|
+
from typing import TYPE_CHECKING, Optional
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING: # 仅类型标注,避免循环依赖
|
|
29
|
+
from camunda.engine.process_engine import ProcessEngine
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _default_lock_owner(name: str) -> str:
|
|
35
|
+
"""生成默认 lock_owner:name-pid-hostname-uuid8(保证跨进程唯一)。
|
|
36
|
+
|
|
37
|
+
name 放在最前,便于人眼 / 断言识别(hostname 可能含连字符或数字,
|
|
38
|
+
pid 同理)。跨进程唯一性:uuid8 + pid + hostname 一起视为全局唯一;
|
|
39
|
+
最终 CAS 仍然按 owner 字符串比对。
|
|
40
|
+
"""
|
|
41
|
+
return f"{name}-{os.getpid()}-{socket.gethostname()}-{uuid.uuid4().hex[:8]}"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class JobExecutor:
|
|
45
|
+
"""到期作业轮询执行器(每进程一个;与引擎实例绑定)。
|
|
46
|
+
|
|
47
|
+
M7:当 engine 持有 Store 时,自动启用 DB CAS lease 抢锁路径,
|
|
48
|
+
lock_owner 默认按 hostname-pid-name-uuid 生成,跨 JobExecutor 实例
|
|
49
|
+
唯一。store=None 时仍走单进程内存路径(向后兼容旧用法)。
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
engine: "ProcessEngine",
|
|
55
|
+
poll_interval: float = 1.0,
|
|
56
|
+
name: str = "job-executor",
|
|
57
|
+
lock_owner: Optional[str] = None,
|
|
58
|
+
lease_seconds: int = 300,
|
|
59
|
+
) -> None:
|
|
60
|
+
self._engine = engine
|
|
61
|
+
self._interval = poll_interval
|
|
62
|
+
self._stop = threading.Event()
|
|
63
|
+
self._thread: Optional[threading.Thread] = None
|
|
64
|
+
self._name = name
|
|
65
|
+
self._lease_seconds = lease_seconds
|
|
66
|
+
# lock_owner:调用方可显式指定(便于测试 / 复用同一 owner 标识),
|
|
67
|
+
# 不指定则按 hostname-pid-name-uuid8 自动生成
|
|
68
|
+
self._lock_owner = lock_owner or _default_lock_owner(name)
|
|
69
|
+
self._db_locking_enabled = engine._store is not None
|
|
70
|
+
|
|
71
|
+
# ------------------------------------------------------------------
|
|
72
|
+
@property
|
|
73
|
+
def is_running(self) -> bool:
|
|
74
|
+
return self._thread is not None and self._thread.is_alive()
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def lock_owner(self) -> str:
|
|
78
|
+
"""当前 JobExecutor 持有的 DB lock_owner(M7 多实例唯一标识)。"""
|
|
79
|
+
return self._lock_owner
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def db_locking_enabled(self) -> bool:
|
|
83
|
+
"""是否启用 DB CAS lease 抢锁(engine 有 Store + lock_owner 已分配)。"""
|
|
84
|
+
return self._db_locking_enabled
|
|
85
|
+
|
|
86
|
+
def tick(self) -> int:
|
|
87
|
+
"""单步:立即执行一轮到期作业(返回执行条数)。线程内外皆可用。
|
|
88
|
+
|
|
89
|
+
M7:当 DB 抢锁启用时,传入 lock_owner 让 execute_due_jobs 走
|
|
90
|
+
CAS lease 路径;否则走内存路径。
|
|
91
|
+
"""
|
|
92
|
+
if self._db_locking_enabled:
|
|
93
|
+
return self._engine.execute_due_jobs(
|
|
94
|
+
lock_owner=self._lock_owner,
|
|
95
|
+
lease_seconds=self._lease_seconds,
|
|
96
|
+
)
|
|
97
|
+
return self._engine.execute_due_jobs()
|
|
98
|
+
|
|
99
|
+
def start(self) -> None:
|
|
100
|
+
"""启动后台轮询线程(幂等:已在运行则忽略)。"""
|
|
101
|
+
if self.is_running:
|
|
102
|
+
return
|
|
103
|
+
self._stop.clear()
|
|
104
|
+
self._thread = threading.Thread(
|
|
105
|
+
target=self._run, name=self._name, daemon=True
|
|
106
|
+
)
|
|
107
|
+
self._thread.start()
|
|
108
|
+
logger.info(
|
|
109
|
+
"JobExecutor %r 启动(owner=%s, lease=%ds, 轮询间隔 %.2fs, DB抢锁=%s)",
|
|
110
|
+
self._name,
|
|
111
|
+
self._lock_owner,
|
|
112
|
+
self._lease_seconds,
|
|
113
|
+
self._interval,
|
|
114
|
+
self._db_locking_enabled,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
def shutdown(self, timeout: Optional[float] = None) -> None:
|
|
118
|
+
"""停止轮询线程并等待退出。
|
|
119
|
+
|
|
120
|
+
M7:不主动释放自己持有的 lease —— 让其自然过期,便于模拟崩溃
|
|
121
|
+
恢复测试(其他 JobExecutor 在 lease 过期后可抢到作业)。若需
|
|
122
|
+
立刻释放,调 engine._store.complete_job_cas / reschedule_job_cas。
|
|
123
|
+
"""
|
|
124
|
+
self._stop.set()
|
|
125
|
+
if self._thread is not None and self._thread.is_alive():
|
|
126
|
+
self._thread.join(timeout=timeout)
|
|
127
|
+
self._thread = None
|
|
128
|
+
logger.info("JobExecutor %r 已停止(owner=%s)", self._name, self._lock_owner)
|
|
129
|
+
|
|
130
|
+
# ------------------------------------------------------------------
|
|
131
|
+
def _run(self) -> None:
|
|
132
|
+
while not self._stop.wait(self._interval):
|
|
133
|
+
try:
|
|
134
|
+
self.tick()
|
|
135
|
+
except Exception: # 单轮异常不杀死轮询线程
|
|
136
|
+
logger.exception("JobExecutor 轮询一轮失败")
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""model 包:纯数据模型层(不依赖 DB / 不依赖解析器)。
|
|
2
|
+
|
|
3
|
+
对齐 Camunda bpmn-model:
|
|
4
|
+
- BpmnModel := BpmnModelInstance 等价物,一个部署单元内可含多个 Process
|
|
5
|
+
- Process := BPMN 流程定义(含 flow_nodes / sequence_flows)
|
|
6
|
+
- FlowNode := 活动节点基类(事件/任务/网关)
|
|
7
|
+
- SequenceFlow := 连线(可带 conditionExpression / default 标记)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from camunda.model.bpmn import (
|
|
11
|
+
BpmnModel,
|
|
12
|
+
Process,
|
|
13
|
+
FlowNode,
|
|
14
|
+
SequenceFlow,
|
|
15
|
+
TimerDefinition,
|
|
16
|
+
StartEvent,
|
|
17
|
+
EndEvent,
|
|
18
|
+
UserTask,
|
|
19
|
+
ServiceTask,
|
|
20
|
+
ExclusiveGateway,
|
|
21
|
+
ParallelGateway,
|
|
22
|
+
IntermediateCatchEvent,
|
|
23
|
+
BoundaryEvent,
|
|
24
|
+
)
|
|
25
|
+
from camunda.model.execution import Execution, ProcessInstance, ActivityInstance
|
|
26
|
+
from camunda.model.task import Task
|
|
27
|
+
from camunda.model.job import Job
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"BpmnModel",
|
|
31
|
+
"Process",
|
|
32
|
+
"FlowNode",
|
|
33
|
+
"SequenceFlow",
|
|
34
|
+
"TimerDefinition",
|
|
35
|
+
"StartEvent",
|
|
36
|
+
"EndEvent",
|
|
37
|
+
"UserTask",
|
|
38
|
+
"ServiceTask",
|
|
39
|
+
"ExclusiveGateway",
|
|
40
|
+
"ParallelGateway",
|
|
41
|
+
"IntermediateCatchEvent",
|
|
42
|
+
"BoundaryEvent",
|
|
43
|
+
"Execution",
|
|
44
|
+
"ProcessInstance",
|
|
45
|
+
"ActivityInstance",
|
|
46
|
+
"Task",
|
|
47
|
+
"Job",
|
|
48
|
+
]
|
camunda/model/bpmn.py
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
"""BPMN 数据模型(对齐 Camunda bpmn-model 职责,纯 dataclass,无解析/无 DB 依赖)。
|
|
2
|
+
|
|
3
|
+
设计要点:
|
|
4
|
+
- BpmnModel 是「部署单元」:一份 *.bpmn 文件可含多个 Process(BpmnModelInstance 等价物)
|
|
5
|
+
- FlowNode 有类型层级,但 M1 采用「注册表 + 运行时行为分派」,见 engine/behavior.py
|
|
6
|
+
- 所有元素的子元素/属性,只保留引擎流转所需字段,扩展属性走 extension 字典
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import Any, Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
# 定时器定义(M3)
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
@dataclass
|
|
19
|
+
class TimerDefinition:
|
|
20
|
+
"""BPMN timerEventDefinition 的语义承载。
|
|
21
|
+
|
|
22
|
+
kind: "duration" | "date" | "cycle"(对应 timeDuration / timeDate / timeCycle 子元素)
|
|
23
|
+
value: 原始文本(如 PT30S / 2026-09-03T09:00:00 / R3/PT10S 或 cron)
|
|
24
|
+
delay_seconds: 仅 duration:value 解析后的秒数(解析期算好,运行时免重解析)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
kind: str
|
|
28
|
+
value: str
|
|
29
|
+
delay_seconds: Optional[float] = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
# 多实例循环特征(M4-2c)
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
@dataclass
|
|
36
|
+
class MultiInstance:
|
|
37
|
+
"""BPMN multiInstanceLoopCharacteristics 的语义承载(挂活动节点)。
|
|
38
|
+
|
|
39
|
+
实例集来源二选一(同时提供 collection 优先于 cardinality):
|
|
40
|
+
- collection_expr:camunda:collection 属性(形如 "${reviewers}",运行时求值
|
|
41
|
+
须得 list/tuple/set -> N 个实例;元素变量 element_variable 逐实例注入)
|
|
42
|
+
- loop_cardinality_expr:bpmn:loopCardinality 子元素(形如 "3" 或 "${count}",
|
|
43
|
+
运行时求值须得 int -> N 个实例,无元素变量)
|
|
44
|
+
都缺省 = 空集合(0 次)语义,运行时部署报错(Camunda 亦要求其一)。
|
|
45
|
+
sequential: isSequential(true=顺序循环单线推进;false=并行 spawn N 实例)
|
|
46
|
+
completion_condition_expr: completionCondition 子元素原文(形如
|
|
47
|
+
"${nrOfCompletedInstances >= 2}")。None = 全部实例完成后活动才结束。
|
|
48
|
+
执行期内置计数器变量(引擎维护,与 Camunda 对齐):
|
|
49
|
+
nrOfInstances / nrOfActiveInstances / nrOfCompletedInstances / loopCounter。
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
sequential: bool = False
|
|
53
|
+
collection_expr: Optional[str] = None
|
|
54
|
+
loop_cardinality_expr: Optional[str] = None
|
|
55
|
+
element_variable: Optional[str] = None
|
|
56
|
+
completion_condition_expr: Optional[str] = None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
# 连线
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
@dataclass
|
|
63
|
+
class SequenceFlow:
|
|
64
|
+
"""BPMN sequenceFlow:连接两个 FlowNode,可带条件(排他网关出边)。"""
|
|
65
|
+
|
|
66
|
+
id: str
|
|
67
|
+
source_ref: str
|
|
68
|
+
target_ref: str
|
|
69
|
+
name: Optional[str] = None
|
|
70
|
+
# Camunda 语义:conditionExpression 文本(形如 "${amount > 1000}"),或 None
|
|
71
|
+
condition_expression: Optional[str] = None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# ---------------------------------------------------------------------------
|
|
75
|
+
# 节点(FlowNode)
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
@dataclass
|
|
78
|
+
class FlowNode:
|
|
79
|
+
"""流程图中所有「节点」的基类:事件 / 任务 / 网关。"""
|
|
80
|
+
|
|
81
|
+
id: str
|
|
82
|
+
name: Optional[str] = None
|
|
83
|
+
incoming: List[str] = field(default_factory=list) # 入边 sequenceFlow id
|
|
84
|
+
outgoing: List[str] = field(default_factory=list) # 出边 sequenceFlow id
|
|
85
|
+
# Camunda 排他网关默认边:default attribute(schema 层面叫 default)
|
|
86
|
+
default_flow: Optional[str] = None
|
|
87
|
+
# Camunda async continuation:asyncBefore 把「节点行为执行」拆成独立 job
|
|
88
|
+
# (M3 实现 asyncBefore;asyncAfter 解析保留,行为 M4 支持,文档化差异)
|
|
89
|
+
async_before: bool = False
|
|
90
|
+
async_after: bool = False
|
|
91
|
+
# 挂在本活动上的边界事件 id(boundaryEvent attachedToRef 归属,解析期回填)
|
|
92
|
+
boundary_events: List[str] = field(default_factory=list)
|
|
93
|
+
# 多实例循环特征(multiInstanceLoopCharacteristics,M4-2c;None = 普通活动)
|
|
94
|
+
multi_instance: Optional[MultiInstance] = None
|
|
95
|
+
# 附加属性(camunda:xxx 扩展 / 其它命名空间),引擎按需读取
|
|
96
|
+
extension: Dict[str, Any] = field(default_factory=dict)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass
|
|
100
|
+
class StartEvent(FlowNode):
|
|
101
|
+
"""流程起点 / 事件子流程触发器。一个 Process 有 0..N 个。
|
|
102
|
+
|
|
103
|
+
事件槽互斥(恰好一个,解析校验):
|
|
104
|
+
- timer:定时触发(M3 流程级 timer start;M4-2b 事件子流程 timer start)
|
|
105
|
+
- error_code:错误触发(仅事件子流程合法,M4-2b;规范强制 is_interrupting=True)
|
|
106
|
+
- message_name:消息触发(事件子流程 message start,M4-2d;宿主 scope 激活期
|
|
107
|
+
订阅,correlate_message 关联触发)
|
|
108
|
+
- signal_name:信号触发(事件子流程 signal start,M4-2d;throw_signal 广播触发)
|
|
109
|
+
流程级(非事件子流程)message/signal start 引擎不落地(M4-2d 文档化差异)。
|
|
110
|
+
is_interrupting:事件子流程 start 的中断标志(true=取消宿主 scope 全部执行;
|
|
111
|
+
false=spawn 并发执行,可多次触发)。普通流程 start 恒为 true(无宿主可中断)。
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
is_interrupting: bool = True
|
|
115
|
+
# 事件槽(四选一,见类 docstring)
|
|
116
|
+
timer: Optional[TimerDefinition] = None
|
|
117
|
+
error_code: Optional[str] = None
|
|
118
|
+
message_name: Optional[str] = None
|
|
119
|
+
signal_name: Optional[str] = None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass
|
|
123
|
+
class IntermediateCatchEvent(FlowNode):
|
|
124
|
+
"""中间捕获事件(timer / message / signal 变体)。
|
|
125
|
+
|
|
126
|
+
token 到达后停等(事件槽互斥):
|
|
127
|
+
- timer:注册 timer-catch Job(M3),到期触发继续;
|
|
128
|
+
- message_name:注册消息订阅(M4-2d),correlate_message 关联触发;
|
|
129
|
+
- signal_name:注册信号订阅(M4-2d),throw_signal / 实例内 signal throw 触发。
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
timer: Optional[TimerDefinition] = None
|
|
133
|
+
message_name: Optional[str] = None
|
|
134
|
+
signal_name: Optional[str] = None
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@dataclass
|
|
138
|
+
class BoundaryEvent(FlowNode):
|
|
139
|
+
"""边界事件(timer / message / signal 变体,M4-1 起)。
|
|
140
|
+
|
|
141
|
+
挂在宿主活动(attachedToRef)上,宿主等待期间事件触发即沿本事件的
|
|
142
|
+
出边走绑定路径:
|
|
143
|
+
- cancel_activity=True(默认)中断式:取消宿主活动(结算 actinst、删除
|
|
144
|
+
宿主 task / 停等 job / 订阅),token 复用走边界出边;
|
|
145
|
+
- cancel_activity=False 非中断式:宿主不取消,spawn 并发线从边界出边走
|
|
146
|
+
(M4-2b4 timer;M4-2d message/signal 同规则——宿主仍在等待,订阅常驻
|
|
147
|
+
可再次触发)。
|
|
148
|
+
事件槽互斥(事件定义解析校验):timer(M4-1)/ message、signal(M4-2d)。
|
|
149
|
+
timer 仅支持 timeDuration / timeDate;timeCycle 拒绝(文档化差异)。
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
attached_to: Optional[str] = None # 宿主活动节点 id(attachedToRef)
|
|
153
|
+
cancel_activity: bool = True # cancelActivity 属性(默认 true)
|
|
154
|
+
timer: Optional[TimerDefinition] = None
|
|
155
|
+
message_name: Optional[str] = None
|
|
156
|
+
signal_name: Optional[str] = None
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@dataclass
|
|
160
|
+
class SubProcess(FlowNode):
|
|
161
|
+
"""内嵌子流程(embedded subProcess,M4-2a)。
|
|
162
|
+
|
|
163
|
+
SubProcess 是父容器(process 或上层 subProcess)里的一个活动节点,自身又是
|
|
164
|
+
一个独立容器:process 字段持有内部 flow_nodes/sequence_flows/start_events
|
|
165
|
+
(结构与 Process 相同)。执行语义:
|
|
166
|
+
- token 到达 SubProcess -> 停驻为 SCOPE + spawn 内部 token 从内部 startEvent
|
|
167
|
+
推进;内部全部走完(子 scope 收束)后父 token 复活沿 SubProcess 出边继续。
|
|
168
|
+
- 边界事件可挂 SubProcess(attachedToRef),等待窗口 = 整段子流程执行期
|
|
169
|
+
(M4-2a 中断式;cancelActivity=false 非中断式随事件子流程 M4-2b 落地)。
|
|
170
|
+
- 变量作用域沿用实例级(M1 文档化差异,无子作用域遮蔽)。
|
|
171
|
+
- triggered_by_event=true(事件子流程)解析保留、运行时明确报错(M4-2b)。
|
|
172
|
+
容器间连线约束:sequenceFlow 只能引用同容器内的节点(跨容器 wire 部署即报错)。
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
# 内部容器(不参与 deploy 的独立 Process 实例;递归嵌套时其 flow_nodes 可再含 SubProcess)
|
|
176
|
+
process: Optional["Process"] = None
|
|
177
|
+
triggered_by_event: bool = False # 事件子流程(Event SubProcess)标志
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@dataclass
|
|
181
|
+
class EndEvent(FlowNode):
|
|
182
|
+
"""流程终点:token 到达即该 execution 结束(可带 throw 事件,互斥)。
|
|
183
|
+
|
|
184
|
+
- error_code:错误结束(M4-2b)——token 结束同时抛 BPMN 错误,沿宿主 scope
|
|
185
|
+
链冒泡找匹配的 error 事件子流程;无匹配等同 none end(对齐 Camunda)。
|
|
186
|
+
- message_name:消息结束(M4-2d)——token 结束同时向本实例投递消息
|
|
187
|
+
(1:1 就近关联,无匹配静默丢弃,对齐 Camunda 实例内 throw 语义)。
|
|
188
|
+
- signal_name:信号结束(M4-2d)——token 结束同时在本实例内广播信号。
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
error_code: Optional[str] = None
|
|
192
|
+
message_name: Optional[str] = None
|
|
193
|
+
signal_name: Optional[str] = None
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@dataclass
|
|
197
|
+
class IntermediateThrowEvent(FlowNode):
|
|
198
|
+
"""中间抛出事件(M4-2d:message / signal throw)。
|
|
199
|
+
|
|
200
|
+
token 经过时不产生等待窗口:抛出的消息/信号在本实例内触发匹配订阅后,
|
|
201
|
+
token 沿出边继续流转(无出边则收束)。消息 = 1:1 就近关联;信号 = 实例内
|
|
202
|
+
广播(M4-2d 文档化差异:signal 跨实例广播由公共 API throw_signal 提供,
|
|
203
|
+
throw 事件本身只广播到本实例)。
|
|
204
|
+
事件槽互斥:message_name / signal_name 二选一;timer / error throw 不支持
|
|
205
|
+
(解析期明确报错,文档化差异)。
|
|
206
|
+
"""
|
|
207
|
+
|
|
208
|
+
message_name: Optional[str] = None
|
|
209
|
+
signal_name: Optional[str] = None
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@dataclass
|
|
213
|
+
class UserTask(FlowNode):
|
|
214
|
+
"""人工任务:引擎创建 Task 等待 complete,M1 不支持 assignee 表达式。"""
|
|
215
|
+
|
|
216
|
+
assignee: Optional[str] = None
|
|
217
|
+
candidate_users: List[str] = field(default_factory=list)
|
|
218
|
+
candidate_groups: List[str] = field(default_factory=list)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@dataclass
|
|
222
|
+
class ServiceTask(FlowNode):
|
|
223
|
+
"""服务任务:实现 = delegate bean 名或 python 可调用注册名。
|
|
224
|
+
|
|
225
|
+
解析规则(Camunda 兼容):
|
|
226
|
+
- camunda:delegateExpression="${myBean}" -> 注册名 myBean
|
|
227
|
+
- camunda:class="com.foo.Bar" -> 注册名取最后一个点号后段(对齐 Java 类短名)
|
|
228
|
+
- 均未指定 -> extension["implementation_ref"] = None,行为默认 pass-through
|
|
229
|
+
"""
|
|
230
|
+
|
|
231
|
+
implementation_ref: Optional[str] = None
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@dataclass
|
|
235
|
+
class BusinessRuleTask(FlowNode):
|
|
236
|
+
"""业务规则任务(M5):调用已部署的 DMN 决策并把结果写入实例变量。
|
|
237
|
+
|
|
238
|
+
- decision_ref:camunda:decisionRef(部署即必填校验,缺失报错)
|
|
239
|
+
- result_variable:camunda:resultVariable(默认 "result"),承接决策
|
|
240
|
+
求值结果(标量/dict/列表,形态由 hitPolicy 决定,见 dmn/engine docstring)
|
|
241
|
+
求值为同步无等待窗口(同 serviceTask);DMN 部署不落库(对齐 delegate
|
|
242
|
+
注册不落库先例,重启后须重新 deploy_dmn,文档化差异)。
|
|
243
|
+
"""
|
|
244
|
+
|
|
245
|
+
decision_ref: Optional[str] = None
|
|
246
|
+
result_variable: str = "result"
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@dataclass
|
|
250
|
+
class ExclusiveGateway(FlowNode):
|
|
251
|
+
"""排他网关(XOR):按条件取第一条满足的出边;无条件边兜底。"""
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@dataclass
|
|
255
|
+
class ParallelGateway(FlowNode):
|
|
256
|
+
"""并行网关(AND):fork 拆分多 token;join 汇聚(M1 单层 join 语义)。"""
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
# tag(localName) -> 节点类 的映射,供解析器分派
|
|
260
|
+
FLOW_NODE_TYPES: Dict[str, type] = {
|
|
261
|
+
"startEvent": StartEvent,
|
|
262
|
+
"endEvent": EndEvent,
|
|
263
|
+
"userTask": UserTask,
|
|
264
|
+
"serviceTask": ServiceTask,
|
|
265
|
+
"businessRuleTask": BusinessRuleTask,
|
|
266
|
+
"exclusiveGateway": ExclusiveGateway,
|
|
267
|
+
"parallelGateway": ParallelGateway,
|
|
268
|
+
"intermediateCatchEvent": IntermediateCatchEvent,
|
|
269
|
+
"intermediateThrowEvent": IntermediateThrowEvent,
|
|
270
|
+
"boundaryEvent": BoundaryEvent,
|
|
271
|
+
"subProcess": SubProcess,
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
# ---------------------------------------------------------------------------
|
|
276
|
+
# 流程与模型
|
|
277
|
+
# ---------------------------------------------------------------------------
|
|
278
|
+
@dataclass
|
|
279
|
+
class Process:
|
|
280
|
+
"""BPMN process 定义(对齐 Camunda ProcessDefinition 的静态部分)。"""
|
|
281
|
+
|
|
282
|
+
id: str # XML process id(Camunda 称之为 key)
|
|
283
|
+
name: Optional[str] = None
|
|
284
|
+
is_executable: bool = True
|
|
285
|
+
# 元素索引
|
|
286
|
+
flow_nodes: Dict[str, FlowNode] = field(default_factory=dict) # id -> FlowNode
|
|
287
|
+
sequence_flows: Dict[str, SequenceFlow] = field(default_factory=dict)
|
|
288
|
+
# 便捷:按类型分组
|
|
289
|
+
start_events: List[StartEvent] = field(default_factory=list)
|
|
290
|
+
|
|
291
|
+
def __post_init__(self) -> None:
|
|
292
|
+
if self.flow_nodes:
|
|
293
|
+
self.start_events = [
|
|
294
|
+
n for n in self.flow_nodes.values() if isinstance(n, StartEvent)
|
|
295
|
+
]
|
|
296
|
+
|
|
297
|
+
def get_flow_node(self, node_id: str) -> FlowNode:
|
|
298
|
+
"""按 id 取节点,缺失抛 KeyError(解析校验后正常流程不会发生)。"""
|
|
299
|
+
return self.flow_nodes[node_id]
|
|
300
|
+
|
|
301
|
+
def outgoing_of(self, node: FlowNode) -> List[SequenceFlow]:
|
|
302
|
+
"""某节点的全部出边(按 XML 出现顺序保持)。"""
|
|
303
|
+
return [self.sequence_flows[fid] for fid in node.outgoing]
|
|
304
|
+
|
|
305
|
+
def incoming_of(self, node: FlowNode) -> List[SequenceFlow]:
|
|
306
|
+
return [self.sequence_flows[fid] for fid in node.incoming]
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
@dataclass
|
|
310
|
+
class BpmnModel:
|
|
311
|
+
"""一份 *.bpmn 部署单元(BpmnModelInstance 等价物)。"""
|
|
312
|
+
|
|
313
|
+
processes: List[Process] = field(default_factory=list)
|
|
314
|
+
# 源 XML 名(部署展示用,非必须)
|
|
315
|
+
source_name: Optional[str] = None
|
|
316
|
+
# 原始 XML 文本(M2 持久化时保存,恢复时可重新解析)
|
|
317
|
+
source_xml: Optional[str] = None
|
|
318
|
+
|
|
319
|
+
def get_process(self, process_key: str) -> Process:
|
|
320
|
+
for p in self.processes:
|
|
321
|
+
if p.id == process_key:
|
|
322
|
+
return p
|
|
323
|
+
raise KeyError(f"process not found in model: {process_key!r}")
|
|
324
|
+
|
|
325
|
+
def process_keys(self) -> List[str]:
|
|
326
|
+
return [p.id for p in self.processes]
|
camunda/model/dmn.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""DMN 1.1/1.3 数据模型(M5,纯 dataclass,无解析/无 DB 依赖)。
|
|
2
|
+
|
|
3
|
+
设计要点(对齐 Camunda dmn-model 职责、保持与 BPMN 模型层同风格):
|
|
4
|
+
- DmnModel 是「部署单元」:一份 *.dmn 文件可含多个 Decision
|
|
5
|
+
- M5 范围 = 决策表(decisionTable);literalExpression / DRD(relation、
|
|
6
|
+
invocation 等)解析期明确报错(文档化差异)
|
|
7
|
+
- 命名空间版本不校验(1.1 与 1.3 同构,按 localName 解析)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from typing import List, Optional
|
|
14
|
+
|
|
15
|
+
# DMN 决策表 hit policy(DMN 1.3 规范 §6.3.4)
|
|
16
|
+
HIT_POLICIES = {"UNIQUE", "FIRST", "ANY", "PRIORITY", "RULE ORDER", "COLLECT"}
|
|
17
|
+
|
|
18
|
+
# COLLECT 聚合子策略(hitPolicy="COLLECT" + aggregator 属性;缺省 = 全行列表)
|
|
19
|
+
AGGREGATORS = {"SUM", "MIN", "MAX", "COUNT"}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class DmnInput:
|
|
24
|
+
"""decisionTable 输入列:inputExpression 文本 = FEEL 表达式(变量/路径)。"""
|
|
25
|
+
|
|
26
|
+
id: Optional[str] = None
|
|
27
|
+
name: Optional[str] = None # label 属性(列展示名,可缺)
|
|
28
|
+
expression: str = "" # inputExpression 内文本(如 "amount")
|
|
29
|
+
type_ref: Optional[str] = None # number / string / boolean(仅文档化)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class DmnOutput:
|
|
34
|
+
"""decisionTable 输出列:name = 结果变量键(缺省回退 label/id)。
|
|
35
|
+
|
|
36
|
+
output_values:outputValues 声明的取值优先级序(仅 PRIORITY hitPolicy
|
|
37
|
+
消费——下标越小优先级越高;其余策略忽略)。
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
id: Optional[str] = None
|
|
41
|
+
name: Optional[str] = None
|
|
42
|
+
label: Optional[str] = None
|
|
43
|
+
type_ref: Optional[str] = None
|
|
44
|
+
output_values: List[str] = field(default_factory=list)
|
|
45
|
+
|
|
46
|
+
def result_key(self) -> str:
|
|
47
|
+
"""输出结果在 dict 里的键(Camunda 语义:name 优先,回退 label/id)。"""
|
|
48
|
+
return self.name or self.label or self.id or "output"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class DmnRule:
|
|
53
|
+
"""决策表规则行:input/output entries 与列按下标一一对应。
|
|
54
|
+
|
|
55
|
+
input_entries 元素为 None = 通配(空文本 / "-",恒命中);
|
|
56
|
+
output_entries 元素为 None = 空输出(FEEL 空表达式,Camunda 用于
|
|
57
|
+
COLLECT COUNT 语义)。
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
id: Optional[str] = None
|
|
61
|
+
input_entries: List[Optional[str]] = field(default_factory=list)
|
|
62
|
+
output_entries: List[Optional[str]] = field(default_factory=list)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class DecisionTable:
|
|
67
|
+
"""决策表:hitPolicy 决定多行命中的收敛方式。"""
|
|
68
|
+
|
|
69
|
+
id: Optional[str] = None
|
|
70
|
+
hit_policy: str = "UNIQUE" # 缺省 = UNIQUE(DMN 规范默认)
|
|
71
|
+
aggregator: Optional[str] = None # 仅 COLLECT:SUM/MIN/MAX/COUNT
|
|
72
|
+
inputs: List[DmnInput] = field(default_factory=list)
|
|
73
|
+
outputs: List[DmnOutput] = field(default_factory=list)
|
|
74
|
+
rules: List[DmnRule] = field(default_factory=list)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class Decision:
|
|
79
|
+
"""一个 decision(M5 仅承载 decisionTable 形态)。"""
|
|
80
|
+
|
|
81
|
+
id: str
|
|
82
|
+
name: Optional[str] = None
|
|
83
|
+
decision_table: Optional[DecisionTable] = None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass
|
|
87
|
+
class DmnModel:
|
|
88
|
+
"""一份 *.dmn 部署单元(对齐 BpmnModel 的部署单元角色)。"""
|
|
89
|
+
|
|
90
|
+
decisions: List[Decision] = field(default_factory=list)
|
|
91
|
+
source_name: Optional[str] = None
|
|
92
|
+
source_xml: Optional[str] = None
|
|
93
|
+
|
|
94
|
+
def get_decision(self, key: str) -> Decision:
|
|
95
|
+
for d in self.decisions:
|
|
96
|
+
if d.id == key:
|
|
97
|
+
return d
|
|
98
|
+
raise KeyError(f"decision not found in model: {key!r}")
|
|
99
|
+
|
|
100
|
+
def decision_keys(self) -> List[str]:
|
|
101
|
+
return [d.id for d in self.decisions]
|