pylitmus 1.0.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/__init__.py +76 -0
- pylitmus/conditions/__init__.py +15 -0
- pylitmus/conditions/base.py +53 -0
- pylitmus/conditions/builder.py +92 -0
- pylitmus/conditions/composite.py +52 -0
- pylitmus/conditions/simple.py +62 -0
- pylitmus/engine.py +244 -0
- pylitmus/evaluators/__init__.py +11 -0
- pylitmus/evaluators/base.py +27 -0
- pylitmus/evaluators/factory.py +372 -0
- pylitmus/exceptions.py +39 -0
- pylitmus/factory.py +179 -0
- pylitmus/integrations/__init__.py +3 -0
- pylitmus/integrations/flask/__init__.py +10 -0
- pylitmus/integrations/flask/extension.py +234 -0
- pylitmus/patterns/__init__.py +21 -0
- pylitmus/patterns/base.py +25 -0
- pylitmus/patterns/engine.py +82 -0
- pylitmus/patterns/exact.py +34 -0
- pylitmus/patterns/fuzzy.py +69 -0
- pylitmus/patterns/glob.py +38 -0
- pylitmus/patterns/range.py +53 -0
- pylitmus/patterns/regex.py +51 -0
- pylitmus/storage/__init__.py +17 -0
- pylitmus/storage/base.py +78 -0
- pylitmus/storage/cached.py +167 -0
- pylitmus/storage/database.py +181 -0
- pylitmus/storage/file.py +143 -0
- pylitmus/storage/memory.py +107 -0
- pylitmus/strategies/__init__.py +15 -0
- pylitmus/strategies/base.py +25 -0
- pylitmus/strategies/max.py +26 -0
- pylitmus/strategies/sum.py +36 -0
- pylitmus/strategies/weighted.py +45 -0
- pylitmus/types.py +93 -0
- pylitmus-1.0.0.dist-info/METADATA +459 -0
- pylitmus-1.0.0.dist-info/RECORD +39 -0
- pylitmus-1.0.0.dist-info/WHEEL +4 -0
- pylitmus-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Evaluator factory for creating condition evaluators.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from datetime import datetime, timedelta
|
|
7
|
+
from functools import lru_cache
|
|
8
|
+
from typing import Any, Dict, Optional, Type, Union
|
|
9
|
+
|
|
10
|
+
from ..exceptions import UnknownOperatorError
|
|
11
|
+
from .base import Evaluator
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class EqualsEvaluator(Evaluator):
|
|
15
|
+
"""Evaluator for equals comparison."""
|
|
16
|
+
|
|
17
|
+
def evaluate(self, field_value, compare_value, case_sensitive=True):
|
|
18
|
+
if isinstance(field_value, str) and isinstance(compare_value, str):
|
|
19
|
+
if not case_sensitive:
|
|
20
|
+
return field_value.lower() == compare_value.lower()
|
|
21
|
+
return field_value == compare_value
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class NotEqualsEvaluator(Evaluator):
|
|
25
|
+
"""Evaluator for not equals comparison."""
|
|
26
|
+
|
|
27
|
+
def evaluate(self, field_value, compare_value, case_sensitive=True):
|
|
28
|
+
if isinstance(field_value, str) and isinstance(compare_value, str):
|
|
29
|
+
if not case_sensitive:
|
|
30
|
+
return field_value.lower() != compare_value.lower()
|
|
31
|
+
return field_value != compare_value
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class GreaterThanEvaluator(Evaluator):
|
|
35
|
+
"""Evaluator for greater than comparison."""
|
|
36
|
+
|
|
37
|
+
def evaluate(self, field_value, compare_value, case_sensitive=True):
|
|
38
|
+
try:
|
|
39
|
+
return field_value > compare_value
|
|
40
|
+
except TypeError:
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class GreaterThanOrEqualEvaluator(Evaluator):
|
|
45
|
+
"""Evaluator for greater than or equal comparison."""
|
|
46
|
+
|
|
47
|
+
def evaluate(self, field_value, compare_value, case_sensitive=True):
|
|
48
|
+
try:
|
|
49
|
+
return field_value >= compare_value
|
|
50
|
+
except TypeError:
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class LessThanEvaluator(Evaluator):
|
|
55
|
+
"""Evaluator for less than comparison."""
|
|
56
|
+
|
|
57
|
+
def evaluate(self, field_value, compare_value, case_sensitive=True):
|
|
58
|
+
try:
|
|
59
|
+
return field_value < compare_value
|
|
60
|
+
except TypeError:
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class LessThanOrEqualEvaluator(Evaluator):
|
|
65
|
+
"""Evaluator for less than or equal comparison."""
|
|
66
|
+
|
|
67
|
+
def evaluate(self, field_value, compare_value, case_sensitive=True):
|
|
68
|
+
try:
|
|
69
|
+
return field_value <= compare_value
|
|
70
|
+
except TypeError:
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class InEvaluator(Evaluator):
|
|
75
|
+
"""Evaluator for in collection comparison."""
|
|
76
|
+
|
|
77
|
+
def evaluate(self, field_value, compare_value, case_sensitive=True):
|
|
78
|
+
if not isinstance(compare_value, (list, tuple, set)):
|
|
79
|
+
return False
|
|
80
|
+
if isinstance(field_value, str) and not case_sensitive:
|
|
81
|
+
return field_value.lower() in [
|
|
82
|
+
v.lower() if isinstance(v, str) else v for v in compare_value
|
|
83
|
+
]
|
|
84
|
+
return field_value in compare_value
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class NotInEvaluator(Evaluator):
|
|
88
|
+
"""Evaluator for not in collection comparison."""
|
|
89
|
+
|
|
90
|
+
def evaluate(self, field_value, compare_value, case_sensitive=True):
|
|
91
|
+
if not isinstance(compare_value, (list, tuple, set)):
|
|
92
|
+
return True
|
|
93
|
+
if isinstance(field_value, str) and not case_sensitive:
|
|
94
|
+
return field_value.lower() not in [
|
|
95
|
+
v.lower() if isinstance(v, str) else v for v in compare_value
|
|
96
|
+
]
|
|
97
|
+
return field_value not in compare_value
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class ContainsEvaluator(Evaluator):
|
|
101
|
+
"""Evaluator for contains (substring) comparison."""
|
|
102
|
+
|
|
103
|
+
def evaluate(self, field_value, compare_value, case_sensitive=True):
|
|
104
|
+
if field_value is None or compare_value is None:
|
|
105
|
+
return False
|
|
106
|
+
field_str = str(field_value)
|
|
107
|
+
compare_str = str(compare_value)
|
|
108
|
+
if not case_sensitive:
|
|
109
|
+
return compare_str.lower() in field_str.lower()
|
|
110
|
+
return compare_str in field_str
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class StartsWithEvaluator(Evaluator):
|
|
114
|
+
"""Evaluator for starts with comparison."""
|
|
115
|
+
|
|
116
|
+
def evaluate(self, field_value, compare_value, case_sensitive=True):
|
|
117
|
+
if field_value is None or compare_value is None:
|
|
118
|
+
return False
|
|
119
|
+
field_str = str(field_value)
|
|
120
|
+
compare_str = str(compare_value)
|
|
121
|
+
if not case_sensitive:
|
|
122
|
+
return field_str.lower().startswith(compare_str.lower())
|
|
123
|
+
return field_str.startswith(compare_str)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class EndsWithEvaluator(Evaluator):
|
|
127
|
+
"""Evaluator for ends with comparison."""
|
|
128
|
+
|
|
129
|
+
def evaluate(self, field_value, compare_value, case_sensitive=True):
|
|
130
|
+
if field_value is None or compare_value is None:
|
|
131
|
+
return False
|
|
132
|
+
field_str = str(field_value)
|
|
133
|
+
compare_str = str(compare_value)
|
|
134
|
+
if not case_sensitive:
|
|
135
|
+
return field_str.lower().endswith(compare_str.lower())
|
|
136
|
+
return field_str.endswith(compare_str)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class IsNullEvaluator(Evaluator):
|
|
140
|
+
"""Evaluator for null check."""
|
|
141
|
+
|
|
142
|
+
def evaluate(self, field_value, compare_value, case_sensitive=True):
|
|
143
|
+
return field_value is None
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class IsNotNullEvaluator(Evaluator):
|
|
147
|
+
"""Evaluator for not null check."""
|
|
148
|
+
|
|
149
|
+
def evaluate(self, field_value, compare_value, case_sensitive=True):
|
|
150
|
+
return field_value is not None
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class MatchesRegexEvaluator(Evaluator):
|
|
154
|
+
"""Evaluator for regex pattern matching."""
|
|
155
|
+
|
|
156
|
+
@staticmethod
|
|
157
|
+
@lru_cache(maxsize=1000)
|
|
158
|
+
def _compile_pattern(pattern: str, flags: int = 0) -> re.Pattern:
|
|
159
|
+
"""Compile and cache regex pattern."""
|
|
160
|
+
return re.compile(pattern, flags)
|
|
161
|
+
|
|
162
|
+
def evaluate(self, field_value, compare_value, case_sensitive=True):
|
|
163
|
+
if field_value is None:
|
|
164
|
+
return False
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
flags = 0 if case_sensitive else re.IGNORECASE
|
|
168
|
+
pattern = self._compile_pattern(compare_value, flags)
|
|
169
|
+
return bool(pattern.search(str(field_value)))
|
|
170
|
+
except (re.error, TypeError):
|
|
171
|
+
return False
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class BetweenEvaluator(Evaluator):
|
|
175
|
+
"""Evaluator for range check (inclusive)."""
|
|
176
|
+
|
|
177
|
+
def evaluate(self, field_value, compare_value, case_sensitive=True):
|
|
178
|
+
if field_value is None:
|
|
179
|
+
return False
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
# compare_value should be [min, max] or {"min": x, "max": y}
|
|
183
|
+
if isinstance(compare_value, (list, tuple)) and len(compare_value) == 2:
|
|
184
|
+
min_val, max_val = compare_value
|
|
185
|
+
elif isinstance(compare_value, dict):
|
|
186
|
+
min_val = compare_value.get("min")
|
|
187
|
+
max_val = compare_value.get("max")
|
|
188
|
+
else:
|
|
189
|
+
return False
|
|
190
|
+
|
|
191
|
+
return min_val <= field_value <= max_val
|
|
192
|
+
except (TypeError, KeyError):
|
|
193
|
+
return False
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class WithinDaysEvaluator(Evaluator):
|
|
197
|
+
"""Evaluator for checking if date is within N days from now."""
|
|
198
|
+
|
|
199
|
+
def _parse_date(self, value: Any) -> Optional[datetime]:
|
|
200
|
+
"""Parse various date formats."""
|
|
201
|
+
if value is None:
|
|
202
|
+
return None
|
|
203
|
+
|
|
204
|
+
if isinstance(value, datetime):
|
|
205
|
+
return value
|
|
206
|
+
|
|
207
|
+
if isinstance(value, str):
|
|
208
|
+
# Try common formats
|
|
209
|
+
formats = [
|
|
210
|
+
"%Y-%m-%d",
|
|
211
|
+
"%Y-%m-%dT%H:%M:%S",
|
|
212
|
+
"%Y-%m-%dT%H:%M:%S.%f",
|
|
213
|
+
"%Y-%m-%dT%H:%M:%SZ",
|
|
214
|
+
"%Y-%m-%dT%H:%M:%S.%fZ",
|
|
215
|
+
"%d/%m/%Y",
|
|
216
|
+
"%m/%d/%Y",
|
|
217
|
+
]
|
|
218
|
+
for fmt in formats:
|
|
219
|
+
try:
|
|
220
|
+
return datetime.strptime(value, fmt)
|
|
221
|
+
except ValueError:
|
|
222
|
+
continue
|
|
223
|
+
|
|
224
|
+
return None
|
|
225
|
+
|
|
226
|
+
def evaluate(self, field_value, compare_value, case_sensitive=True):
|
|
227
|
+
field_date = self._parse_date(field_value)
|
|
228
|
+
if not field_date:
|
|
229
|
+
return False
|
|
230
|
+
|
|
231
|
+
try:
|
|
232
|
+
days = int(compare_value)
|
|
233
|
+
cutoff = datetime.utcnow() - timedelta(days=days)
|
|
234
|
+
return field_date >= cutoff
|
|
235
|
+
except (ValueError, TypeError):
|
|
236
|
+
return False
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class BeforeEvaluator(Evaluator):
|
|
240
|
+
"""Evaluator for checking if date is before another date."""
|
|
241
|
+
|
|
242
|
+
def _parse_date(self, value: Any) -> Optional[datetime]:
|
|
243
|
+
"""Parse various date formats."""
|
|
244
|
+
if value is None:
|
|
245
|
+
return None
|
|
246
|
+
|
|
247
|
+
if isinstance(value, datetime):
|
|
248
|
+
return value
|
|
249
|
+
|
|
250
|
+
if isinstance(value, str):
|
|
251
|
+
formats = [
|
|
252
|
+
"%Y-%m-%d",
|
|
253
|
+
"%Y-%m-%dT%H:%M:%S",
|
|
254
|
+
"%Y-%m-%dT%H:%M:%S.%f",
|
|
255
|
+
"%Y-%m-%dT%H:%M:%SZ",
|
|
256
|
+
]
|
|
257
|
+
for fmt in formats:
|
|
258
|
+
try:
|
|
259
|
+
return datetime.strptime(value, fmt)
|
|
260
|
+
except ValueError:
|
|
261
|
+
continue
|
|
262
|
+
|
|
263
|
+
return None
|
|
264
|
+
|
|
265
|
+
def evaluate(self, field_value, compare_value, case_sensitive=True):
|
|
266
|
+
field_date = self._parse_date(field_value)
|
|
267
|
+
compare_date = self._parse_date(compare_value)
|
|
268
|
+
|
|
269
|
+
if field_date is None or compare_date is None:
|
|
270
|
+
return False
|
|
271
|
+
|
|
272
|
+
return field_date < compare_date
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
class AfterEvaluator(Evaluator):
|
|
276
|
+
"""Evaluator for checking if date is after another date."""
|
|
277
|
+
|
|
278
|
+
def _parse_date(self, value: Any) -> Optional[datetime]:
|
|
279
|
+
"""Parse various date formats."""
|
|
280
|
+
if value is None:
|
|
281
|
+
return None
|
|
282
|
+
|
|
283
|
+
if isinstance(value, datetime):
|
|
284
|
+
return value
|
|
285
|
+
|
|
286
|
+
if isinstance(value, str):
|
|
287
|
+
formats = [
|
|
288
|
+
"%Y-%m-%d",
|
|
289
|
+
"%Y-%m-%dT%H:%M:%S",
|
|
290
|
+
"%Y-%m-%dT%H:%M:%S.%f",
|
|
291
|
+
"%Y-%m-%dT%H:%M:%SZ",
|
|
292
|
+
]
|
|
293
|
+
for fmt in formats:
|
|
294
|
+
try:
|
|
295
|
+
return datetime.strptime(value, fmt)
|
|
296
|
+
except ValueError:
|
|
297
|
+
continue
|
|
298
|
+
|
|
299
|
+
return None
|
|
300
|
+
|
|
301
|
+
def evaluate(self, field_value, compare_value, case_sensitive=True):
|
|
302
|
+
field_date = self._parse_date(field_value)
|
|
303
|
+
compare_date = self._parse_date(compare_value)
|
|
304
|
+
|
|
305
|
+
if field_date is None or compare_date is None:
|
|
306
|
+
return False
|
|
307
|
+
|
|
308
|
+
return field_date > compare_date
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
class EvaluatorFactory:
|
|
312
|
+
"""Factory for creating condition evaluators."""
|
|
313
|
+
|
|
314
|
+
_evaluators: Dict[str, Type[Evaluator]] = {
|
|
315
|
+
# Comparison
|
|
316
|
+
"equals": EqualsEvaluator,
|
|
317
|
+
"not_equals": NotEqualsEvaluator,
|
|
318
|
+
"greater_than": GreaterThanEvaluator,
|
|
319
|
+
"greater_than_or_equal": GreaterThanOrEqualEvaluator,
|
|
320
|
+
"less_than": LessThanEvaluator,
|
|
321
|
+
"less_than_or_equal": LessThanOrEqualEvaluator,
|
|
322
|
+
"between": BetweenEvaluator,
|
|
323
|
+
# Collection
|
|
324
|
+
"in": InEvaluator,
|
|
325
|
+
"not_in": NotInEvaluator,
|
|
326
|
+
# String
|
|
327
|
+
"contains": ContainsEvaluator,
|
|
328
|
+
"starts_with": StartsWithEvaluator,
|
|
329
|
+
"ends_with": EndsWithEvaluator,
|
|
330
|
+
"matches_regex": MatchesRegexEvaluator,
|
|
331
|
+
# Null
|
|
332
|
+
"is_null": IsNullEvaluator,
|
|
333
|
+
"is_not_null": IsNotNullEvaluator,
|
|
334
|
+
# Temporal
|
|
335
|
+
"within_days": WithinDaysEvaluator,
|
|
336
|
+
"before": BeforeEvaluator,
|
|
337
|
+
"after": AfterEvaluator,
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
@classmethod
|
|
341
|
+
def get(cls, operator: str) -> Evaluator:
|
|
342
|
+
"""
|
|
343
|
+
Get an evaluator for the given operator.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
operator: Operator name
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
Evaluator instance
|
|
350
|
+
|
|
351
|
+
Raises:
|
|
352
|
+
UnknownOperatorError: If operator is not registered
|
|
353
|
+
"""
|
|
354
|
+
if operator not in cls._evaluators:
|
|
355
|
+
raise UnknownOperatorError(f"Unknown operator: {operator}")
|
|
356
|
+
return cls._evaluators[operator]()
|
|
357
|
+
|
|
358
|
+
@classmethod
|
|
359
|
+
def register(cls, operator: str, evaluator_class: Type[Evaluator]) -> None:
|
|
360
|
+
"""
|
|
361
|
+
Register a custom evaluator.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
operator: Operator name
|
|
365
|
+
evaluator_class: Evaluator class
|
|
366
|
+
"""
|
|
367
|
+
cls._evaluators[operator] = evaluator_class
|
|
368
|
+
|
|
369
|
+
@classmethod
|
|
370
|
+
def get_supported_operators(cls) -> list:
|
|
371
|
+
"""Get list of supported operators."""
|
|
372
|
+
return list(cls._evaluators.keys())
|
pylitmus/exceptions.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom exceptions for the CMAP Rules Engine.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class RuleEngineError(Exception):
|
|
7
|
+
"""Base exception for rule engine errors."""
|
|
8
|
+
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class UnknownOperatorError(RuleEngineError):
|
|
13
|
+
"""Raised when an unknown operator is used."""
|
|
14
|
+
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ConditionError(RuleEngineError):
|
|
19
|
+
"""Raised when condition evaluation fails."""
|
|
20
|
+
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class StorageError(RuleEngineError):
|
|
25
|
+
"""Raised when storage operations fail."""
|
|
26
|
+
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ConfigurationError(RuleEngineError):
|
|
31
|
+
"""Raised when configuration is invalid."""
|
|
32
|
+
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class EvaluationError(RuleEngineError):
|
|
37
|
+
"""Raised when rule evaluation fails."""
|
|
38
|
+
|
|
39
|
+
pass
|
pylitmus/factory.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Factory functions for creating RuleEngine instances with sensible defaults.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import TYPE_CHECKING, Dict, List, Optional, Union
|
|
8
|
+
|
|
9
|
+
from .engine import RuleEngine
|
|
10
|
+
from .storage import (
|
|
11
|
+
CachedRuleRepository,
|
|
12
|
+
DatabaseRuleRepository,
|
|
13
|
+
FileRuleRepository,
|
|
14
|
+
InMemoryRuleRepository,
|
|
15
|
+
RuleRepository,
|
|
16
|
+
)
|
|
17
|
+
from .strategies import MaxStrategy, ScoringStrategy, SumStrategy, WeightedStrategy
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from .types import Rule
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def create_engine(
|
|
24
|
+
# Storage
|
|
25
|
+
storage_backend: str = "memory",
|
|
26
|
+
database_url: Optional[str] = None,
|
|
27
|
+
rules_file: Optional[str] = None,
|
|
28
|
+
rules: Optional[List["Rule"]] = None,
|
|
29
|
+
repository: Optional[RuleRepository] = None,
|
|
30
|
+
# Caching
|
|
31
|
+
cache_backend: str = "memory",
|
|
32
|
+
cache_url: Optional[str] = None,
|
|
33
|
+
cache_ttl: int = 300,
|
|
34
|
+
# Scoring
|
|
35
|
+
scoring_strategy: Union[str, ScoringStrategy] = "sum",
|
|
36
|
+
# Decision
|
|
37
|
+
decision_thresholds: Optional[Dict[str, int]] = None,
|
|
38
|
+
) -> RuleEngine:
|
|
39
|
+
"""
|
|
40
|
+
Create a configured RuleEngine instance.
|
|
41
|
+
|
|
42
|
+
This is the recommended way to create a RuleEngine with
|
|
43
|
+
sensible defaults for most use cases.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
storage_backend: 'memory', 'database', or 'file'
|
|
47
|
+
database_url: Database connection URL (for 'database' backend)
|
|
48
|
+
rules_file: Path to rules file (for 'file' backend)
|
|
49
|
+
rules: List of Rule objects (for 'memory' backend)
|
|
50
|
+
repository: Pre-configured repository (overrides other storage options)
|
|
51
|
+
cache_backend: 'memory', 'redis', or 'none'
|
|
52
|
+
cache_url: Redis URL (for 'redis' cache)
|
|
53
|
+
cache_ttl: Cache TTL in seconds
|
|
54
|
+
scoring_strategy: 'sum', 'weighted', 'max', or ScoringStrategy instance
|
|
55
|
+
decision_thresholds: Custom thresholds {'approve': 30, 'review': 70}
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Configured RuleEngine instance
|
|
59
|
+
|
|
60
|
+
Examples:
|
|
61
|
+
# Simple in-memory engine
|
|
62
|
+
engine = create_engine()
|
|
63
|
+
|
|
64
|
+
# In-memory with predefined rules
|
|
65
|
+
engine = create_engine(rules=[rule1, rule2])
|
|
66
|
+
|
|
67
|
+
# Database-backed with Redis cache
|
|
68
|
+
engine = create_engine(
|
|
69
|
+
storage_backend='database',
|
|
70
|
+
database_url='postgresql://localhost/mydb',
|
|
71
|
+
cache_backend='redis',
|
|
72
|
+
cache_url='redis://localhost:6379/0'
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# File-based rules
|
|
76
|
+
engine = create_engine(
|
|
77
|
+
storage_backend='file',
|
|
78
|
+
rules_file='./rules/fraud_rules.yaml'
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# With weighted scoring
|
|
82
|
+
engine = create_engine(
|
|
83
|
+
scoring_strategy='weighted',
|
|
84
|
+
decision_thresholds={'approve': 25, 'review': 60}
|
|
85
|
+
)
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
# Build or use provided repository
|
|
89
|
+
if repository is not None:
|
|
90
|
+
repo = repository
|
|
91
|
+
else:
|
|
92
|
+
repo = create_repository(
|
|
93
|
+
backend=storage_backend,
|
|
94
|
+
database_url=database_url,
|
|
95
|
+
rules_file=rules_file,
|
|
96
|
+
rules=rules,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Wrap with cache if needed
|
|
100
|
+
if cache_backend != "none" and not isinstance(repo, CachedRuleRepository):
|
|
101
|
+
repo = CachedRuleRepository(
|
|
102
|
+
repository=repo,
|
|
103
|
+
cache_backend=cache_backend,
|
|
104
|
+
cache_url=cache_url,
|
|
105
|
+
ttl_seconds=cache_ttl,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Build or use provided strategy
|
|
109
|
+
strategy = create_strategy(scoring_strategy)
|
|
110
|
+
|
|
111
|
+
# Create engine
|
|
112
|
+
return RuleEngine(
|
|
113
|
+
repository=repo,
|
|
114
|
+
scoring_strategy=strategy,
|
|
115
|
+
decision_thresholds=decision_thresholds,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def create_repository(
|
|
120
|
+
backend: str = "memory",
|
|
121
|
+
database_url: Optional[str] = None,
|
|
122
|
+
rules_file: Optional[str] = None,
|
|
123
|
+
rules: Optional[List["Rule"]] = None,
|
|
124
|
+
**kwargs,
|
|
125
|
+
) -> RuleRepository:
|
|
126
|
+
"""
|
|
127
|
+
Create a rule repository.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
backend: 'memory', 'database', or 'file'
|
|
131
|
+
database_url: Database URL for 'database' backend
|
|
132
|
+
rules_file: File path for 'file' backend
|
|
133
|
+
rules: Rules list for 'memory' backend
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
RuleRepository instance
|
|
137
|
+
"""
|
|
138
|
+
if backend == "memory":
|
|
139
|
+
return InMemoryRuleRepository(rules=rules or [])
|
|
140
|
+
|
|
141
|
+
elif backend == "database":
|
|
142
|
+
if not database_url:
|
|
143
|
+
raise ValueError("database_url required for 'database' backend")
|
|
144
|
+
return DatabaseRuleRepository(database_url)
|
|
145
|
+
|
|
146
|
+
elif backend == "file":
|
|
147
|
+
if not rules_file:
|
|
148
|
+
raise ValueError("rules_file required for 'file' backend")
|
|
149
|
+
return FileRuleRepository(rules_file)
|
|
150
|
+
|
|
151
|
+
else:
|
|
152
|
+
raise ValueError(f"Unknown storage backend: {backend}")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def create_strategy(strategy: Union[str, ScoringStrategy]) -> ScoringStrategy:
|
|
156
|
+
"""
|
|
157
|
+
Create a scoring strategy.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
strategy: Strategy name ('sum', 'weighted', 'max') or instance
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
ScoringStrategy instance
|
|
164
|
+
"""
|
|
165
|
+
if isinstance(strategy, ScoringStrategy):
|
|
166
|
+
return strategy
|
|
167
|
+
|
|
168
|
+
strategies = {
|
|
169
|
+
"sum": SumStrategy,
|
|
170
|
+
"weighted": WeightedStrategy,
|
|
171
|
+
"max": MaxStrategy,
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if strategy not in strategies:
|
|
175
|
+
raise ValueError(
|
|
176
|
+
f"Unknown strategy: {strategy}. Use: {list(strategies.keys())}"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
return strategies[strategy]()
|