pylitmus 1.0.0__tar.gz → 1.1.0__tar.gz
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-1.0.0 → pylitmus-1.1.0}/PKG-INFO +1 -1
- {pylitmus-1.0.0 → pylitmus-1.1.0}/pyproject.toml +1 -1
- {pylitmus-1.0.0 → pylitmus-1.1.0}/src/pylitmus/__init__.py +3 -2
- {pylitmus-1.0.0 → pylitmus-1.1.0}/src/pylitmus/engine.py +32 -21
- {pylitmus-1.0.0 → pylitmus-1.1.0}/src/pylitmus/factory.py +16 -7
- {pylitmus-1.0.0 → pylitmus-1.1.0}/src/pylitmus/types.py +29 -3
- {pylitmus-1.0.0 → pylitmus-1.1.0}/tests/test_phase1_core.py +50 -21
- {pylitmus-1.0.0 → pylitmus-1.1.0}/tests/test_phase2_conditions.py +4 -4
- {pylitmus-1.0.0 → pylitmus-1.1.0}/tests/test_phase3_evaluators.py +5 -5
- {pylitmus-1.0.0 → pylitmus-1.1.0}/tests/test_phase4_strategies.py +10 -10
- {pylitmus-1.0.0 → pylitmus-1.1.0}/tests/test_phase5_storage.py +4 -4
- {pylitmus-1.0.0 → pylitmus-1.1.0}/tests/test_phase6_patterns.py +7 -7
- {pylitmus-1.0.0 → pylitmus-1.1.0}/tests/test_phase7_flask.py +25 -25
- {pylitmus-1.0.0 → pylitmus-1.1.0}/tests/test_phase8_factory.py +50 -14
- pylitmus-1.1.0/tests/test_phase9_decision_tiers.py +566 -0
- {pylitmus-1.0.0 → pylitmus-1.1.0}/CHANGELOG.md +0 -0
- {pylitmus-1.0.0 → pylitmus-1.1.0}/LICENSE +0 -0
- {pylitmus-1.0.0 → pylitmus-1.1.0}/README.md +0 -0
- {pylitmus-1.0.0 → pylitmus-1.1.0}/docs/api-reference.md +0 -0
- {pylitmus-1.0.0 → pylitmus-1.1.0}/docs/flask-integration.md +0 -0
- {pylitmus-1.0.0 → pylitmus-1.1.0}/docs/quickstart.md +0 -0
- {pylitmus-1.0.0 → pylitmus-1.1.0}/docs/rules-format.md +0 -0
- {pylitmus-1.0.0 → pylitmus-1.1.0}/examples/basic_usage.py +0 -0
- {pylitmus-1.0.0 → pylitmus-1.1.0}/examples/flask_app/app.py +0 -0
- {pylitmus-1.0.0 → pylitmus-1.1.0}/examples/flask_app/requirements.txt +0 -0
- {pylitmus-1.0.0 → pylitmus-1.1.0}/examples/flask_app/rules.yaml +0 -0
- {pylitmus-1.0.0 → pylitmus-1.1.0}/src/pylitmus/conditions/__init__.py +0 -0
- {pylitmus-1.0.0 → pylitmus-1.1.0}/src/pylitmus/conditions/base.py +0 -0
- {pylitmus-1.0.0 → pylitmus-1.1.0}/src/pylitmus/conditions/builder.py +0 -0
- {pylitmus-1.0.0 → pylitmus-1.1.0}/src/pylitmus/conditions/composite.py +0 -0
- {pylitmus-1.0.0 → pylitmus-1.1.0}/src/pylitmus/conditions/simple.py +0 -0
- {pylitmus-1.0.0 → pylitmus-1.1.0}/src/pylitmus/evaluators/__init__.py +0 -0
- {pylitmus-1.0.0 → pylitmus-1.1.0}/src/pylitmus/evaluators/base.py +0 -0
- {pylitmus-1.0.0 → pylitmus-1.1.0}/src/pylitmus/evaluators/factory.py +0 -0
- {pylitmus-1.0.0 → pylitmus-1.1.0}/src/pylitmus/exceptions.py +0 -0
- {pylitmus-1.0.0 → pylitmus-1.1.0}/src/pylitmus/integrations/__init__.py +0 -0
- {pylitmus-1.0.0 → pylitmus-1.1.0}/src/pylitmus/integrations/flask/__init__.py +0 -0
- {pylitmus-1.0.0 → pylitmus-1.1.0}/src/pylitmus/integrations/flask/extension.py +0 -0
- {pylitmus-1.0.0 → pylitmus-1.1.0}/src/pylitmus/patterns/__init__.py +0 -0
- {pylitmus-1.0.0 → pylitmus-1.1.0}/src/pylitmus/patterns/base.py +0 -0
- {pylitmus-1.0.0 → pylitmus-1.1.0}/src/pylitmus/patterns/engine.py +0 -0
- {pylitmus-1.0.0 → pylitmus-1.1.0}/src/pylitmus/patterns/exact.py +0 -0
- {pylitmus-1.0.0 → pylitmus-1.1.0}/src/pylitmus/patterns/fuzzy.py +0 -0
- {pylitmus-1.0.0 → pylitmus-1.1.0}/src/pylitmus/patterns/glob.py +0 -0
- {pylitmus-1.0.0 → pylitmus-1.1.0}/src/pylitmus/patterns/range.py +0 -0
- {pylitmus-1.0.0 → pylitmus-1.1.0}/src/pylitmus/patterns/regex.py +0 -0
- {pylitmus-1.0.0 → pylitmus-1.1.0}/src/pylitmus/storage/__init__.py +0 -0
- {pylitmus-1.0.0 → pylitmus-1.1.0}/src/pylitmus/storage/base.py +0 -0
- {pylitmus-1.0.0 → pylitmus-1.1.0}/src/pylitmus/storage/cached.py +0 -0
- {pylitmus-1.0.0 → pylitmus-1.1.0}/src/pylitmus/storage/database.py +0 -0
- {pylitmus-1.0.0 → pylitmus-1.1.0}/src/pylitmus/storage/file.py +0 -0
- {pylitmus-1.0.0 → pylitmus-1.1.0}/src/pylitmus/storage/memory.py +0 -0
- {pylitmus-1.0.0 → pylitmus-1.1.0}/src/pylitmus/strategies/__init__.py +0 -0
- {pylitmus-1.0.0 → pylitmus-1.1.0}/src/pylitmus/strategies/base.py +0 -0
- {pylitmus-1.0.0 → pylitmus-1.1.0}/src/pylitmus/strategies/max.py +0 -0
- {pylitmus-1.0.0 → pylitmus-1.1.0}/src/pylitmus/strategies/sum.py +0 -0
- {pylitmus-1.0.0 → pylitmus-1.1.0}/src/pylitmus/strategies/weighted.py +0 -0
- {pylitmus-1.0.0 → pylitmus-1.1.0}/tests/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pylitmus
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1.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/
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pylitmus"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.1.0"
|
|
8
8
|
description = "A high-performance rules engine for Python - evaluate data against configurable rules and get clear verdicts"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -26,9 +26,9 @@ from .storage import (
|
|
|
26
26
|
RuleRepository,
|
|
27
27
|
)
|
|
28
28
|
from .strategies import MaxStrategy, ScoringStrategy, SumStrategy, WeightedStrategy
|
|
29
|
-
from .types import AssessmentResult, Operator, Rule, RuleResult, Severity
|
|
29
|
+
from .types import AssessmentResult, DecisionTier, Operator, Rule, RuleResult, Severity
|
|
30
30
|
|
|
31
|
-
__version__ = "1.
|
|
31
|
+
__version__ = "1.1.0"
|
|
32
32
|
|
|
33
33
|
__all__ = [
|
|
34
34
|
# Main
|
|
@@ -40,6 +40,7 @@ __all__ = [
|
|
|
40
40
|
"Rule",
|
|
41
41
|
"RuleResult",
|
|
42
42
|
"AssessmentResult",
|
|
43
|
+
"DecisionTier",
|
|
43
44
|
"Operator",
|
|
44
45
|
"Severity",
|
|
45
46
|
# Conditions
|
|
@@ -7,7 +7,7 @@ import time
|
|
|
7
7
|
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
|
8
8
|
|
|
9
9
|
from .exceptions import EvaluationError
|
|
10
|
-
from .types import AssessmentResult, Rule, RuleResult
|
|
10
|
+
from .types import AssessmentResult, DecisionTier, Rule, RuleResult
|
|
11
11
|
|
|
12
12
|
if TYPE_CHECKING:
|
|
13
13
|
from .storage.base import RuleRepository
|
|
@@ -21,24 +21,34 @@ class RuleEngine:
|
|
|
21
21
|
Main rules engine for evaluating data against configured rules.
|
|
22
22
|
|
|
23
23
|
Usage:
|
|
24
|
+
# Without decision tiers (returns None for decision)
|
|
24
25
|
engine = RuleEngine(
|
|
25
26
|
repository=DatabaseRuleRepository(db_url),
|
|
26
27
|
scoring_strategy=WeightedStrategy()
|
|
27
28
|
)
|
|
28
29
|
|
|
30
|
+
# With user-defined decision tiers
|
|
31
|
+
engine = RuleEngine(
|
|
32
|
+
repository=DatabaseRuleRepository(db_url),
|
|
33
|
+
scoring_strategy=WeightedStrategy(),
|
|
34
|
+
decision_tiers=[
|
|
35
|
+
DecisionTier("AUTO_APPROVE", 0, 20, "Very low risk"),
|
|
36
|
+
DecisionTier("APPROVE", 20, 40, "Low risk"),
|
|
37
|
+
DecisionTier("SOFT_REVIEW", 40, 60, "Medium risk"),
|
|
38
|
+
DecisionTier("HARD_REVIEW", 60, 80, "Higher risk"),
|
|
39
|
+
DecisionTier("FLAG", 80, 95, "High risk"),
|
|
40
|
+
DecisionTier("AUTO_REJECT", 95, 101, "Very high risk"),
|
|
41
|
+
]
|
|
42
|
+
)
|
|
43
|
+
|
|
29
44
|
result = engine.evaluate(claim, context)
|
|
30
45
|
"""
|
|
31
46
|
|
|
32
|
-
DEFAULT_THRESHOLDS = {
|
|
33
|
-
"approve": 30,
|
|
34
|
-
"review": 70,
|
|
35
|
-
}
|
|
36
|
-
|
|
37
47
|
def __init__(
|
|
38
48
|
self,
|
|
39
49
|
repository: "RuleRepository",
|
|
40
50
|
scoring_strategy: Optional["ScoringStrategy"] = None,
|
|
41
|
-
|
|
51
|
+
decision_tiers: Optional[List[DecisionTier]] = None,
|
|
42
52
|
condition_builder: Optional[Any] = None,
|
|
43
53
|
):
|
|
44
54
|
"""
|
|
@@ -47,12 +57,13 @@ class RuleEngine:
|
|
|
47
57
|
Args:
|
|
48
58
|
repository: Rule storage backend
|
|
49
59
|
scoring_strategy: Strategy for calculating scores
|
|
50
|
-
|
|
60
|
+
decision_tiers: User-defined decision tiers with score ranges.
|
|
61
|
+
If not provided, decision will be None.
|
|
51
62
|
condition_builder: Builder for creating conditions from dicts
|
|
52
63
|
"""
|
|
53
64
|
self.repository = repository
|
|
54
65
|
self._scoring_strategy = scoring_strategy
|
|
55
|
-
self.
|
|
66
|
+
self._decision_tiers = decision_tiers
|
|
56
67
|
self._condition_builder = condition_builder
|
|
57
68
|
self._rules_cache: Optional[List[Rule]] = None
|
|
58
69
|
|
|
@@ -189,25 +200,25 @@ class RuleEngine:
|
|
|
189
200
|
explanation=rule.description if triggered else "Condition not met",
|
|
190
201
|
)
|
|
191
202
|
|
|
192
|
-
def _make_decision(self, score: int) -> str:
|
|
203
|
+
def _make_decision(self, score: int) -> Optional[str]:
|
|
193
204
|
"""
|
|
194
|
-
Make a decision based on the total score.
|
|
205
|
+
Make a decision based on the total score and configured decision tiers.
|
|
195
206
|
|
|
196
207
|
Args:
|
|
197
208
|
score: Total calculated score
|
|
198
209
|
|
|
199
210
|
Returns:
|
|
200
|
-
Decision string
|
|
211
|
+
Decision string if a matching tier is found, None otherwise.
|
|
212
|
+
If no decision tiers are configured, returns None.
|
|
201
213
|
"""
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
return "FLAG"
|
|
214
|
+
if not self._decision_tiers:
|
|
215
|
+
return None
|
|
216
|
+
|
|
217
|
+
for tier in self._decision_tiers:
|
|
218
|
+
if tier.matches(score):
|
|
219
|
+
return tier.name
|
|
220
|
+
|
|
221
|
+
return None
|
|
211
222
|
|
|
212
223
|
def reload_rules(self) -> None:
|
|
213
224
|
"""Force reload rules from repository."""
|
|
@@ -4,7 +4,7 @@ Factory functions for creating RuleEngine instances with sensible defaults.
|
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
-
from typing import TYPE_CHECKING,
|
|
7
|
+
from typing import TYPE_CHECKING, List, Optional, Union
|
|
8
8
|
|
|
9
9
|
from .engine import RuleEngine
|
|
10
10
|
from .storage import (
|
|
@@ -15,6 +15,7 @@ from .storage import (
|
|
|
15
15
|
RuleRepository,
|
|
16
16
|
)
|
|
17
17
|
from .strategies import MaxStrategy, ScoringStrategy, SumStrategy, WeightedStrategy
|
|
18
|
+
from .types import DecisionTier
|
|
18
19
|
|
|
19
20
|
if TYPE_CHECKING:
|
|
20
21
|
from .types import Rule
|
|
@@ -34,7 +35,7 @@ def create_engine(
|
|
|
34
35
|
# Scoring
|
|
35
36
|
scoring_strategy: Union[str, ScoringStrategy] = "sum",
|
|
36
37
|
# Decision
|
|
37
|
-
|
|
38
|
+
decision_tiers: Optional[List[DecisionTier]] = None,
|
|
38
39
|
) -> RuleEngine:
|
|
39
40
|
"""
|
|
40
41
|
Create a configured RuleEngine instance.
|
|
@@ -52,13 +53,14 @@ def create_engine(
|
|
|
52
53
|
cache_url: Redis URL (for 'redis' cache)
|
|
53
54
|
cache_ttl: Cache TTL in seconds
|
|
54
55
|
scoring_strategy: 'sum', 'weighted', 'max', or ScoringStrategy instance
|
|
55
|
-
|
|
56
|
+
decision_tiers: User-defined decision tiers with score ranges.
|
|
57
|
+
If not provided, decision will be None in results.
|
|
56
58
|
|
|
57
59
|
Returns:
|
|
58
60
|
Configured RuleEngine instance
|
|
59
61
|
|
|
60
62
|
Examples:
|
|
61
|
-
# Simple in-memory engine
|
|
63
|
+
# Simple in-memory engine (no decision tiers - decision will be None)
|
|
62
64
|
engine = create_engine()
|
|
63
65
|
|
|
64
66
|
# In-memory with predefined rules
|
|
@@ -78,10 +80,17 @@ def create_engine(
|
|
|
78
80
|
rules_file='./rules/fraud_rules.yaml'
|
|
79
81
|
)
|
|
80
82
|
|
|
81
|
-
# With
|
|
83
|
+
# With user-defined decision tiers
|
|
82
84
|
engine = create_engine(
|
|
83
85
|
scoring_strategy='weighted',
|
|
84
|
-
|
|
86
|
+
decision_tiers=[
|
|
87
|
+
DecisionTier("AUTO_APPROVE", 0, 20, "Very low risk"),
|
|
88
|
+
DecisionTier("APPROVE", 20, 40, "Low risk"),
|
|
89
|
+
DecisionTier("SOFT_REVIEW", 40, 60, "Medium risk"),
|
|
90
|
+
DecisionTier("HARD_REVIEW", 60, 80, "Higher risk"),
|
|
91
|
+
DecisionTier("FLAG", 80, 95, "High risk"),
|
|
92
|
+
DecisionTier("AUTO_REJECT", 95, 101, "Very high risk"),
|
|
93
|
+
]
|
|
85
94
|
)
|
|
86
95
|
"""
|
|
87
96
|
|
|
@@ -112,7 +121,7 @@ def create_engine(
|
|
|
112
121
|
return RuleEngine(
|
|
113
122
|
repository=repo,
|
|
114
123
|
scoring_strategy=strategy,
|
|
115
|
-
|
|
124
|
+
decision_tiers=decision_tiers,
|
|
116
125
|
)
|
|
117
126
|
|
|
118
127
|
|
|
@@ -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
|
|
@@ -5,10 +5,10 @@ Phase 1 Tests: Core Engine - Types, RuleEngine, Exceptions
|
|
|
5
5
|
from datetime import datetime, timedelta
|
|
6
6
|
|
|
7
7
|
import pytest
|
|
8
|
-
|
|
9
|
-
from cmap_rules_engine import (
|
|
8
|
+
from pylitmus import (
|
|
10
9
|
AssessmentResult,
|
|
11
10
|
ConditionError,
|
|
11
|
+
DecisionTier,
|
|
12
12
|
EvaluationError,
|
|
13
13
|
Operator,
|
|
14
14
|
Rule,
|
|
@@ -19,8 +19,8 @@ from cmap_rules_engine import (
|
|
|
19
19
|
StorageError,
|
|
20
20
|
UnknownOperatorError,
|
|
21
21
|
)
|
|
22
|
-
from
|
|
23
|
-
from
|
|
22
|
+
from pylitmus.storage import InMemoryRuleRepository
|
|
23
|
+
from pylitmus.strategies import SumStrategy
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
class TestOperatorEnum:
|
|
@@ -207,13 +207,20 @@ class TestAssessmentResult:
|
|
|
207
207
|
|
|
208
208
|
def test_assessment_result_defaults(self):
|
|
209
209
|
"""Test AssessmentResult default values."""
|
|
210
|
-
result = AssessmentResult(total_score=0, decision=
|
|
210
|
+
result = AssessmentResult(total_score=0, decision=None)
|
|
211
211
|
|
|
212
212
|
assert result.triggered_rules == []
|
|
213
213
|
assert result.all_rules_evaluated == 0
|
|
214
214
|
assert result.processing_time_ms == 0.0
|
|
215
215
|
assert result.metadata == {}
|
|
216
216
|
|
|
217
|
+
def test_assessment_result_with_none_decision(self):
|
|
218
|
+
"""Test AssessmentResult with None decision (no tiers configured)."""
|
|
219
|
+
result = AssessmentResult(total_score=50, decision=None)
|
|
220
|
+
|
|
221
|
+
assert result.total_score == 50
|
|
222
|
+
assert result.decision is None
|
|
223
|
+
|
|
217
224
|
|
|
218
225
|
class TestExceptions:
|
|
219
226
|
"""Tests for custom exceptions."""
|
|
@@ -288,10 +295,19 @@ class TestRuleEngine:
|
|
|
288
295
|
]
|
|
289
296
|
|
|
290
297
|
@pytest.fixture
|
|
291
|
-
def
|
|
292
|
-
"""Create
|
|
298
|
+
def standard_tiers(self):
|
|
299
|
+
"""Create standard decision tiers."""
|
|
300
|
+
return [
|
|
301
|
+
DecisionTier("APPROVE", 0, 30),
|
|
302
|
+
DecisionTier("REVIEW", 30, 70),
|
|
303
|
+
DecisionTier("FLAG", 70, 101),
|
|
304
|
+
]
|
|
305
|
+
|
|
306
|
+
@pytest.fixture
|
|
307
|
+
def engine(self, sample_rules, standard_tiers):
|
|
308
|
+
"""Create engine with sample rules and standard tiers."""
|
|
293
309
|
repo = InMemoryRuleRepository(sample_rules)
|
|
294
|
-
return RuleEngine(repository=repo)
|
|
310
|
+
return RuleEngine(repository=repo, decision_tiers=standard_tiers)
|
|
295
311
|
|
|
296
312
|
def test_engine_creation(self, sample_rules):
|
|
297
313
|
"""Test engine creation."""
|
|
@@ -299,16 +315,19 @@ class TestRuleEngine:
|
|
|
299
315
|
engine = RuleEngine(repository=repo)
|
|
300
316
|
|
|
301
317
|
assert engine.repository is repo
|
|
302
|
-
assert engine.
|
|
318
|
+
assert engine._decision_tiers is None
|
|
303
319
|
|
|
304
|
-
def
|
|
305
|
-
"""Test engine with custom
|
|
320
|
+
def test_engine_with_custom_tiers(self, sample_rules):
|
|
321
|
+
"""Test engine with custom decision tiers."""
|
|
306
322
|
repo = InMemoryRuleRepository(sample_rules)
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
323
|
+
custom_tiers = [
|
|
324
|
+
DecisionTier("LOW", 0, 20),
|
|
325
|
+
DecisionTier("MEDIUM", 20, 50),
|
|
326
|
+
DecisionTier("HIGH", 50, 101),
|
|
327
|
+
]
|
|
328
|
+
engine = RuleEngine(repository=repo, decision_tiers=custom_tiers)
|
|
310
329
|
|
|
311
|
-
assert engine.
|
|
330
|
+
assert engine._decision_tiers == custom_tiers
|
|
312
331
|
|
|
313
332
|
def test_engine_evaluate_no_triggers(self, engine):
|
|
314
333
|
"""Test evaluation with no rules triggered."""
|
|
@@ -347,7 +366,7 @@ class TestRuleEngine:
|
|
|
347
366
|
result = engine.evaluate({"amount": 6000}) # triggers 60 point rule
|
|
348
367
|
assert result.decision == "REVIEW"
|
|
349
368
|
|
|
350
|
-
def test_engine_decision_flag(self, sample_rules):
|
|
369
|
+
def test_engine_decision_flag(self, sample_rules, standard_tiers):
|
|
351
370
|
"""Test FLAG decision (score >= 70)."""
|
|
352
371
|
# Add a high score rule
|
|
353
372
|
sample_rules.append(
|
|
@@ -364,11 +383,21 @@ class TestRuleEngine:
|
|
|
364
383
|
)
|
|
365
384
|
|
|
366
385
|
repo = InMemoryRuleRepository(sample_rules)
|
|
367
|
-
engine = RuleEngine(repository=repo)
|
|
386
|
+
engine = RuleEngine(repository=repo, decision_tiers=standard_tiers)
|
|
368
387
|
|
|
369
388
|
result = engine.evaluate({"critical": True, "amount": 1000})
|
|
370
389
|
assert result.decision == "FLAG"
|
|
371
390
|
|
|
391
|
+
def test_engine_no_decision_tiers(self, sample_rules):
|
|
392
|
+
"""Test engine without decision tiers returns None decision."""
|
|
393
|
+
repo = InMemoryRuleRepository(sample_rules)
|
|
394
|
+
engine = RuleEngine(repository=repo) # No decision_tiers
|
|
395
|
+
|
|
396
|
+
result = engine.evaluate({"amount": 6000})
|
|
397
|
+
|
|
398
|
+
assert result.total_score == 60
|
|
399
|
+
assert result.decision is None
|
|
400
|
+
|
|
372
401
|
def test_engine_evaluate_rule_directly(self, engine, sample_rules):
|
|
373
402
|
"""Test evaluating a single rule."""
|
|
374
403
|
rule = sample_rules[0] # HIGH_AMOUNT
|
|
@@ -448,7 +477,7 @@ class TestRuleEngineWithStrategies:
|
|
|
448
477
|
|
|
449
478
|
def test_sum_strategy(self, rules):
|
|
450
479
|
"""Test SumStrategy adds scores."""
|
|
451
|
-
from
|
|
480
|
+
from pylitmus.strategies import SumStrategy
|
|
452
481
|
|
|
453
482
|
repo = InMemoryRuleRepository(rules)
|
|
454
483
|
engine = RuleEngine(repository=repo, scoring_strategy=SumStrategy())
|
|
@@ -459,7 +488,7 @@ class TestRuleEngineWithStrategies:
|
|
|
459
488
|
|
|
460
489
|
def test_sum_strategy_with_cap(self, rules):
|
|
461
490
|
"""Test SumStrategy respects max cap."""
|
|
462
|
-
from
|
|
491
|
+
from pylitmus.strategies import SumStrategy
|
|
463
492
|
|
|
464
493
|
repo = InMemoryRuleRepository(rules)
|
|
465
494
|
engine = RuleEngine(repository=repo, scoring_strategy=SumStrategy(max_score=50))
|
|
@@ -470,7 +499,7 @@ class TestRuleEngineWithStrategies:
|
|
|
470
499
|
|
|
471
500
|
def test_max_strategy(self, rules):
|
|
472
501
|
"""Test MaxStrategy takes highest score."""
|
|
473
|
-
from
|
|
502
|
+
from pylitmus.strategies import MaxStrategy
|
|
474
503
|
|
|
475
504
|
repo = InMemoryRuleRepository(rules)
|
|
476
505
|
engine = RuleEngine(repository=repo, scoring_strategy=MaxStrategy())
|
|
@@ -481,7 +510,7 @@ class TestRuleEngineWithStrategies:
|
|
|
481
510
|
|
|
482
511
|
def test_weighted_strategy(self, rules):
|
|
483
512
|
"""Test WeightedStrategy weights by severity."""
|
|
484
|
-
from
|
|
513
|
+
from pylitmus.strategies import WeightedStrategy
|
|
485
514
|
|
|
486
515
|
repo = InMemoryRuleRepository(rules)
|
|
487
516
|
engine = RuleEngine(repository=repo, scoring_strategy=WeightedStrategy())
|
|
@@ -4,13 +4,13 @@ Phase 2 Tests: Conditions System - Simple and Composite Conditions
|
|
|
4
4
|
|
|
5
5
|
import pytest
|
|
6
6
|
|
|
7
|
-
from
|
|
7
|
+
from pylitmus.conditions import (
|
|
8
8
|
CompositeCondition,
|
|
9
9
|
Condition,
|
|
10
10
|
ConditionBuilder,
|
|
11
11
|
SimpleCondition,
|
|
12
12
|
)
|
|
13
|
-
from
|
|
13
|
+
from pylitmus.exceptions import UnknownOperatorError
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
class TestConditionBase:
|
|
@@ -523,8 +523,8 @@ class TestConditionIntegration:
|
|
|
523
523
|
|
|
524
524
|
def test_condition_with_rule_engine(self):
|
|
525
525
|
"""Test conditions work correctly with RuleEngine."""
|
|
526
|
-
from
|
|
527
|
-
from
|
|
526
|
+
from pylitmus import RuleEngine, Rule, Severity
|
|
527
|
+
from pylitmus.storage import InMemoryRuleRepository
|
|
528
528
|
|
|
529
529
|
rules = [
|
|
530
530
|
Rule(
|
|
@@ -6,8 +6,8 @@ from datetime import datetime, timedelta
|
|
|
6
6
|
|
|
7
7
|
import pytest
|
|
8
8
|
|
|
9
|
-
from
|
|
10
|
-
from
|
|
9
|
+
from pylitmus.evaluators import Evaluator, EvaluatorFactory
|
|
10
|
+
from pylitmus.exceptions import UnknownOperatorError
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class TestEvaluatorBase:
|
|
@@ -411,7 +411,7 @@ class TestEvaluatorIntegration:
|
|
|
411
411
|
|
|
412
412
|
def test_all_operators_with_simple_condition(self):
|
|
413
413
|
"""Test all operators work correctly through SimpleCondition."""
|
|
414
|
-
from
|
|
414
|
+
from pylitmus.conditions import SimpleCondition
|
|
415
415
|
|
|
416
416
|
# Test a selection of operators
|
|
417
417
|
test_cases = [
|
|
@@ -435,8 +435,8 @@ class TestEvaluatorIntegration:
|
|
|
435
435
|
|
|
436
436
|
def test_temporal_operators_with_rule_engine(self):
|
|
437
437
|
"""Test temporal operators work with RuleEngine."""
|
|
438
|
-
from
|
|
439
|
-
from
|
|
438
|
+
from pylitmus import Rule, RuleEngine, Severity
|
|
439
|
+
from pylitmus.storage import InMemoryRuleRepository
|
|
440
440
|
|
|
441
441
|
yesterday = (datetime.utcnow() - timedelta(days=1)).strftime("%Y-%m-%d")
|
|
442
442
|
old_date = (datetime.utcnow() - timedelta(days=30)).strftime("%Y-%m-%d")
|
|
@@ -4,13 +4,13 @@ Phase 4 Tests: Scoring Strategies - Sum, Weighted, Max strategies
|
|
|
4
4
|
|
|
5
5
|
import pytest
|
|
6
6
|
|
|
7
|
-
from
|
|
7
|
+
from pylitmus.strategies import (
|
|
8
8
|
MaxStrategy,
|
|
9
9
|
ScoringStrategy,
|
|
10
10
|
SumStrategy,
|
|
11
11
|
WeightedStrategy,
|
|
12
12
|
)
|
|
13
|
-
from
|
|
13
|
+
from pylitmus.types import RuleResult, Severity
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
def make_result(score: int, severity: Severity = Severity.MEDIUM) -> RuleResult:
|
|
@@ -326,8 +326,8 @@ class TestStrategyIntegration:
|
|
|
326
326
|
|
|
327
327
|
def test_engine_with_sum_strategy(self):
|
|
328
328
|
"""Test RuleEngine uses SumStrategy correctly."""
|
|
329
|
-
from
|
|
330
|
-
from
|
|
329
|
+
from pylitmus import Rule, RuleEngine, Severity
|
|
330
|
+
from pylitmus.storage import InMemoryRuleRepository
|
|
331
331
|
|
|
332
332
|
rules = [
|
|
333
333
|
Rule(
|
|
@@ -360,8 +360,8 @@ class TestStrategyIntegration:
|
|
|
360
360
|
|
|
361
361
|
def test_engine_with_weighted_strategy(self):
|
|
362
362
|
"""Test RuleEngine uses WeightedStrategy correctly."""
|
|
363
|
-
from
|
|
364
|
-
from
|
|
363
|
+
from pylitmus import Rule, RuleEngine, Severity
|
|
364
|
+
from pylitmus.storage import InMemoryRuleRepository
|
|
365
365
|
|
|
366
366
|
rules = [
|
|
367
367
|
Rule(
|
|
@@ -395,8 +395,8 @@ class TestStrategyIntegration:
|
|
|
395
395
|
|
|
396
396
|
def test_engine_with_max_strategy(self):
|
|
397
397
|
"""Test RuleEngine uses MaxStrategy correctly."""
|
|
398
|
-
from
|
|
399
|
-
from
|
|
398
|
+
from pylitmus import Rule, RuleEngine, Severity
|
|
399
|
+
from pylitmus.storage import InMemoryRuleRepository
|
|
400
400
|
|
|
401
401
|
rules = [
|
|
402
402
|
Rule(
|
|
@@ -429,8 +429,8 @@ class TestStrategyIntegration:
|
|
|
429
429
|
|
|
430
430
|
def test_default_strategy_is_sum(self):
|
|
431
431
|
"""Test that default strategy is SumStrategy."""
|
|
432
|
-
from
|
|
433
|
-
from
|
|
432
|
+
from pylitmus import RuleEngine
|
|
433
|
+
from pylitmus.storage import InMemoryRuleRepository
|
|
434
434
|
|
|
435
435
|
repo = InMemoryRuleRepository([])
|
|
436
436
|
engine = RuleEngine(repository=repo)
|
|
@@ -8,13 +8,13 @@ import tempfile
|
|
|
8
8
|
|
|
9
9
|
import pytest
|
|
10
10
|
|
|
11
|
-
from
|
|
11
|
+
from pylitmus.storage import (
|
|
12
12
|
CachedRuleRepository,
|
|
13
13
|
FileRuleRepository,
|
|
14
14
|
InMemoryRuleRepository,
|
|
15
15
|
RuleRepository,
|
|
16
16
|
)
|
|
17
|
-
from
|
|
17
|
+
from pylitmus.types import Rule, Severity
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
def make_rule(code: str, score: int = 50, enabled: bool = True) -> Rule:
|
|
@@ -516,7 +516,7 @@ class TestStorageIntegration:
|
|
|
516
516
|
|
|
517
517
|
def test_engine_with_file_repository(self):
|
|
518
518
|
"""Test RuleEngine with FileRuleRepository."""
|
|
519
|
-
from
|
|
519
|
+
from pylitmus import RuleEngine
|
|
520
520
|
|
|
521
521
|
content = {
|
|
522
522
|
"rules": [
|
|
@@ -552,7 +552,7 @@ class TestStorageIntegration:
|
|
|
552
552
|
|
|
553
553
|
def test_engine_with_cached_repository(self):
|
|
554
554
|
"""Test RuleEngine with CachedRuleRepository."""
|
|
555
|
-
from
|
|
555
|
+
from pylitmus import RuleEngine
|
|
556
556
|
|
|
557
557
|
inner = InMemoryRuleRepository(
|
|
558
558
|
[
|
|
@@ -4,7 +4,7 @@ Phase 6 Tests: Pattern Matching - Regex, Fuzzy, Range matchers
|
|
|
4
4
|
|
|
5
5
|
import pytest
|
|
6
6
|
|
|
7
|
-
from
|
|
7
|
+
from pylitmus.patterns import (
|
|
8
8
|
EnhancedPatternEngine,
|
|
9
9
|
ExactMatcher,
|
|
10
10
|
FuzzyMatcher,
|
|
@@ -374,8 +374,8 @@ class TestPatternIntegration:
|
|
|
374
374
|
|
|
375
375
|
def test_register_fuzzy_evaluator(self):
|
|
376
376
|
"""Test registering fuzzy matcher as an evaluator."""
|
|
377
|
-
from
|
|
378
|
-
from
|
|
377
|
+
from pylitmus.evaluators import Evaluator, EvaluatorFactory
|
|
378
|
+
from pylitmus.patterns import FuzzyMatcher
|
|
379
379
|
|
|
380
380
|
class FuzzyMatchEvaluator(Evaluator):
|
|
381
381
|
def __init__(self):
|
|
@@ -400,10 +400,10 @@ class TestPatternIntegration:
|
|
|
400
400
|
|
|
401
401
|
def test_patterns_with_rule_engine(self):
|
|
402
402
|
"""Test pattern matching integration with RuleEngine."""
|
|
403
|
-
from
|
|
404
|
-
from
|
|
405
|
-
from
|
|
406
|
-
from
|
|
403
|
+
from pylitmus import Rule, RuleEngine, Severity
|
|
404
|
+
from pylitmus.evaluators import Evaluator, EvaluatorFactory
|
|
405
|
+
from pylitmus.patterns import GlobMatcher
|
|
406
|
+
from pylitmus.storage import InMemoryRuleRepository
|
|
407
407
|
|
|
408
408
|
# Register glob evaluator
|
|
409
409
|
class GlobMatchEvaluator(Evaluator):
|