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,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base scoring strategy class.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from typing import List
|
|
7
|
+
|
|
8
|
+
from ..types import RuleResult
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ScoringStrategy(ABC):
|
|
12
|
+
"""Base class for scoring strategies."""
|
|
13
|
+
|
|
14
|
+
@abstractmethod
|
|
15
|
+
def calculate(self, results: List[RuleResult]) -> int:
|
|
16
|
+
"""
|
|
17
|
+
Calculate final score from triggered rule results.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
results: List of RuleResult from triggered rules
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Final score (0-100)
|
|
24
|
+
"""
|
|
25
|
+
pass
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Max scoring strategy.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import List
|
|
6
|
+
|
|
7
|
+
from ..types import RuleResult
|
|
8
|
+
from .base import ScoringStrategy
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MaxStrategy(ScoringStrategy):
|
|
12
|
+
"""Take the maximum score from triggered rules."""
|
|
13
|
+
|
|
14
|
+
def calculate(self, results: List[RuleResult]) -> int:
|
|
15
|
+
"""
|
|
16
|
+
Return the highest score from triggered rules.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
results: List of triggered RuleResults
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Maximum score
|
|
23
|
+
"""
|
|
24
|
+
if not results:
|
|
25
|
+
return 0
|
|
26
|
+
return max(r.score for r in results)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sum scoring strategy.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import List
|
|
6
|
+
|
|
7
|
+
from ..types import RuleResult
|
|
8
|
+
from .base import ScoringStrategy
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SumStrategy(ScoringStrategy):
|
|
12
|
+
"""Sum all scores, capped at max_score."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, max_score: int = 100):
|
|
15
|
+
"""
|
|
16
|
+
Initialize sum strategy.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
max_score: Maximum score cap (default 100)
|
|
20
|
+
"""
|
|
21
|
+
self.max_score = max_score
|
|
22
|
+
|
|
23
|
+
def calculate(self, results: List[RuleResult]) -> int:
|
|
24
|
+
"""
|
|
25
|
+
Calculate total score by summing all triggered rule scores.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
results: List of triggered RuleResults
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Sum of scores, capped at max_score
|
|
32
|
+
"""
|
|
33
|
+
if not results:
|
|
34
|
+
return 0
|
|
35
|
+
total = sum(r.score for r in results)
|
|
36
|
+
return min(total, self.max_score)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Weighted scoring strategy.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import List
|
|
6
|
+
|
|
7
|
+
from ..types import RuleResult, Severity
|
|
8
|
+
from .base import ScoringStrategy
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class WeightedStrategy(ScoringStrategy):
|
|
12
|
+
"""Severity-weighted average scoring."""
|
|
13
|
+
|
|
14
|
+
SEVERITY_WEIGHTS = {
|
|
15
|
+
Severity.LOW: 1,
|
|
16
|
+
Severity.MEDIUM: 2,
|
|
17
|
+
Severity.HIGH: 3,
|
|
18
|
+
Severity.CRITICAL: 4,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
def calculate(self, results: List[RuleResult]) -> int:
|
|
22
|
+
"""
|
|
23
|
+
Calculate weighted average score based on severity.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
results: List of triggered RuleResults
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Weighted average score, capped at 100
|
|
30
|
+
"""
|
|
31
|
+
if not results:
|
|
32
|
+
return 0
|
|
33
|
+
|
|
34
|
+
total_weight = 0
|
|
35
|
+
weighted_sum = 0
|
|
36
|
+
|
|
37
|
+
for r in results:
|
|
38
|
+
weight = self.SEVERITY_WEIGHTS.get(r.severity, 1)
|
|
39
|
+
weighted_sum += r.score * weight
|
|
40
|
+
total_weight += weight
|
|
41
|
+
|
|
42
|
+
if total_weight == 0:
|
|
43
|
+
return 0
|
|
44
|
+
|
|
45
|
+
return min(int(weighted_sum / total_weight), 100)
|
pylitmus/types.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core type definitions for the CMAP Rules Engine.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Operator(str, Enum):
|
|
12
|
+
"""Supported condition operators."""
|
|
13
|
+
EQUALS = "equals"
|
|
14
|
+
NOT_EQUALS = "not_equals"
|
|
15
|
+
GREATER_THAN = "greater_than"
|
|
16
|
+
GREATER_THAN_OR_EQUAL = "greater_than_or_equal"
|
|
17
|
+
LESS_THAN = "less_than"
|
|
18
|
+
LESS_THAN_OR_EQUAL = "less_than_or_equal"
|
|
19
|
+
IN = "in"
|
|
20
|
+
NOT_IN = "not_in"
|
|
21
|
+
CONTAINS = "contains"
|
|
22
|
+
STARTS_WITH = "starts_with"
|
|
23
|
+
ENDS_WITH = "ends_with"
|
|
24
|
+
MATCHES_REGEX = "matches_regex"
|
|
25
|
+
IS_NULL = "is_null"
|
|
26
|
+
IS_NOT_NULL = "is_not_null"
|
|
27
|
+
WITHIN_DAYS = "within_days"
|
|
28
|
+
BEFORE = "before"
|
|
29
|
+
AFTER = "after"
|
|
30
|
+
BETWEEN = "between"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Severity(str, Enum):
|
|
34
|
+
"""Rule severity levels."""
|
|
35
|
+
LOW = "LOW"
|
|
36
|
+
MEDIUM = "MEDIUM"
|
|
37
|
+
HIGH = "HIGH"
|
|
38
|
+
CRITICAL = "CRITICAL"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class Rule:
|
|
43
|
+
"""A rule definition."""
|
|
44
|
+
code: str
|
|
45
|
+
name: str
|
|
46
|
+
description: str
|
|
47
|
+
category: str
|
|
48
|
+
severity: Severity
|
|
49
|
+
score: int
|
|
50
|
+
enabled: bool
|
|
51
|
+
conditions: Dict[str, Any]
|
|
52
|
+
version: int = 1
|
|
53
|
+
effective_from: Optional[datetime] = None
|
|
54
|
+
effective_to: Optional[datetime] = None
|
|
55
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
56
|
+
|
|
57
|
+
def is_effective(self, at_time: Optional[datetime] = None) -> bool:
|
|
58
|
+
"""Check if rule is effective at given time."""
|
|
59
|
+
if not self.enabled:
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
check_time = at_time or datetime.utcnow()
|
|
63
|
+
|
|
64
|
+
if self.effective_from and check_time < self.effective_from:
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
if self.effective_to and check_time > self.effective_to:
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
return True
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class RuleResult:
|
|
75
|
+
"""Result of evaluating a single rule."""
|
|
76
|
+
rule_code: str
|
|
77
|
+
rule_name: str
|
|
78
|
+
triggered: bool
|
|
79
|
+
score: int
|
|
80
|
+
severity: Severity
|
|
81
|
+
category: str
|
|
82
|
+
explanation: str
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass
|
|
86
|
+
class AssessmentResult:
|
|
87
|
+
"""Complete assessment result."""
|
|
88
|
+
total_score: int
|
|
89
|
+
decision: str
|
|
90
|
+
triggered_rules: List[RuleResult] = field(default_factory=list)
|
|
91
|
+
all_rules_evaluated: int = 0
|
|
92
|
+
processing_time_ms: float = 0.0
|
|
93
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pylitmus
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A high-performance rules engine for Python - evaluate data against configurable rules and get clear verdicts
|
|
5
|
+
Project-URL: Homepage, https://github.com/yourorg/pylitmus
|
|
6
|
+
Project-URL: Documentation, https://pylitmus.readthedocs.io/
|
|
7
|
+
Project-URL: Repository, https://github.com/yourorg/pylitmus.git
|
|
8
|
+
Project-URL: Changelog, https://github.com/yourorg/pylitmus/blob/main/CHANGELOG.md
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/yourorg/pylitmus/issues
|
|
10
|
+
Author-email: CMAP Team <team@example.com>
|
|
11
|
+
License: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: business-rules,decision-engine,fraud-detection,litmus-test,risk-assessment,rules-engine,scoring-engine
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
|
+
Requires-Dist: pyyaml>=6.0
|
|
25
|
+
Provides-Extra: all
|
|
26
|
+
Requires-Dist: flask>=3.0.0; extra == 'all'
|
|
27
|
+
Requires-Dist: redis>=5.0.0; extra == 'all'
|
|
28
|
+
Requires-Dist: sqlalchemy>=2.0.0; extra == 'all'
|
|
29
|
+
Provides-Extra: database
|
|
30
|
+
Requires-Dist: sqlalchemy>=2.0.0; extra == 'database'
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: pytest-cov>=4.1.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest>=7.4.0; extra == 'dev'
|
|
34
|
+
Provides-Extra: flask
|
|
35
|
+
Requires-Dist: flask>=3.0.0; extra == 'flask'
|
|
36
|
+
Provides-Extra: redis
|
|
37
|
+
Requires-Dist: redis>=5.0.0; extra == 'redis'
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
|
|
40
|
+
# pylitmus
|
|
41
|
+
|
|
42
|
+
A high-performance rules engine for Python. Like a litmus test for your data - evaluate against configurable rules and get clear verdicts.
|
|
43
|
+
|
|
44
|
+
[](https://badge.fury.io/py/pylitmus)
|
|
45
|
+
[](https://pypi.org/project/pylitmus/)
|
|
46
|
+
[](LICENSE)
|
|
47
|
+
[](https://github.com/org/pylitmus/actions)
|
|
48
|
+
[](https://codecov.io/gh/org/pylitmus)
|
|
49
|
+
|
|
50
|
+
## Features
|
|
51
|
+
|
|
52
|
+
- **YAML/JSON rule definitions** - Business-friendly rule configuration
|
|
53
|
+
- **Hot-reload** - Rules can be updated without restart
|
|
54
|
+
- **Multiple storage backends** - Memory, database, file
|
|
55
|
+
- **Caching** - Redis and in-memory caching support
|
|
56
|
+
- **Multiple scoring strategies** - Sum, weighted, max
|
|
57
|
+
- **Flask integration** - Easy integration with Flask apps
|
|
58
|
+
- **18 built-in operators** - Comparison, collection, string, null, temporal
|
|
59
|
+
- **Extensible** - Custom evaluators and strategies
|
|
60
|
+
|
|
61
|
+
## Installation
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install pylitmus
|
|
65
|
+
|
|
66
|
+
# With database support
|
|
67
|
+
pip install pylitmus[database]
|
|
68
|
+
|
|
69
|
+
# With Redis caching
|
|
70
|
+
pip install pylitmus[redis]
|
|
71
|
+
|
|
72
|
+
# With Flask integration
|
|
73
|
+
pip install pylitmus[flask]
|
|
74
|
+
|
|
75
|
+
# All extras
|
|
76
|
+
pip install pylitmus[all]
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Quick Start
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from pylitmus import create_engine, Rule, Severity
|
|
83
|
+
|
|
84
|
+
# Create engine with inline rules
|
|
85
|
+
engine = create_engine(rules=[
|
|
86
|
+
Rule(
|
|
87
|
+
code='AMT_001',
|
|
88
|
+
name='High Amount',
|
|
89
|
+
description='Flag high amounts',
|
|
90
|
+
category='AMOUNT',
|
|
91
|
+
severity=Severity.HIGH,
|
|
92
|
+
score=60,
|
|
93
|
+
enabled=True,
|
|
94
|
+
conditions={'field': 'amount', 'operator': 'greater_than', 'value': 5000}
|
|
95
|
+
)
|
|
96
|
+
])
|
|
97
|
+
|
|
98
|
+
# Evaluate data
|
|
99
|
+
result = engine.evaluate({'amount': 6000})
|
|
100
|
+
|
|
101
|
+
print(f"Score: {result.total_score}") # 60
|
|
102
|
+
print(f"Decision: {result.decision}") # FLAG
|
|
103
|
+
print(f"Triggered: {[r.rule_code for r in result.triggered_rules]}") # ['AMT_001']
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## YAML Rules
|
|
107
|
+
|
|
108
|
+
Define rules in YAML files for easy management:
|
|
109
|
+
|
|
110
|
+
```yaml
|
|
111
|
+
# rules.yaml
|
|
112
|
+
rules:
|
|
113
|
+
- code: "AMT_001"
|
|
114
|
+
name: "High Amount"
|
|
115
|
+
description: "Flag transactions over $5000"
|
|
116
|
+
category: "AMOUNT"
|
|
117
|
+
severity: "HIGH"
|
|
118
|
+
score: 60
|
|
119
|
+
enabled: true
|
|
120
|
+
conditions:
|
|
121
|
+
field: "amount"
|
|
122
|
+
operator: "greater_than"
|
|
123
|
+
value: 5000
|
|
124
|
+
|
|
125
|
+
- code: "RISK_001"
|
|
126
|
+
name: "High Risk Country"
|
|
127
|
+
description: "Flag transactions from high-risk countries"
|
|
128
|
+
category: "RISK"
|
|
129
|
+
severity: "CRITICAL"
|
|
130
|
+
score: 80
|
|
131
|
+
enabled: true
|
|
132
|
+
conditions:
|
|
133
|
+
field: "country"
|
|
134
|
+
operator: "in"
|
|
135
|
+
value: ["XX", "YY", "ZZ"]
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Load rules from file:
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
engine = create_engine(
|
|
142
|
+
storage_backend='file',
|
|
143
|
+
rules_file='rules.yaml'
|
|
144
|
+
)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Composite Conditions
|
|
148
|
+
|
|
149
|
+
Combine conditions with AND/OR logic:
|
|
150
|
+
|
|
151
|
+
```yaml
|
|
152
|
+
conditions:
|
|
153
|
+
all: # AND
|
|
154
|
+
- field: "amount"
|
|
155
|
+
operator: "greater_than"
|
|
156
|
+
value: 1000
|
|
157
|
+
- any: # OR
|
|
158
|
+
- field: "is_new_customer"
|
|
159
|
+
operator: "equals"
|
|
160
|
+
value: true
|
|
161
|
+
- field: "country"
|
|
162
|
+
operator: "in"
|
|
163
|
+
value: ["NG", "KE", "GH"]
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Or use the alternative format:
|
|
167
|
+
|
|
168
|
+
```yaml
|
|
169
|
+
conditions:
|
|
170
|
+
type: "AND"
|
|
171
|
+
conditions:
|
|
172
|
+
- field: "amount"
|
|
173
|
+
operator: "greater_than"
|
|
174
|
+
value: 1000
|
|
175
|
+
- field: "is_international"
|
|
176
|
+
operator: "equals"
|
|
177
|
+
value: true
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Available Operators
|
|
181
|
+
|
|
182
|
+
| Category | Operators |
|
|
183
|
+
|----------|-----------|
|
|
184
|
+
| **Comparison** | `equals`, `not_equals`, `greater_than`, `greater_than_or_equal`, `less_than`, `less_than_or_equal`, `between` |
|
|
185
|
+
| **Collection** | `in`, `not_in`, `contains`, `not_contains` |
|
|
186
|
+
| **String** | `starts_with`, `ends_with`, `matches_regex` |
|
|
187
|
+
| **Null** | `is_null`, `is_not_null` |
|
|
188
|
+
| **Temporal** | `within_days`, `before`, `after` |
|
|
189
|
+
|
|
190
|
+
## Scoring Strategies
|
|
191
|
+
|
|
192
|
+
### Sum Strategy (Default)
|
|
193
|
+
Adds up all triggered rule scores, capped at 100.
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
engine = create_engine(scoring_strategy='sum')
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Weighted Strategy
|
|
200
|
+
Uses severity-based weights (LOW=1, MEDIUM=2, HIGH=3, CRITICAL=4).
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
engine = create_engine(scoring_strategy='weighted')
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Max Strategy
|
|
207
|
+
Takes the highest score from triggered rules.
|
|
208
|
+
|
|
209
|
+
```python
|
|
210
|
+
engine = create_engine(scoring_strategy='max')
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Decision Thresholds
|
|
214
|
+
|
|
215
|
+
Customize decision boundaries:
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
engine = create_engine(
|
|
219
|
+
decision_thresholds={
|
|
220
|
+
'approve': 30, # Score < 30 = APPROVE
|
|
221
|
+
'review': 70 # Score 30-70 = REVIEW, >= 70 = FLAG
|
|
222
|
+
}
|
|
223
|
+
)
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## Storage Backends
|
|
227
|
+
|
|
228
|
+
### In-Memory
|
|
229
|
+
```python
|
|
230
|
+
engine = create_engine(storage_backend='memory', rules=[...])
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### File-Based
|
|
234
|
+
```python
|
|
235
|
+
engine = create_engine(
|
|
236
|
+
storage_backend='file',
|
|
237
|
+
rules_file='rules.yaml' # or rules.json
|
|
238
|
+
)
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Database
|
|
242
|
+
```python
|
|
243
|
+
engine = create_engine(
|
|
244
|
+
storage_backend='database',
|
|
245
|
+
database_url='postgresql://localhost/mydb'
|
|
246
|
+
)
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## Caching
|
|
250
|
+
|
|
251
|
+
### Memory Cache
|
|
252
|
+
```python
|
|
253
|
+
engine = create_engine(
|
|
254
|
+
cache_backend='memory',
|
|
255
|
+
cache_ttl=300 # 5 minutes
|
|
256
|
+
)
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Redis Cache
|
|
260
|
+
```python
|
|
261
|
+
engine = create_engine(
|
|
262
|
+
cache_backend='redis',
|
|
263
|
+
cache_url='redis://localhost:6379/0',
|
|
264
|
+
cache_ttl=600
|
|
265
|
+
)
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### No Cache
|
|
269
|
+
```python
|
|
270
|
+
engine = create_engine(cache_backend='none')
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
## Flask Integration
|
|
274
|
+
|
|
275
|
+
```python
|
|
276
|
+
from flask import Flask
|
|
277
|
+
from pylitmus.integrations.flask import CmapRulesEngine, get_engine
|
|
278
|
+
|
|
279
|
+
app = Flask(__name__)
|
|
280
|
+
app.config['CMAP_RULES_FILE'] = 'rules.yaml'
|
|
281
|
+
|
|
282
|
+
rules_engine = CmapRulesEngine(app)
|
|
283
|
+
|
|
284
|
+
@app.route('/evaluate', methods=['POST'])
|
|
285
|
+
def evaluate():
|
|
286
|
+
data = request.json
|
|
287
|
+
engine = get_engine()
|
|
288
|
+
result = engine.evaluate(data)
|
|
289
|
+
return {
|
|
290
|
+
'score': result.total_score,
|
|
291
|
+
'decision': result.decision,
|
|
292
|
+
'triggered_rules': [r.rule_code for r in result.triggered_rules]
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
## Nested Field Access
|
|
297
|
+
|
|
298
|
+
Access nested data using dot notation:
|
|
299
|
+
|
|
300
|
+
```python
|
|
301
|
+
data = {
|
|
302
|
+
'transaction': {
|
|
303
|
+
'amount': 6000,
|
|
304
|
+
'merchant': {
|
|
305
|
+
'category': 'electronics'
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
# Rule condition
|
|
311
|
+
conditions:
|
|
312
|
+
field: "transaction.merchant.category"
|
|
313
|
+
operator: "equals"
|
|
314
|
+
value: "electronics"
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
## Pattern Matching
|
|
318
|
+
|
|
319
|
+
Advanced pattern matching capabilities:
|
|
320
|
+
|
|
321
|
+
```python
|
|
322
|
+
from pylitmus import EnhancedPatternEngine
|
|
323
|
+
|
|
324
|
+
pattern_engine = EnhancedPatternEngine()
|
|
325
|
+
|
|
326
|
+
# Regex matching
|
|
327
|
+
pattern_engine.add_pattern('email', r'^[\w.-]+@[\w.-]+\.\w+$', 'regex')
|
|
328
|
+
|
|
329
|
+
# Fuzzy matching
|
|
330
|
+
pattern_engine.add_pattern('name', 'John Smith', 'fuzzy', threshold=0.8)
|
|
331
|
+
|
|
332
|
+
# Range matching
|
|
333
|
+
pattern_engine.add_pattern('age', {'min': 18, 'max': 65}, 'range')
|
|
334
|
+
|
|
335
|
+
# Check matches
|
|
336
|
+
result = pattern_engine.match_all({
|
|
337
|
+
'email': 'user@example.com',
|
|
338
|
+
'name': 'Jon Smith',
|
|
339
|
+
'age': 25
|
|
340
|
+
})
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
## API Reference
|
|
344
|
+
|
|
345
|
+
### create_engine()
|
|
346
|
+
|
|
347
|
+
```python
|
|
348
|
+
def create_engine(
|
|
349
|
+
storage_backend: str = 'memory',
|
|
350
|
+
database_url: str = None,
|
|
351
|
+
rules_file: str = None,
|
|
352
|
+
rules: List[Rule] = None,
|
|
353
|
+
repository: RuleRepository = None,
|
|
354
|
+
cache_backend: str = 'memory',
|
|
355
|
+
cache_url: str = None,
|
|
356
|
+
cache_ttl: int = 300,
|
|
357
|
+
scoring_strategy: str = 'sum',
|
|
358
|
+
decision_thresholds: Dict[str, int] = None,
|
|
359
|
+
) -> RuleEngine
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### RuleEngine.evaluate()
|
|
363
|
+
|
|
364
|
+
```python
|
|
365
|
+
def evaluate(
|
|
366
|
+
self,
|
|
367
|
+
data: Dict[str, Any],
|
|
368
|
+
context: Dict[str, Any] = None,
|
|
369
|
+
filters: Dict[str, Any] = None
|
|
370
|
+
) -> AssessmentResult
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
### AssessmentResult
|
|
374
|
+
|
|
375
|
+
```python
|
|
376
|
+
@dataclass
|
|
377
|
+
class AssessmentResult:
|
|
378
|
+
total_score: int # Total calculated score
|
|
379
|
+
decision: str # APPROVE, REVIEW, or FLAG
|
|
380
|
+
triggered_rules: List[RuleResult] # Rules that matched
|
|
381
|
+
processing_time_ms: float # Processing time in ms
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
## Full Example
|
|
385
|
+
|
|
386
|
+
```python
|
|
387
|
+
from pylitmus import (
|
|
388
|
+
create_engine,
|
|
389
|
+
Rule,
|
|
390
|
+
Severity,
|
|
391
|
+
InMemoryRuleRepository,
|
|
392
|
+
WeightedStrategy,
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
# Define rules
|
|
396
|
+
rules = [
|
|
397
|
+
Rule(
|
|
398
|
+
code='AMT_HIGH',
|
|
399
|
+
name='High Amount',
|
|
400
|
+
description='Flag high-value transactions',
|
|
401
|
+
category='AMOUNT',
|
|
402
|
+
severity=Severity.HIGH,
|
|
403
|
+
score=60,
|
|
404
|
+
enabled=True,
|
|
405
|
+
conditions={'field': 'amount', 'operator': 'greater_than', 'value': 5000}
|
|
406
|
+
),
|
|
407
|
+
Rule(
|
|
408
|
+
code='NEW_CUSTOMER',
|
|
409
|
+
name='New Customer',
|
|
410
|
+
description='Flag new customer transactions',
|
|
411
|
+
category='CUSTOMER',
|
|
412
|
+
severity=Severity.MEDIUM,
|
|
413
|
+
score=30,
|
|
414
|
+
enabled=True,
|
|
415
|
+
conditions={'field': 'is_new_customer', 'operator': 'equals', 'value': True}
|
|
416
|
+
),
|
|
417
|
+
Rule(
|
|
418
|
+
code='INTL_TXN',
|
|
419
|
+
name='International Transaction',
|
|
420
|
+
description='Flag international transactions',
|
|
421
|
+
category='GEOGRAPHY',
|
|
422
|
+
severity=Severity.LOW,
|
|
423
|
+
score=20,
|
|
424
|
+
enabled=True,
|
|
425
|
+
conditions={'field': 'is_international', 'operator': 'equals', 'value': True}
|
|
426
|
+
),
|
|
427
|
+
]
|
|
428
|
+
|
|
429
|
+
# Create engine with weighted scoring
|
|
430
|
+
engine = create_engine(
|
|
431
|
+
rules=rules,
|
|
432
|
+
scoring_strategy='weighted',
|
|
433
|
+
decision_thresholds={'approve': 25, 'review': 60}
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
# Evaluate transaction
|
|
437
|
+
result = engine.evaluate({
|
|
438
|
+
'amount': 6000,
|
|
439
|
+
'is_new_customer': True,
|
|
440
|
+
'is_international': False
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
print(f"Total Score: {result.total_score}")
|
|
444
|
+
print(f"Decision: {result.decision}")
|
|
445
|
+
print(f"Triggered Rules: {[r.rule_code for r in result.triggered_rules]}")
|
|
446
|
+
print(f"Processing Time: {result.processing_time_ms:.2f}ms")
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
## Documentation
|
|
450
|
+
|
|
451
|
+
- [Quick Start Guide](docs/quickstart.md)
|
|
452
|
+
- [Rule Format](docs/rules-format.md)
|
|
453
|
+
- [API Reference](docs/api-reference.md)
|
|
454
|
+
- [Flask Integration](docs/flask-integration.md)
|
|
455
|
+
- [Examples](examples/)
|
|
456
|
+
|
|
457
|
+
## License
|
|
458
|
+
|
|
459
|
+
MIT License - see [LICENSE](LICENSE) for details.
|