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/dmn/engine.py
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""DMN 决策引擎(M5-3):决策表求值 + hitPolicy 收敛语义。
|
|
2
|
+
|
|
3
|
+
结果形态(与 Camunda DmnDecisionResult 的常用形态对齐、文档化差异见下):
|
|
4
|
+
- UNIQUE / FIRST / ANY:单输出列 -> 标量;多输出列 -> dict{output键: 值};
|
|
5
|
+
无命中 -> None
|
|
6
|
+
- RULE ORDER / COLLECT(无聚合):行结果列表(标量或 dict,按命中顺序)
|
|
7
|
+
- COLLECT + SUM/MIN/MAX:标量(要求恰好 1 个输出列)
|
|
8
|
+
- COLLECT + COUNT:命中行数(int)
|
|
9
|
+
|
|
10
|
+
文档化差异:
|
|
11
|
+
- UNIQUE 多行命中 -> 运行时 ExpressionEvaluationException(DMN 规范违例)
|
|
12
|
+
- ANY 各行输出不一致 -> 运行时报错
|
|
13
|
+
- PRIORITY 单输出列按 outputValues 优先级序取最高;多输出列不支持
|
|
14
|
+
- 无命中不抛异常返回 None(对齐 Camunda 空结果语义)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from typing import Any, Dict, List, Optional
|
|
20
|
+
|
|
21
|
+
from camunda.common.exceptions import (
|
|
22
|
+
ExpressionEvaluationException,
|
|
23
|
+
NotFoundException,
|
|
24
|
+
)
|
|
25
|
+
from camunda.dmn.feel import eval_expression, eval_unary_test
|
|
26
|
+
from camunda.model.dmn import Decision, DecisionTable, DmnModel, DmnOutput, DmnRule
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class DmnEngine:
|
|
30
|
+
"""独立 DMN 引擎(对齐 Camunda DecisionService 职责,可脱离 BPMN 单用)。"""
|
|
31
|
+
|
|
32
|
+
def __init__(self) -> None:
|
|
33
|
+
# key -> Decision(重复部署视为新版本,覆盖并版本 +1,对齐 ACT_RE_DECDEF)
|
|
34
|
+
self._decisions: Dict[str, Decision] = {}
|
|
35
|
+
self._decision_versions: Dict[str, int] = {}
|
|
36
|
+
|
|
37
|
+
# ------------------------------------------------------------------
|
|
38
|
+
# RepositoryService 语义
|
|
39
|
+
# ------------------------------------------------------------------
|
|
40
|
+
def deploy(self, model: DmnModel) -> List[str]:
|
|
41
|
+
"""部署 DmnModel,返回 decision key 列表(重复 key 版本 +1)。"""
|
|
42
|
+
keys: List[str] = []
|
|
43
|
+
for dec in model.decisions:
|
|
44
|
+
self._decisions[dec.id] = dec
|
|
45
|
+
self._decision_versions[dec.id] = self._decision_versions.get(dec.id, 0) + 1
|
|
46
|
+
keys.append(dec.id)
|
|
47
|
+
return keys
|
|
48
|
+
|
|
49
|
+
def get_decision(self, key: str) -> Decision:
|
|
50
|
+
if key not in self._decisions:
|
|
51
|
+
raise NotFoundException(f"未部署的决策定义: {key!r}")
|
|
52
|
+
return self._decisions[key]
|
|
53
|
+
|
|
54
|
+
def get_decision_version(self, key: str) -> int:
|
|
55
|
+
return self._decision_versions.get(key, 0)
|
|
56
|
+
|
|
57
|
+
def list_decisions(self) -> List[Dict[str, Any]]:
|
|
58
|
+
"""已部署决策列表(key / name / version,部署序)。"""
|
|
59
|
+
return [
|
|
60
|
+
{"key": d.id, "name": d.name, "version": self._decision_versions.get(d.id, 0)}
|
|
61
|
+
for d in self._decisions.values()
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
# ------------------------------------------------------------------
|
|
65
|
+
# 决策求值
|
|
66
|
+
# ------------------------------------------------------------------
|
|
67
|
+
def evaluate_decision(self, key: str, variables: Optional[dict] = None) -> Any:
|
|
68
|
+
"""求值决策表,返回形态见模块 docstring。"""
|
|
69
|
+
decision = self.get_decision(key)
|
|
70
|
+
table = decision.decision_table
|
|
71
|
+
if table is None:
|
|
72
|
+
raise ExpressionEvaluationException(
|
|
73
|
+
f"decision {key!r} 不含 decisionTable(M5 仅支持决策表)"
|
|
74
|
+
)
|
|
75
|
+
return self._evaluate_table(table, variables or {}, key)
|
|
76
|
+
|
|
77
|
+
# ------------------------------------------------------------------
|
|
78
|
+
# 决策表核心求值
|
|
79
|
+
# ------------------------------------------------------------------
|
|
80
|
+
def _evaluate_table(self, table: DecisionTable, variables: dict, key: str) -> Any:
|
|
81
|
+
# 1) 输入列求值(inputExpression 文本对 variables)
|
|
82
|
+
input_values = [
|
|
83
|
+
eval_expression(inp.expression, variables) for inp in table.inputs
|
|
84
|
+
]
|
|
85
|
+
# 2) 规则命中过滤(全列 unaryTests 与)
|
|
86
|
+
hits: List[DmnRule] = []
|
|
87
|
+
for rule in table.rules:
|
|
88
|
+
matched = True
|
|
89
|
+
for text, value in zip(rule.input_entries, input_values):
|
|
90
|
+
if not eval_unary_test(text, value):
|
|
91
|
+
matched = False
|
|
92
|
+
break
|
|
93
|
+
if matched:
|
|
94
|
+
hits.append(rule)
|
|
95
|
+
if not hits:
|
|
96
|
+
return self._no_hit_result(table)
|
|
97
|
+
# 3) hitPolicy 收敛
|
|
98
|
+
policy = table.hit_policy
|
|
99
|
+
if policy == "UNIQUE":
|
|
100
|
+
if len(hits) > 1:
|
|
101
|
+
raise ExpressionEvaluationException(
|
|
102
|
+
f"决策 {key!r} hitPolicy=UNIQUE 违例:{len(hits)} 条规则同时命中"
|
|
103
|
+
)
|
|
104
|
+
return self._row_result(table, hits[0], variables)
|
|
105
|
+
if policy == "FIRST":
|
|
106
|
+
return self._row_result(table, hits[0], variables)
|
|
107
|
+
if policy == "ANY":
|
|
108
|
+
results = [self._row_result(table, r, variables) for r in hits]
|
|
109
|
+
first = results[0]
|
|
110
|
+
if any(r != first for r in results[1:]):
|
|
111
|
+
raise ExpressionEvaluationException(
|
|
112
|
+
f"决策 {key!r} hitPolicy=ANY 违例:命中规则输出不一致"
|
|
113
|
+
)
|
|
114
|
+
return first
|
|
115
|
+
if policy == "PRIORITY":
|
|
116
|
+
return self._priority_result(table, hits, variables, key)
|
|
117
|
+
# RULE ORDER / COLLECT 共用行收集
|
|
118
|
+
results = [self._row_result(table, r, variables) for r in hits]
|
|
119
|
+
if policy == "RULE ORDER":
|
|
120
|
+
return results
|
|
121
|
+
return self._collect_result(table, results, hits)
|
|
122
|
+
|
|
123
|
+
# ------------------------------------------------------------------
|
|
124
|
+
# 收敛子策略
|
|
125
|
+
# ------------------------------------------------------------------
|
|
126
|
+
def _no_hit_result(self, table: DecisionTable) -> Any:
|
|
127
|
+
agg = table.aggregator
|
|
128
|
+
if table.hit_policy == "COLLECT" and agg == "COUNT":
|
|
129
|
+
return 0
|
|
130
|
+
if table.hit_policy == "COLLECT" and agg in ("SUM", "MIN", "MAX"):
|
|
131
|
+
return None
|
|
132
|
+
if table.hit_policy in ("RULE ORDER", "COLLECT"):
|
|
133
|
+
return []
|
|
134
|
+
return None # UNIQUE / FIRST / ANY 无命中 -> 空结果(对齐 Camunda)
|
|
135
|
+
|
|
136
|
+
def _row_result(
|
|
137
|
+
self, table: DecisionTable, rule: DmnRule, variables: dict
|
|
138
|
+
) -> Any:
|
|
139
|
+
"""单行结果:单输出列 -> 标量;多输出列 -> dict。"""
|
|
140
|
+
values = [
|
|
141
|
+
eval_expression(text, variables) if text is not None else None
|
|
142
|
+
for text in rule.output_entries
|
|
143
|
+
]
|
|
144
|
+
if len(table.outputs) == 1:
|
|
145
|
+
return values[0]
|
|
146
|
+
return {out.result_key(): v for out, v in zip(table.outputs, values)}
|
|
147
|
+
|
|
148
|
+
def _priority_result(
|
|
149
|
+
self, table: DecisionTable, hits: List[DmnRule], variables: dict, key: str
|
|
150
|
+
) -> Any:
|
|
151
|
+
if len(table.outputs) != 1:
|
|
152
|
+
raise ExpressionEvaluationException(
|
|
153
|
+
f"决策 {key!r} hitPolicy=PRIORITY 仅支持单输出列"
|
|
154
|
+
f"(实际 {len(table.outputs)} 列)"
|
|
155
|
+
)
|
|
156
|
+
priority: List[str] = table.outputs[0].output_values
|
|
157
|
+
if not priority:
|
|
158
|
+
raise ExpressionEvaluationException(
|
|
159
|
+
f"决策 {key!r} hitPolicy=PRIORITY 需要 output 声明 outputValues"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
def rank(rule: DmnRule) -> int:
|
|
163
|
+
text = rule.output_entries[0]
|
|
164
|
+
val = eval_expression(text, variables) if text is not None else None
|
|
165
|
+
for i, p in enumerate(priority):
|
|
166
|
+
# 优先级按字面量文本比对(outputValues 里的原文)
|
|
167
|
+
if _literal_text(p) == _literal_text(
|
|
168
|
+
text if text is not None else ""
|
|
169
|
+
):
|
|
170
|
+
return i
|
|
171
|
+
if _values_loose_eq(val, p):
|
|
172
|
+
return i
|
|
173
|
+
return len(priority) # 未声明取值 = 最低优先级
|
|
174
|
+
|
|
175
|
+
best = min(hits, key=rank)
|
|
176
|
+
return self._row_result(table, best, variables)
|
|
177
|
+
|
|
178
|
+
def _collect_result(
|
|
179
|
+
self, table: DecisionTable, results: List[Any], hits: List[DmnRule]
|
|
180
|
+
) -> Any:
|
|
181
|
+
agg = table.aggregator
|
|
182
|
+
if agg is None:
|
|
183
|
+
return results
|
|
184
|
+
if agg == "COUNT":
|
|
185
|
+
return len(hits)
|
|
186
|
+
# SUM/MIN/MAX:要求单输出列数值
|
|
187
|
+
if len(table.outputs) != 1:
|
|
188
|
+
raise ExpressionEvaluationException(
|
|
189
|
+
f"COLLECT {agg} 仅支持单输出列(实际 {len(table.outputs)} 列)"
|
|
190
|
+
)
|
|
191
|
+
nums = [v for v in results if v is not None]
|
|
192
|
+
non_numeric = [v for v in nums if isinstance(v, bool) or not isinstance(v, (int, float))]
|
|
193
|
+
if non_numeric:
|
|
194
|
+
raise ExpressionEvaluationException(
|
|
195
|
+
f"COLLECT {agg} 要求数值输出,遇到 {non_numeric[0]!r}"
|
|
196
|
+
)
|
|
197
|
+
if agg == "SUM":
|
|
198
|
+
return sum(nums)
|
|
199
|
+
if agg == "MIN":
|
|
200
|
+
return min(nums) if nums else None
|
|
201
|
+
return max(nums) if nums else None
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _literal_text(s: str) -> str:
|
|
205
|
+
"""去字符串字面量引号:'"A"' -> 'A'。"""
|
|
206
|
+
s = s.strip()
|
|
207
|
+
if len(s) >= 2 and s[0] == '"' and s[-1] == '"':
|
|
208
|
+
return s[1:-1]
|
|
209
|
+
return s
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _values_loose_eq(val: Any, literal: str) -> bool:
|
|
213
|
+
"""求值结果与 outputValues 字面量的宽松比对。"""
|
|
214
|
+
if isinstance(val, str):
|
|
215
|
+
return val == _literal_text(literal)
|
|
216
|
+
try:
|
|
217
|
+
return val == float(literal)
|
|
218
|
+
except (TypeError, ValueError):
|
|
219
|
+
return False
|
camunda/dmn/feel.py
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
"""FEEL 表达式子集求值器(M5-2,手写递归下降,无外部依赖)。
|
|
2
|
+
|
|
3
|
+
范围(文档化差异:仅 FEEL Friendly 子集,覆盖决策表典型用法):
|
|
4
|
+
|
|
5
|
+
unaryTests(输入单元格,配合输入值求布尔):
|
|
6
|
+
- 通配(空文本/"-",解析期已归一为 None)恒命中
|
|
7
|
+
- 布尔/数值/字符串字面量比较(裸值 = 相等语义)
|
|
8
|
+
- 比较算子:= != < <= > >=
|
|
9
|
+
- 区间:[a..b] 闭闭、(a..b) 开开、]a..b[ / (a..b] 混合开闭(DMN 双标记法均支持)
|
|
10
|
+
- 逗号列表 = OR(任一命中)
|
|
11
|
+
- not(...) 取反
|
|
12
|
+
- null 字面量(= null 判缺变量)
|
|
13
|
+
|
|
14
|
+
expression(输出单元格 / inputExpression):
|
|
15
|
+
- 字面量:number / "string" / true / false / null
|
|
16
|
+
- 变量引用(IDENT,未定义 -> null,对齐 FEEL 缺变量语义)
|
|
17
|
+
- 算术:+ - * / 与一元负号、括号,标准优先级
|
|
18
|
+
- 字符串 + 拼接
|
|
19
|
+
|
|
20
|
+
不支持(运行时明确报错):between、in、函数调用、日期时间、路径表达式、
|
|
21
|
+
instance of、for/some/every 等。
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from typing import Any, List, Optional
|
|
27
|
+
|
|
28
|
+
from camunda.common.exceptions import ExpressionEvaluationException
|
|
29
|
+
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
# 词法
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
_TWO_CHAR_OPS = {">=", "<=", "!="}
|
|
34
|
+
_ONE_CHAR_OPS = set("><=+-*/(),[]")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class _Token:
|
|
38
|
+
__slots__ = ("kind", "value", "pos")
|
|
39
|
+
|
|
40
|
+
def __init__(self, kind: str, value: Any, pos: int) -> None:
|
|
41
|
+
self.kind = kind # NUM / STR / IDENT / OP / DOTDOT / EOF
|
|
42
|
+
self.value = value
|
|
43
|
+
self.pos = pos
|
|
44
|
+
|
|
45
|
+
def __repr__(self) -> str: # 调试便利
|
|
46
|
+
return f"_Token({self.kind!r}, {self.value!r})"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _tokenize(text: str) -> List[_Token]:
|
|
50
|
+
tokens: List[_Token] = []
|
|
51
|
+
i, n = 0, len(text)
|
|
52
|
+
while i < n:
|
|
53
|
+
ch = text[i]
|
|
54
|
+
if ch.isspace():
|
|
55
|
+
i += 1
|
|
56
|
+
continue
|
|
57
|
+
if ch == '"':
|
|
58
|
+
j = i + 1
|
|
59
|
+
buf = []
|
|
60
|
+
while j < n and text[j] != '"':
|
|
61
|
+
buf.append(text[j])
|
|
62
|
+
j += 1
|
|
63
|
+
if j >= n:
|
|
64
|
+
raise ExpressionEvaluationException(
|
|
65
|
+
f"字符串字面量未闭合: {text!r}"
|
|
66
|
+
)
|
|
67
|
+
tokens.append(_Token("STR", "".join(buf), i))
|
|
68
|
+
i = j + 1
|
|
69
|
+
continue
|
|
70
|
+
if ch.isdigit() or (ch == "." and i + 1 < n and text[i + 1].isdigit()):
|
|
71
|
+
j = i
|
|
72
|
+
seen_dot = False
|
|
73
|
+
while j < n and (text[j].isdigit() or (text[j] == "." and not seen_dot)):
|
|
74
|
+
if text[j] == ".":
|
|
75
|
+
# ".." 是区间分隔符,不是小数点
|
|
76
|
+
if j + 1 < n and text[j + 1] == ".":
|
|
77
|
+
break
|
|
78
|
+
seen_dot = True
|
|
79
|
+
j += 1
|
|
80
|
+
raw = text[i:j]
|
|
81
|
+
num = float(raw) if "." in raw else int(raw)
|
|
82
|
+
tokens.append(_Token("NUM", num, i))
|
|
83
|
+
i = j
|
|
84
|
+
continue
|
|
85
|
+
if ch.isalpha() or ch == "_":
|
|
86
|
+
j = i
|
|
87
|
+
while j < n and (text[j].isalnum() or text[j] == "_"):
|
|
88
|
+
j += 1
|
|
89
|
+
tokens.append(_Token("IDENT", text[i:j], i))
|
|
90
|
+
i = j
|
|
91
|
+
continue
|
|
92
|
+
if text.startswith("..", i):
|
|
93
|
+
tokens.append(_Token("DOTDOT", "..", i))
|
|
94
|
+
i += 2
|
|
95
|
+
continue
|
|
96
|
+
if text[i : i + 2] in _TWO_CHAR_OPS:
|
|
97
|
+
tokens.append(_Token("OP", text[i : i + 2], i))
|
|
98
|
+
i += 2
|
|
99
|
+
continue
|
|
100
|
+
if ch in _ONE_CHAR_OPS:
|
|
101
|
+
tokens.append(_Token("OP", ch, i))
|
|
102
|
+
i += 1
|
|
103
|
+
continue
|
|
104
|
+
raise ExpressionEvaluationException(
|
|
105
|
+
f"FEEL 不支持的字符 {ch!r}(位置 {i}): {text!r}"
|
|
106
|
+
)
|
|
107
|
+
tokens.append(_Token("EOF", None, n))
|
|
108
|
+
return tokens
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# ---------------------------------------------------------------------------
|
|
112
|
+
# 求值辅助
|
|
113
|
+
# ---------------------------------------------------------------------------
|
|
114
|
+
def _cmp_key(v: Any, ctx: str) -> Any:
|
|
115
|
+
"""比较前类型守卫:None 与不可比较类型直接报错(FEEL 非法比较语义)。"""
|
|
116
|
+
if v is None:
|
|
117
|
+
raise ExpressionEvaluationException(f"{ctx}: 操作数为 null 不可比较")
|
|
118
|
+
if isinstance(v, bool) or isinstance(v, (int, float, str)):
|
|
119
|
+
return v
|
|
120
|
+
raise ExpressionEvaluationException(f"{ctx}: 类型 {type(v).__name__} 不可比较")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _values_equal(a: Any, b: Any) -> bool:
|
|
124
|
+
"""FEEL 相等:数值跨 int/float;None 只与 null 相等;bool 严格。"""
|
|
125
|
+
if a is None or b is None:
|
|
126
|
+
return a is None and b is None
|
|
127
|
+
if isinstance(a, bool) or isinstance(b, bool):
|
|
128
|
+
return isinstance(a, bool) and isinstance(b, bool) and a == b
|
|
129
|
+
if isinstance(a, (int, float)) and isinstance(b, (int, float)):
|
|
130
|
+
return a == b
|
|
131
|
+
if isinstance(a, str) and isinstance(b, str):
|
|
132
|
+
return a == b
|
|
133
|
+
return False
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _order_cmp(a: Any, b: Any) -> int:
|
|
137
|
+
"""FEEL 排序比较:数值或字符串同类;bool 不可排序比较。"""
|
|
138
|
+
if isinstance(a, bool) or isinstance(b, bool):
|
|
139
|
+
raise ExpressionEvaluationException("布尔值不支持 < <= > >= 比较")
|
|
140
|
+
if isinstance(a, (int, float)) and isinstance(b, (int, float)):
|
|
141
|
+
return (a > b) - (a < b)
|
|
142
|
+
if isinstance(a, str) and isinstance(b, str):
|
|
143
|
+
return (a > b) - (a < b)
|
|
144
|
+
raise ExpressionEvaluationException(
|
|
145
|
+
f"类型不可排序比较: {type(a).__name__} vs {type(b).__name__}"
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# ---------------------------------------------------------------------------
|
|
150
|
+
# 递归下降解析/求值(parse 即 eval,无 AST 中间层——表达式足够小)
|
|
151
|
+
# ---------------------------------------------------------------------------
|
|
152
|
+
class _Parser:
|
|
153
|
+
def __init__(self, text: str, variables: dict) -> None:
|
|
154
|
+
self.text = text
|
|
155
|
+
self.vars = variables
|
|
156
|
+
self.tokens = _tokenize(text)
|
|
157
|
+
self.i = 0
|
|
158
|
+
|
|
159
|
+
# -- token 工具 -----------------------------------------------------
|
|
160
|
+
@property
|
|
161
|
+
def cur(self) -> _Token:
|
|
162
|
+
return self.tokens[self.i]
|
|
163
|
+
|
|
164
|
+
def _next(self) -> _Token:
|
|
165
|
+
tok = self.tokens[self.i]
|
|
166
|
+
self.i += 1
|
|
167
|
+
return tok
|
|
168
|
+
|
|
169
|
+
def _expect_op(self, op: str) -> None:
|
|
170
|
+
tok = self._next()
|
|
171
|
+
if tok.kind not in ("OP", "DOTDOT") or tok.value != op:
|
|
172
|
+
raise ExpressionEvaluationException(
|
|
173
|
+
f"期望 {op!r} 实得 {tok.value!r}(位置 {tok.pos}): {self.text!r}"
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
def _peek_is_op(self, *ops: str) -> Optional[str]:
|
|
177
|
+
tok = self.cur
|
|
178
|
+
if tok.kind == "OP" and tok.value in ops:
|
|
179
|
+
return tok.value
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
# -- 入口 -----------------------------------------------------------
|
|
183
|
+
def eval(self) -> Any:
|
|
184
|
+
val = self.additive()
|
|
185
|
+
if self.cur.kind != "EOF":
|
|
186
|
+
raise ExpressionEvaluationException(
|
|
187
|
+
f"表达式存在未消费的尾部 {self.cur.value!r}: {self.text!r}"
|
|
188
|
+
)
|
|
189
|
+
return val
|
|
190
|
+
|
|
191
|
+
def eval_unary_test(self, input_value: Any) -> bool:
|
|
192
|
+
"""unaryTests 入口:not(...) / 逗号 OR 列表 / 主体。"""
|
|
193
|
+
return self._disjunction(input_value)
|
|
194
|
+
|
|
195
|
+
def _next_is_open_paren(self) -> bool:
|
|
196
|
+
nxt = self.tokens[self.i + 1] if self.i + 1 < len(self.tokens) else None
|
|
197
|
+
return nxt is not None and nxt.kind == "OP" and nxt.value == "("
|
|
198
|
+
|
|
199
|
+
def _disjunction(self, input_value: Any) -> bool:
|
|
200
|
+
"""逗号列表 = OR(DMN unaryTests 语义,逐项完整解析)。"""
|
|
201
|
+
result = self._item(input_value)
|
|
202
|
+
while self._peek_is_op(","):
|
|
203
|
+
self._next()
|
|
204
|
+
result = self._item(input_value) or result
|
|
205
|
+
return result
|
|
206
|
+
|
|
207
|
+
def _item(self, input_value: Any) -> bool:
|
|
208
|
+
"""单个 unaryTest 项:not(...) 或 positiveUnaryTest。"""
|
|
209
|
+
if (
|
|
210
|
+
self.cur.kind == "IDENT"
|
|
211
|
+
and self.cur.value == "not"
|
|
212
|
+
and self._next_is_open_paren()
|
|
213
|
+
):
|
|
214
|
+
self._next() # not
|
|
215
|
+
self._expect_op("(")
|
|
216
|
+
result = not self._disjunction(input_value)
|
|
217
|
+
self._expect_op(")")
|
|
218
|
+
return result
|
|
219
|
+
return self._positive(input_value)
|
|
220
|
+
|
|
221
|
+
def _positive(self, input_value: Any) -> bool:
|
|
222
|
+
"""positiveUnaryTest:比较 / 区间 / 裸表达式相等。"""
|
|
223
|
+
op = self._peek_is_op("=", "!=", ">", ">=", "<", "<=")
|
|
224
|
+
if op:
|
|
225
|
+
self._next()
|
|
226
|
+
if op in (">", ">=", "<", "<=") and input_value is None:
|
|
227
|
+
return False # FEEL:null 参与排序比较 = false(规则不命中)
|
|
228
|
+
rhs = self.additive()
|
|
229
|
+
return _apply_comparison(op, input_value, rhs)
|
|
230
|
+
# 区间:[ ] ( 开头的 interval
|
|
231
|
+
if self._peek_is_op("[", "]", "("):
|
|
232
|
+
return self._interval(input_value)
|
|
233
|
+
# 裸表达式 = 相等语义(FEEL:positiveUnaryExpression 命中判断)
|
|
234
|
+
lhs = self.additive()
|
|
235
|
+
return _values_equal(input_value, lhs)
|
|
236
|
+
|
|
237
|
+
def _interval(self, input_value: Any) -> bool:
|
|
238
|
+
open_tok = self._next() # [ ] (
|
|
239
|
+
open_ch = open_tok.value
|
|
240
|
+
low_closed = open_ch == "["
|
|
241
|
+
low = self.additive()
|
|
242
|
+
self._expect_op("..")
|
|
243
|
+
high = self.additive()
|
|
244
|
+
close_tok = self._next()
|
|
245
|
+
close_ch = close_tok.value
|
|
246
|
+
if close_ch not in ("]", "[", ")"):
|
|
247
|
+
raise ExpressionEvaluationException(
|
|
248
|
+
f"区间右端非法 {close_ch!r}: {self.text!r}"
|
|
249
|
+
)
|
|
250
|
+
high_closed = close_ch == "]"
|
|
251
|
+
if input_value is None:
|
|
252
|
+
return False # null 不落在任何区间
|
|
253
|
+
try:
|
|
254
|
+
above_low = _order_cmp(_cmp_key(input_value, "区间下界"), _cmp_key(low, "区间下界"))
|
|
255
|
+
above_high = _order_cmp(_cmp_key(input_value, "区间上界"), _cmp_key(high, "区间上界"))
|
|
256
|
+
except ExpressionEvaluationException:
|
|
257
|
+
raise
|
|
258
|
+
if low_closed:
|
|
259
|
+
if above_low < 0:
|
|
260
|
+
return False
|
|
261
|
+
elif above_low <= 0:
|
|
262
|
+
return False
|
|
263
|
+
if high_closed:
|
|
264
|
+
if above_high > 0:
|
|
265
|
+
return False
|
|
266
|
+
elif above_high >= 0:
|
|
267
|
+
return False
|
|
268
|
+
return True
|
|
269
|
+
|
|
270
|
+
# -- 表达式层次 -------------------------------------------------------
|
|
271
|
+
def additive(self) -> Any:
|
|
272
|
+
val = self.mult()
|
|
273
|
+
while True:
|
|
274
|
+
op = self._peek_is_op("+", "-")
|
|
275
|
+
if not op:
|
|
276
|
+
return val
|
|
277
|
+
self._next()
|
|
278
|
+
rhs = self.mult()
|
|
279
|
+
if op == "+":
|
|
280
|
+
if isinstance(val, str) or isinstance(rhs, str):
|
|
281
|
+
if not (isinstance(val, str) and isinstance(rhs, str)):
|
|
282
|
+
raise ExpressionEvaluationException(
|
|
283
|
+
"字符串 + 仅支持字符串拼接"
|
|
284
|
+
)
|
|
285
|
+
val = val + rhs
|
|
286
|
+
else:
|
|
287
|
+
val = _arith(val, rhs, "+", self.text)
|
|
288
|
+
else:
|
|
289
|
+
val = _arith(val, rhs, "-", self.text)
|
|
290
|
+
|
|
291
|
+
def mult(self) -> Any:
|
|
292
|
+
val = self.unary_expr()
|
|
293
|
+
while True:
|
|
294
|
+
op = self._peek_is_op("*", "/")
|
|
295
|
+
if not op:
|
|
296
|
+
return val
|
|
297
|
+
self._next()
|
|
298
|
+
val = _arith(val, self.unary_expr(), op, self.text)
|
|
299
|
+
|
|
300
|
+
def unary_expr(self) -> Any:
|
|
301
|
+
tok = self.cur
|
|
302
|
+
if tok.kind == "OP" and tok.value == "-":
|
|
303
|
+
self._next()
|
|
304
|
+
return -_to_number(self.unary_expr(), self.text)
|
|
305
|
+
return self.primary()
|
|
306
|
+
|
|
307
|
+
def primary(self) -> Any:
|
|
308
|
+
tok = self._next()
|
|
309
|
+
if tok.kind == "NUM" or tok.kind == "STR":
|
|
310
|
+
return tok.value
|
|
311
|
+
if tok.kind == "IDENT":
|
|
312
|
+
name = tok.value
|
|
313
|
+
if name == "true":
|
|
314
|
+
return True
|
|
315
|
+
if name == "false":
|
|
316
|
+
return False
|
|
317
|
+
if name == "null":
|
|
318
|
+
return None
|
|
319
|
+
nxt = self.cur
|
|
320
|
+
if nxt.kind == "OP" and nxt.value == "(":
|
|
321
|
+
raise ExpressionEvaluationException(
|
|
322
|
+
f"FEEL 子集不支持函数调用 {name!r}(...): {self.text!r}"
|
|
323
|
+
)
|
|
324
|
+
# 变量引用(未定义 -> null,FEEL 缺变量语义)
|
|
325
|
+
return self.vars.get(name)
|
|
326
|
+
if tok.kind == "OP" and tok.value == "(":
|
|
327
|
+
val = self.additive()
|
|
328
|
+
self._expect_op(")")
|
|
329
|
+
return val
|
|
330
|
+
raise ExpressionEvaluationException(
|
|
331
|
+
f"意外的记号 {tok.value!r}(位置 {tok.pos}): {self.text!r}"
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _apply_comparison(op: str, lhs: Any, rhs: Any) -> bool:
|
|
336
|
+
if op == "=":
|
|
337
|
+
return _values_equal(lhs, rhs)
|
|
338
|
+
if op == "!=":
|
|
339
|
+
return not _values_equal(lhs, rhs)
|
|
340
|
+
return _cmp_bool(op, lhs, rhs)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _cmp_bool(op: str, lhs: Any, rhs: Any) -> bool:
|
|
344
|
+
c = _order_cmp(_cmp_key(lhs, f"比较 {op}"), _cmp_key(rhs, f"比较 {op}"))
|
|
345
|
+
return {"<": c < 0, "<=": c <= 0, ">": c > 0, ">=": c >= 0}[op]
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _arith(a: Any, b: Any, op: str, text: str) -> Any:
|
|
349
|
+
if isinstance(a, str) or isinstance(b, str):
|
|
350
|
+
raise ExpressionEvaluationException(
|
|
351
|
+
f"算术 {op} 不支持字符串操作数: {text!r}"
|
|
352
|
+
)
|
|
353
|
+
if a is None or b is None:
|
|
354
|
+
raise ExpressionEvaluationException(f"算术 {op} 操作数为 null: {text!r}")
|
|
355
|
+
if isinstance(a, bool) or isinstance(b, bool):
|
|
356
|
+
raise ExpressionEvaluationException(f"算术 {op} 操作数为布尔: {text!r}")
|
|
357
|
+
try:
|
|
358
|
+
if op == "+":
|
|
359
|
+
return a + b
|
|
360
|
+
if op == "-":
|
|
361
|
+
return a - b
|
|
362
|
+
if op == "*":
|
|
363
|
+
return a * b
|
|
364
|
+
if op == "/":
|
|
365
|
+
if b == 0:
|
|
366
|
+
raise ExpressionEvaluationException(f"除零: {text!r}")
|
|
367
|
+
r = a / b
|
|
368
|
+
return r
|
|
369
|
+
except TypeError as e:
|
|
370
|
+
raise ExpressionEvaluationException(f"算术 {op} 类型错误: {text!r} ({e})") from e
|
|
371
|
+
raise ExpressionEvaluationException(f"未知算术 {op}: {text!r}")
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _to_number(v: Any, text: str) -> float:
|
|
375
|
+
if isinstance(v, bool) or not isinstance(v, (int, float)):
|
|
376
|
+
raise ExpressionEvaluationException(f"一元负号要求数值操作数: {text!r}")
|
|
377
|
+
return v
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
# ---------------------------------------------------------------------------
|
|
381
|
+
# 公共入口
|
|
382
|
+
# ---------------------------------------------------------------------------
|
|
383
|
+
def eval_unary_test(text: Optional[str], input_value: Any) -> bool:
|
|
384
|
+
"""FEEL unaryTests 求值。text=None = 通配(恒命中)。"""
|
|
385
|
+
if text is None:
|
|
386
|
+
return True
|
|
387
|
+
return _Parser(text, {}).eval_unary_test(input_value)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def eval_expression(text: str, variables: dict) -> Any:
|
|
391
|
+
"""FEEL expression 求值(输出单元格 / inputExpression)。"""
|
|
392
|
+
return _Parser(text, variables or {}).eval()
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""engine 包:流程引擎核心(M1 内存版)。
|
|
2
|
+
|
|
3
|
+
- ProcessEngine 门面:deploy / start_process_instance / complete_task / 查询
|
|
4
|
+
- behavior 节点行为分派(start/end/userTask/serviceTask/gateway/flow)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from camunda.engine.process_engine import ProcessEngine
|
|
8
|
+
|
|
9
|
+
__all__ = ["ProcessEngine"]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""节点行为(M1 简化版)。
|
|
2
|
+
|
|
3
|
+
Camunda 用 PvmAtomicOperation 把「进入/离开节点」拆成可拦截的原子操作,
|
|
4
|
+
M1 以函数式 dispatch 呈现同样的语义,主推进循环在 process_engine 内。
|
|
5
|
+
|
|
6
|
+
本模块放**纯逻辑**(便于单测):
|
|
7
|
+
- 排他网关选流规则(条件顺序求值 -> default -> 无条件兜底)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import List, Optional
|
|
13
|
+
|
|
14
|
+
from camunda.common.exceptions import ProcessInstanceException
|
|
15
|
+
from camunda.engine.expression import evaluate_condition
|
|
16
|
+
from camunda.model.bpmn import ExclusiveGateway, SequenceFlow
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def select_exclusive_gateway_flow(
|
|
20
|
+
gw: ExclusiveGateway,
|
|
21
|
+
flows: List[SequenceFlow],
|
|
22
|
+
variables: dict,
|
|
23
|
+
) -> SequenceFlow:
|
|
24
|
+
"""排他网关选流(Camunda 语义):
|
|
25
|
+
1. 按出边顺序取第一条条件为真的边(默认跳过 default)
|
|
26
|
+
2. 无命中时走 default_flow
|
|
27
|
+
3. 再兜底:无条件表达式(condition_expression is None)的出边
|
|
28
|
+
4. 全不中 -> ProcessInstanceException(对应 Camunda 抛 NoOutgoingFlowsFound)
|
|
29
|
+
"""
|
|
30
|
+
candidate = None
|
|
31
|
+
for flow in flows:
|
|
32
|
+
if flow.id == gw.default_flow:
|
|
33
|
+
continue
|
|
34
|
+
if flow.condition_expression is None:
|
|
35
|
+
# 无条件边兜底候选,继续检查后面是否有真条件
|
|
36
|
+
candidate = candidate or flow
|
|
37
|
+
continue
|
|
38
|
+
try:
|
|
39
|
+
if evaluate_condition(flow.condition_expression, variables):
|
|
40
|
+
return flow
|
|
41
|
+
except ProcessInstanceException:
|
|
42
|
+
raise # 表达式错误直接上抛,便于定位
|
|
43
|
+
if gw.default_flow:
|
|
44
|
+
for flow in flows:
|
|
45
|
+
if flow.id == gw.default_flow:
|
|
46
|
+
return flow
|
|
47
|
+
if candidate is not None:
|
|
48
|
+
return candidate
|
|
49
|
+
raise ProcessInstanceException(
|
|
50
|
+
f"排他网关 {gw.id!r} 无任何出边条件满足,且无 default/无条件兜底边"
|
|
51
|
+
)
|