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.
Files changed (46) hide show
  1. camunda/__init__.py +10 -0
  2. camunda/api/__init__.py +14 -0
  3. camunda/api/app.py +80 -0
  4. camunda/api/deps.py +17 -0
  5. camunda/api/errors.py +84 -0
  6. camunda/api/pagination.py +74 -0
  7. camunda/api/routers/__init__.py +5 -0
  8. camunda/api/routers/decision.py +57 -0
  9. camunda/api/routers/deployment.py +139 -0
  10. camunda/api/routers/history.py +128 -0
  11. camunda/api/routers/process_definition.py +49 -0
  12. camunda/api/routers/process_instance.py +106 -0
  13. camunda/api/routers/task.py +92 -0
  14. camunda/api/schemas.py +200 -0
  15. camunda/common/__init__.py +19 -0
  16. camunda/common/clock.py +30 -0
  17. camunda/common/exceptions.py +34 -0
  18. camunda/common/idgen.py +23 -0
  19. camunda/common/timers.py +106 -0
  20. camunda/dmn/__init__.py +5 -0
  21. camunda/dmn/engine.py +219 -0
  22. camunda/dmn/feel.py +392 -0
  23. camunda/engine/__init__.py +9 -0
  24. camunda/engine/behavior.py +51 -0
  25. camunda/engine/expression.py +126 -0
  26. camunda/engine/process_engine.py +3237 -0
  27. camunda/job/__init__.py +9 -0
  28. camunda/job/executor.py +136 -0
  29. camunda/model/__init__.py +48 -0
  30. camunda/model/bpmn.py +326 -0
  31. camunda/model/dmn.py +101 -0
  32. camunda/model/execution.py +121 -0
  33. camunda/model/job.py +88 -0
  34. camunda/model/task.py +33 -0
  35. camunda/model/variable.py +35 -0
  36. camunda/parser/__init__.py +5 -0
  37. camunda/parser/bpmn_parser.py +646 -0
  38. camunda/parser/dmn_parser.py +225 -0
  39. camunda/persistence/__init__.py +21 -0
  40. camunda/persistence/entities.py +202 -0
  41. camunda/persistence/store.py +721 -0
  42. camunda_python-0.1.0.dist-info/METADATA +377 -0
  43. camunda_python-0.1.0.dist-info/RECORD +46 -0
  44. camunda_python-0.1.0.dist-info/WHEEL +5 -0
  45. camunda_python-0.1.0.dist-info/licenses/LICENSE +200 -0
  46. camunda_python-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,9 @@
1
+ """job 包:作业执行器(M3 里程碑交付:Timer / async continuation 轮询执行;
2
+ M4-1 扩展 timer 边界事件作业)。
3
+
4
+ - executor.JobExecutor 后台轮询线程 + tick() 单步(到期作业自动执行/重试/死信)
5
+ """
6
+
7
+ from camunda.job.executor import JobExecutor
8
+
9
+ __all__ = ["JobExecutor"]
@@ -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]