pylitmus 1.0.0__py3-none-any.whl → 1.2.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.
pylitmus/rete/nodes.py ADDED
@@ -0,0 +1,318 @@
1
+ """
2
+ RETE Network Node Implementations.
3
+
4
+ This module contains the core node types for the RETE network:
5
+ - AlphaNode: Tests single conditions
6
+ - AlphaMemory: Stores facts that pass alpha tests
7
+ - BetaNode: Joins results from multiple conditions (AND logic)
8
+ - OrNode: Handles OR logic by tracking any matching branch
9
+ - TerminalNode: Represents rule activation points
10
+ """
11
+
12
+ from dataclasses import dataclass, field
13
+ from typing import Any, Callable, Dict, List, Optional, Set
14
+
15
+ from ..evaluators import EvaluatorFactory
16
+
17
+
18
+ @dataclass
19
+ class ConditionKey:
20
+ """
21
+ Unique key for identifying conditions for sharing.
22
+
23
+ Two conditions with the same key can share an alpha node.
24
+ """
25
+
26
+ field: str
27
+ operator: str
28
+ value: Any
29
+
30
+ def __hash__(self) -> int:
31
+ # Handle unhashable values (like lists)
32
+ value_hash = (
33
+ hash(tuple(self.value))
34
+ if isinstance(self.value, list)
35
+ else hash(self.value)
36
+ )
37
+ return hash((self.field, self.operator, value_hash))
38
+
39
+ def __eq__(self, other: object) -> bool:
40
+ if not isinstance(other, ConditionKey):
41
+ return False
42
+ return (
43
+ self.field == other.field
44
+ and self.operator == other.operator
45
+ and self.value == other.value
46
+ )
47
+
48
+
49
+ class AlphaNode:
50
+ """
51
+ Tests a single condition against incoming facts.
52
+
53
+ Alpha nodes form the first layer of the RETE network and perform
54
+ intra-element tests (tests on a single fact/data item).
55
+ """
56
+
57
+ def __init__(
58
+ self,
59
+ field: str,
60
+ operator: str,
61
+ value: Any,
62
+ condition_key: Optional[ConditionKey] = None,
63
+ ):
64
+ """
65
+ Initialize an alpha node.
66
+
67
+ Args:
68
+ field: The field path to test (supports nested paths like "user.age")
69
+ operator: The comparison operator (equals, greater_than, etc.)
70
+ value: The value to compare against
71
+ condition_key: Optional key for condition sharing
72
+ """
73
+ self.field = field
74
+ self.operator = operator
75
+ self.value = value
76
+ self.condition_key = condition_key or ConditionKey(field, operator, value)
77
+
78
+ # Get the evaluator for this operator
79
+ self._evaluator = EvaluatorFactory.get(operator)
80
+
81
+ # Memory of facts that passed this condition
82
+ self.memory: List[Dict[str, Any]] = []
83
+
84
+ # Child nodes to notify when a fact passes
85
+ self.children: List["BetaNode"] = []
86
+
87
+ # Track the last evaluation result for the current fact
88
+ self._last_result: Optional[bool] = None
89
+
90
+ def evaluate(self, data: Dict[str, Any]) -> bool:
91
+ """
92
+ Test if data passes this condition.
93
+
94
+ Args:
95
+ data: The fact/data to test
96
+
97
+ Returns:
98
+ True if the condition passes, False otherwise
99
+ """
100
+ field_value = self._get_nested_value(data, self.field)
101
+ result = self._evaluator.evaluate(field_value, self.value)
102
+ self._last_result = result
103
+ return result
104
+
105
+ def activate(self, data: Dict[str, Any]) -> bool:
106
+ """
107
+ Activate this node with a fact.
108
+
109
+ If the fact passes the condition, it's stored in memory and
110
+ child nodes are notified.
111
+
112
+ Args:
113
+ data: The fact/data to process
114
+
115
+ Returns:
116
+ True if the fact passed the condition
117
+ """
118
+ if self.evaluate(data):
119
+ self.memory.append(data)
120
+ # Notify children
121
+ for child in self.children:
122
+ child.alpha_activation(self, data)
123
+ return True
124
+ return False
125
+
126
+ def clear_memory(self) -> None:
127
+ """Clear the alpha memory."""
128
+ self.memory.clear()
129
+ self._last_result = None
130
+
131
+ def _get_nested_value(self, data: Dict[str, Any], field_path: str) -> Any:
132
+ """
133
+ Get a value from nested data using dot notation.
134
+
135
+ Args:
136
+ data: The data dictionary
137
+ field_path: Path like "user.address.city"
138
+
139
+ Returns:
140
+ The value at the path, or None if not found
141
+ """
142
+ keys = field_path.split(".")
143
+ value = data
144
+
145
+ for key in keys:
146
+ if isinstance(value, dict):
147
+ value = value.get(key)
148
+ else:
149
+ return None
150
+
151
+ if value is None:
152
+ return None
153
+
154
+ return value
155
+
156
+ def __repr__(self) -> str:
157
+ return f"AlphaNode({self.field} {self.operator} {self.value})"
158
+
159
+
160
+ class BetaNode:
161
+ """
162
+ Joins results from multiple alpha nodes (AND logic).
163
+
164
+ Beta nodes form the second layer of the RETE network and perform
165
+ inter-element tests (combining results from multiple conditions).
166
+ """
167
+
168
+ def __init__(self, required_alphas: List[AlphaNode]):
169
+ """
170
+ Initialize a beta node.
171
+
172
+ Args:
173
+ required_alphas: List of alpha nodes that must all pass
174
+ """
175
+ self.required_alphas = required_alphas
176
+ self.alpha_set = set(id(a) for a in required_alphas)
177
+
178
+ # Track which alphas have been satisfied for current evaluation
179
+ self._satisfied: Set[int] = set()
180
+
181
+ # Memory of complete matches
182
+ self.memory: List[Dict[str, Any]] = []
183
+
184
+ # Child nodes (terminal nodes or other beta nodes)
185
+ self.children: List["TerminalNode"] = []
186
+
187
+ def alpha_activation(self, alpha: AlphaNode, data: Dict[str, Any]) -> None:
188
+ """
189
+ Called when an alpha node passes.
190
+
191
+ Args:
192
+ alpha: The alpha node that passed
193
+ data: The fact that passed the alpha test
194
+ """
195
+ self._satisfied.add(id(alpha))
196
+
197
+ def is_satisfied(self) -> bool:
198
+ """Check if all required alpha conditions are satisfied."""
199
+ return self._satisfied == self.alpha_set
200
+
201
+ def activate(self, data: Dict[str, Any]) -> bool:
202
+ """
203
+ Check if this beta node is fully satisfied and activate children.
204
+
205
+ Args:
206
+ data: The fact being evaluated
207
+
208
+ Returns:
209
+ True if all conditions are satisfied
210
+ """
211
+ if self.is_satisfied():
212
+ self.memory.append(data)
213
+ for child in self.children:
214
+ child.activate(data)
215
+ return True
216
+ return False
217
+
218
+ def clear(self) -> None:
219
+ """Clear state for new evaluation."""
220
+ self._satisfied.clear()
221
+ self.memory.clear()
222
+
223
+ def __repr__(self) -> str:
224
+ return f"BetaNode(requires={len(self.required_alphas)} alphas)"
225
+
226
+
227
+ class OrNode:
228
+ """
229
+ Handles OR logic by tracking if any branch matches.
230
+
231
+ Unlike BetaNode (AND), OrNode passes if ANY of its child conditions pass.
232
+ """
233
+
234
+ def __init__(self, branches: List[List[AlphaNode]]):
235
+ """
236
+ Initialize an OR node.
237
+
238
+ Args:
239
+ branches: List of condition branches, where each branch is
240
+ a list of alpha nodes that must all pass (AND within branch)
241
+ """
242
+ self.branches = branches
243
+
244
+ # Track which branches have been satisfied
245
+ self._satisfied_branches: Set[int] = set()
246
+
247
+ # Child nodes
248
+ self.children: List["TerminalNode"] = []
249
+
250
+ def branch_satisfied(self, branch_index: int) -> None:
251
+ """Mark a branch as satisfied."""
252
+ self._satisfied_branches.add(branch_index)
253
+
254
+ def is_satisfied(self) -> bool:
255
+ """Check if any branch is satisfied."""
256
+ return len(self._satisfied_branches) > 0
257
+
258
+ def activate(self, data: Dict[str, Any]) -> bool:
259
+ """
260
+ Check if any branch is satisfied and activate children.
261
+
262
+ Args:
263
+ data: The fact being evaluated
264
+
265
+ Returns:
266
+ True if any branch is satisfied
267
+ """
268
+ if self.is_satisfied():
269
+ for child in self.children:
270
+ child.activate(data)
271
+ return True
272
+ return False
273
+
274
+ def clear(self) -> None:
275
+ """Clear state for new evaluation."""
276
+ self._satisfied_branches.clear()
277
+
278
+ def __repr__(self) -> str:
279
+ return f"OrNode(branches={len(self.branches)})"
280
+
281
+
282
+ @dataclass
283
+ class TerminalNode:
284
+ """
285
+ Represents a rule activation point.
286
+
287
+ When a terminal node is activated, it means all conditions for the
288
+ associated rule have been satisfied.
289
+ """
290
+
291
+ rule_code: str
292
+ rule_name: str
293
+ score: int
294
+ severity: str
295
+ category: str
296
+ description: str
297
+
298
+ # Track activation state
299
+ activated: bool = False
300
+ activation_data: Optional[Dict[str, Any]] = None
301
+
302
+ def activate(self, data: Dict[str, Any]) -> None:
303
+ """
304
+ Activate this terminal node (rule triggered).
305
+
306
+ Args:
307
+ data: The fact that triggered the rule
308
+ """
309
+ self.activated = True
310
+ self.activation_data = data
311
+
312
+ def clear(self) -> None:
313
+ """Clear activation state."""
314
+ self.activated = False
315
+ self.activation_data = None
316
+
317
+ def __repr__(self) -> str:
318
+ return f"TerminalNode({self.rule_code}, activated={self.activated})"
pylitmus/types.py CHANGED
@@ -3,13 +3,14 @@ Core type definitions for the CMAP Rules Engine.
3
3
  """
4
4
 
5
5
  from dataclasses import dataclass, field
6
- from typing import Any, Dict, List, Optional
7
- from enum import Enum
8
6
  from datetime import datetime
7
+ from enum import Enum
8
+ from typing import Any, Dict, List, Optional
9
9
 
10
10
 
11
11
  class Operator(str, Enum):
12
12
  """Supported condition operators."""
13
+
13
14
  EQUALS = "equals"
14
15
  NOT_EQUALS = "not_equals"
15
16
  GREATER_THAN = "greater_than"
@@ -32,6 +33,7 @@ class Operator(str, Enum):
32
33
 
33
34
  class Severity(str, Enum):
34
35
  """Rule severity levels."""
36
+
35
37
  LOW = "LOW"
36
38
  MEDIUM = "MEDIUM"
37
39
  HIGH = "HIGH"
@@ -41,6 +43,7 @@ class Severity(str, Enum):
41
43
  @dataclass
42
44
  class Rule:
43
45
  """A rule definition."""
46
+
44
47
  code: str
45
48
  name: str
46
49
  description: str
@@ -73,6 +76,7 @@ class Rule:
73
76
  @dataclass
74
77
  class RuleResult:
75
78
  """Result of evaluating a single rule."""
79
+
76
80
  rule_code: str
77
81
  rule_name: str
78
82
  triggered: bool
@@ -82,11 +86,33 @@ class RuleResult:
82
86
  explanation: str
83
87
 
84
88
 
89
+ @dataclass
90
+ class DecisionTier:
91
+ """
92
+ A decision tier definition with score range.
93
+
94
+ Example:
95
+ DecisionTier(name="APPROVE", min_score=0, max_score=30)
96
+ DecisionTier(name="REVIEW", min_score=30, max_score=70)
97
+ DecisionTier(name="FLAG", min_score=70, max_score=100)
98
+ """
99
+
100
+ name: str
101
+ min_score: int
102
+ max_score: int
103
+ description: Optional[str] = None
104
+
105
+ def matches(self, score: int) -> bool:
106
+ """Check if score falls within this tier's range (min inclusive, max exclusive)."""
107
+ return self.min_score <= score < self.max_score
108
+
109
+
85
110
  @dataclass
86
111
  class AssessmentResult:
87
112
  """Complete assessment result."""
113
+
88
114
  total_score: int
89
- decision: str
115
+ decision: Optional[str]
90
116
  triggered_rules: List[RuleResult] = field(default_factory=list)
91
117
  all_rules_evaluated: int = 0
92
118
  processing_time_ms: float = 0.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pylitmus
3
- Version: 1.0.0
3
+ Version: 1.2.0
4
4
  Summary: A high-performance rules engine for Python - evaluate data against configurable rules and get clear verdicts
5
5
  Project-URL: Homepage, https://github.com/yourorg/pylitmus
6
6
  Project-URL: Documentation, https://pylitmus.readthedocs.io/
@@ -1,8 +1,8 @@
1
- pylitmus/__init__.py,sha256=MivCPQfBek8sZneeSOBIoYA575Se4pUlunP9uBvS_s0,1843
2
- pylitmus/engine.py,sha256=j-5w4AhH3FVaUnvTnJQLGREWfV8fFjaGifTOIkDI2YE,7371
1
+ pylitmus/__init__.py,sha256=JFhGwboRoytGdSWpRMLnC21aQoB-gfu5ahEh7Fd7FDA,1877
2
+ pylitmus/engine.py,sha256=TTS4eumtMdljsXFhrTvGuG3qql-ChrQPgzRawgvvcZg,13932
3
3
  pylitmus/exceptions.py,sha256=c5AcD1LXAH52NDWTVp3xHp4vXaHwQW75n6JuaJ4D0Dk,653
4
- pylitmus/factory.py,sha256=BrhFtm0iuMg7uFlaNb5QnFTwPGkyozeVRNnEAhDWqjw,5143
5
- pylitmus/types.py,sha256=bqU4dsBfzlcBnbiLjD90zQ8LW9nJZqdj9VDcZYcYvI8,2274
4
+ pylitmus/factory.py,sha256=k-HrZhKIkWXfPzyW0cCI1ZKr-Z_7aYC_S4wocvE-SgI,6211
5
+ pylitmus/types.py,sha256=d0BgF5K7EpGSvpeH8B5sveqqDIPKhyZ1DYrt5gTGnf0,2875
6
6
  pylitmus/conditions/__init__.py,sha256=jGfOpD4cLBk2lpbnfPyNtPFbUAs0lkMO9AJ5FO4rHgc,303
7
7
  pylitmus/conditions/base.py,sha256=OaNyyf4eCy2kUYE2xEw4b2XdTzeHlF3r0XB18CSEshg,1261
8
8
  pylitmus/conditions/builder.py,sha256=kymvWoMNH_ss1B1u7hs49InNjW8WWS8kyp5-rCdVk_c,2924
@@ -13,7 +13,7 @@ pylitmus/evaluators/base.py,sha256=POX8EyPTPYyEvZgBIuVWNqvS0AYR-m3X6uQXirHXWCQ,6
13
13
  pylitmus/evaluators/factory.py,sha256=_lMB9dWBu9qBS3tF_Xli5hBkeaLhzF0dZszJaYJdq00,11593
14
14
  pylitmus/integrations/__init__.py,sha256=DFmjG3bvDpHuhXA3nMXyeu9UXObylxjqYa-5DXgWJW4,58
15
15
  pylitmus/integrations/flask/__init__.py,sha256=Blahqk76BmYGgaFfuw8ZX-V4-uIay-nDni5gVgOeBtg,161
16
- pylitmus/integrations/flask/extension.py,sha256=KZyEZCPCeC4qdwAC0YQH7BeqmkV1FTpK5mwkgX_yiPM,6564
16
+ pylitmus/integrations/flask/extension.py,sha256=Uq41kE0OB_SaoBNi5QZvUTnjXSna_1EbAsXPJOOKNCc,7853
17
17
  pylitmus/patterns/__init__.py,sha256=3jjbMmo4BDLIE9IYPKApgahgxjUHOgqlaE7I9s7KLHA,458
18
18
  pylitmus/patterns/base.py,sha256=7AyyZvYtpLOGNt54A2XVjTdgsRoyAM2vQUDBifZIzm0,549
19
19
  pylitmus/patterns/engine.py,sha256=I_52LseNZjwPDpZc0jVuq3sw1tjiEd2YPt-noqpWe7A,2264
@@ -22,6 +22,10 @@ pylitmus/patterns/fuzzy.py,sha256=EVNHgWVifb3KAnUlwair4gZcOwtbjv7W_6tZzCQXkFU,18
22
22
  pylitmus/patterns/glob.py,sha256=y7w4cI6X0dACivWoZKSIyRcOMWfrE4YtAPV_PtSELQA,928
23
23
  pylitmus/patterns/range.py,sha256=ARhPipHn7hyLG7LaPVp4vqz_7oNgrBiwPlEUxFCvjxk,1441
24
24
  pylitmus/patterns/regex.py,sha256=RaElvD0bsmLN3IknVz6NzfCDmDFyQI6dPmvpL-j93X0,1360
25
+ pylitmus/rete/__init__.py,sha256=d0YU7fF_SdOfMoTplwGrYRF4eCbyEav0N8d2aqKRYOo,809
26
+ pylitmus/rete/compiler.py,sha256=nLMee3oDJ-34JHvMZtnfHnimjDmA8oMzsmqhhXdG9s8,11231
27
+ pylitmus/rete/network.py,sha256=kZt-NW5GqGul9f42S5sG_qRJDpoQPOQ2vd81PxNM7tI,8105
28
+ pylitmus/rete/nodes.py,sha256=3Z1p0yvaOW231CdVviN_ysrwnpoNFFj0sJr-UMv4jBQ,8993
25
29
  pylitmus/storage/__init__.py,sha256=-H455-ISCkaFltpPIoqFuJTPc2bMDYN9o-mBnnRbEi0,410
26
30
  pylitmus/storage/base.py,sha256=SuaT--Ao90R4nxQc8_f63Xp9TzvQ70wYFBEK_1DUPiQ,1620
27
31
  pylitmus/storage/cached.py,sha256=o3a_Fq6DqfzJ-61GE3G2KNcNJrxaweddWRWLvpI24eI,5465
@@ -33,7 +37,7 @@ pylitmus/strategies/base.py,sha256=Fq9JgUVdbuPlwEk8pdSyiLZ-RBRm_3DnJkWAWCtGmRk,5
33
37
  pylitmus/strategies/max.py,sha256=BUjZ9P3DUrDShcvy7_0pVo3Pa9lSLJI5cX9bRELUAlU,553
34
38
  pylitmus/strategies/sum.py,sha256=kA0MHiYSpLa-lYR0nMmTGGKMMKm58jrqkuC9a5sTy0U,831
35
39
  pylitmus/strategies/weighted.py,sha256=GqTt8yqS3m2GYgX9nElhjk7p8ah6lAFrtpZiCDHTSQg,1019
36
- pylitmus-1.0.0.dist-info/METADATA,sha256=el-ecW2t9Lqg9vMFppw7xD5m7guNpULSW6PAKj2axP0,11220
37
- pylitmus-1.0.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
38
- pylitmus-1.0.0.dist-info/licenses/LICENSE,sha256=BtvJ2KboQyfSvjSh9-nxRiVFK4VjxFWXe5x4sFpe7Xw,1066
39
- pylitmus-1.0.0.dist-info/RECORD,,
40
+ pylitmus-1.2.0.dist-info/METADATA,sha256=uPfL-CWlizBXtkWUz56Kz4TGn5hqnJ_Q-1s73ZZFmII,11220
41
+ pylitmus-1.2.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
42
+ pylitmus-1.2.0.dist-info/licenses/LICENSE,sha256=BtvJ2KboQyfSvjSh9-nxRiVFK4VjxFWXe5x4sFpe7Xw,1066
43
+ pylitmus-1.2.0.dist-info/RECORD,,