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
pylitmus/__init__.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pylitmus - A high-performance rules engine for Python.
|
|
3
|
+
|
|
4
|
+
Evaluate data against configurable rules and get clear verdicts.
|
|
5
|
+
Like a litmus test for your data.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .conditions import CompositeCondition, Condition, ConditionBuilder, SimpleCondition
|
|
9
|
+
from .engine import RuleEngine
|
|
10
|
+
from .evaluators import Evaluator, EvaluatorFactory
|
|
11
|
+
from .exceptions import (
|
|
12
|
+
ConditionError,
|
|
13
|
+
ConfigurationError,
|
|
14
|
+
EvaluationError,
|
|
15
|
+
RuleEngineError,
|
|
16
|
+
StorageError,
|
|
17
|
+
UnknownOperatorError,
|
|
18
|
+
)
|
|
19
|
+
from .factory import create_engine, create_repository, create_strategy
|
|
20
|
+
from .patterns import EnhancedPatternEngine, PatternMatcher
|
|
21
|
+
from .storage import (
|
|
22
|
+
CachedRuleRepository,
|
|
23
|
+
DatabaseRuleRepository,
|
|
24
|
+
FileRuleRepository,
|
|
25
|
+
InMemoryRuleRepository,
|
|
26
|
+
RuleRepository,
|
|
27
|
+
)
|
|
28
|
+
from .strategies import MaxStrategy, ScoringStrategy, SumStrategy, WeightedStrategy
|
|
29
|
+
from .types import AssessmentResult, Operator, Rule, RuleResult, Severity
|
|
30
|
+
|
|
31
|
+
__version__ = "1.0.0"
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
# Main
|
|
35
|
+
"RuleEngine",
|
|
36
|
+
"create_engine",
|
|
37
|
+
"create_repository",
|
|
38
|
+
"create_strategy",
|
|
39
|
+
# Types
|
|
40
|
+
"Rule",
|
|
41
|
+
"RuleResult",
|
|
42
|
+
"AssessmentResult",
|
|
43
|
+
"Operator",
|
|
44
|
+
"Severity",
|
|
45
|
+
# Conditions
|
|
46
|
+
"Condition",
|
|
47
|
+
"SimpleCondition",
|
|
48
|
+
"CompositeCondition",
|
|
49
|
+
"ConditionBuilder",
|
|
50
|
+
# Evaluators
|
|
51
|
+
"Evaluator",
|
|
52
|
+
"EvaluatorFactory",
|
|
53
|
+
# Strategies
|
|
54
|
+
"ScoringStrategy",
|
|
55
|
+
"SumStrategy",
|
|
56
|
+
"WeightedStrategy",
|
|
57
|
+
"MaxStrategy",
|
|
58
|
+
# Storage
|
|
59
|
+
"RuleRepository",
|
|
60
|
+
"InMemoryRuleRepository",
|
|
61
|
+
"DatabaseRuleRepository",
|
|
62
|
+
"FileRuleRepository",
|
|
63
|
+
"CachedRuleRepository",
|
|
64
|
+
# Patterns
|
|
65
|
+
"PatternMatcher",
|
|
66
|
+
"EnhancedPatternEngine",
|
|
67
|
+
# Exceptions
|
|
68
|
+
"RuleEngineError",
|
|
69
|
+
"UnknownOperatorError",
|
|
70
|
+
"ConditionError",
|
|
71
|
+
"StorageError",
|
|
72
|
+
"ConfigurationError",
|
|
73
|
+
"EvaluationError",
|
|
74
|
+
# Version
|
|
75
|
+
"__version__",
|
|
76
|
+
]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Conditions module for the CMAP Rules Engine.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .base import Condition
|
|
6
|
+
from .builder import ConditionBuilder
|
|
7
|
+
from .composite import CompositeCondition
|
|
8
|
+
from .simple import SimpleCondition
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"Condition",
|
|
12
|
+
"SimpleCondition",
|
|
13
|
+
"CompositeCondition",
|
|
14
|
+
"ConditionBuilder",
|
|
15
|
+
]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base condition class for the CMAP Rules Engine.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from typing import Any, Dict
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Condition(ABC):
|
|
10
|
+
"""Abstract base class for all conditions."""
|
|
11
|
+
|
|
12
|
+
@abstractmethod
|
|
13
|
+
def evaluate(self, data: Dict[str, Any]) -> bool:
|
|
14
|
+
"""
|
|
15
|
+
Evaluate condition against data.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
data: Data dictionary to evaluate
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
True if condition is met, False otherwise
|
|
22
|
+
"""
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
@abstractmethod
|
|
26
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
27
|
+
"""
|
|
28
|
+
Serialize condition to dictionary.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Dictionary representation of condition
|
|
32
|
+
"""
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
def get_nested_value(self, data: Dict[str, Any], field_path: str) -> Any:
|
|
36
|
+
"""
|
|
37
|
+
Get value from nested dict using dot notation.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
data: Data dictionary
|
|
41
|
+
field_path: Dot-separated path (e.g., 'provider.type')
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Value at path or None if not found
|
|
45
|
+
"""
|
|
46
|
+
keys = field_path.split(".")
|
|
47
|
+
value = data
|
|
48
|
+
for key in keys:
|
|
49
|
+
if isinstance(value, dict):
|
|
50
|
+
value = value.get(key)
|
|
51
|
+
else:
|
|
52
|
+
return None
|
|
53
|
+
return value
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Condition builder for creating conditions from dictionaries.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict
|
|
6
|
+
|
|
7
|
+
from .base import Condition
|
|
8
|
+
from .composite import CompositeCondition
|
|
9
|
+
from .simple import SimpleCondition
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ConditionBuilder:
|
|
13
|
+
"""Build Condition objects from dictionary definitions."""
|
|
14
|
+
|
|
15
|
+
def build(self, definition: Dict[str, Any]) -> Condition:
|
|
16
|
+
"""
|
|
17
|
+
Build a Condition from a dictionary definition.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
definition: Dictionary defining the condition
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Condition object
|
|
24
|
+
|
|
25
|
+
Examples:
|
|
26
|
+
# Simple condition
|
|
27
|
+
builder.build({
|
|
28
|
+
'field': 'amount',
|
|
29
|
+
'operator': 'greater_than',
|
|
30
|
+
'value': 1000
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
# Composite condition (AND)
|
|
34
|
+
builder.build({
|
|
35
|
+
'all': [
|
|
36
|
+
{'field': 'amount', 'operator': 'greater_than', 'value': 1000},
|
|
37
|
+
{'field': 'status', 'operator': 'equals', 'value': 'pending'}
|
|
38
|
+
]
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
# Composite condition (OR)
|
|
42
|
+
builder.build({
|
|
43
|
+
'any': [
|
|
44
|
+
{'field': 'type', 'operator': 'equals', 'value': 'urgent'},
|
|
45
|
+
{'field': 'priority', 'operator': 'equals', 'value': 'high'}
|
|
46
|
+
]
|
|
47
|
+
})
|
|
48
|
+
"""
|
|
49
|
+
if "all" in definition:
|
|
50
|
+
return self._build_composite("all", definition["all"])
|
|
51
|
+
elif "any" in definition:
|
|
52
|
+
return self._build_composite("any", definition["any"])
|
|
53
|
+
elif "type" in definition and "conditions" in definition:
|
|
54
|
+
# Support alternative format: {"type": "AND", "conditions": [...]}
|
|
55
|
+
condition_type = definition["type"].upper()
|
|
56
|
+
operator = "all" if condition_type == "AND" else "any"
|
|
57
|
+
return self._build_composite(operator, definition["conditions"])
|
|
58
|
+
else:
|
|
59
|
+
return self._build_simple(definition)
|
|
60
|
+
|
|
61
|
+
def _build_composite(
|
|
62
|
+
self, operator: str, conditions_data: list
|
|
63
|
+
) -> CompositeCondition:
|
|
64
|
+
"""
|
|
65
|
+
Build a composite condition.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
operator: 'all' or 'any'
|
|
69
|
+
conditions_data: List of condition definitions
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
CompositeCondition object
|
|
73
|
+
"""
|
|
74
|
+
conditions = [self.build(c) for c in conditions_data]
|
|
75
|
+
return CompositeCondition(operator=operator, conditions=conditions)
|
|
76
|
+
|
|
77
|
+
def _build_simple(self, definition: Dict[str, Any]) -> SimpleCondition:
|
|
78
|
+
"""
|
|
79
|
+
Build a simple condition.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
definition: Dictionary with field, operator, value
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
SimpleCondition object
|
|
86
|
+
"""
|
|
87
|
+
return SimpleCondition(
|
|
88
|
+
field=definition["field"],
|
|
89
|
+
operator=definition["operator"],
|
|
90
|
+
value=definition.get("value"),
|
|
91
|
+
case_sensitive=definition.get("case_sensitive", True),
|
|
92
|
+
)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Composite condition implementation.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, List, Literal
|
|
6
|
+
|
|
7
|
+
from .base import Condition
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CompositeCondition(Condition):
|
|
11
|
+
"""A composite condition combining multiple conditions with AND/OR."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, operator: Literal["all", "any"], conditions: List[Condition]):
|
|
14
|
+
"""
|
|
15
|
+
Initialize a composite condition.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
operator: 'all' for AND logic, 'any' for OR logic
|
|
19
|
+
conditions: List of conditions to combine
|
|
20
|
+
"""
|
|
21
|
+
self.operator = operator
|
|
22
|
+
self.conditions = conditions
|
|
23
|
+
|
|
24
|
+
def evaluate(self, data: Dict[str, Any]) -> bool:
|
|
25
|
+
"""
|
|
26
|
+
Evaluate the composite condition against data.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
data: Data dictionary to evaluate
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
True if condition is met based on operator logic
|
|
33
|
+
"""
|
|
34
|
+
if not self.conditions:
|
|
35
|
+
return True
|
|
36
|
+
|
|
37
|
+
if self.operator == "all":
|
|
38
|
+
return all(c.evaluate(data) for c in self.conditions)
|
|
39
|
+
else: # 'any'
|
|
40
|
+
return any(c.evaluate(data) for c in self.conditions)
|
|
41
|
+
|
|
42
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
43
|
+
"""
|
|
44
|
+
Serialize condition to dictionary.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Dictionary representation of condition
|
|
48
|
+
"""
|
|
49
|
+
return {self.operator: [c.to_dict() for c in self.conditions]}
|
|
50
|
+
|
|
51
|
+
def __repr__(self) -> str:
|
|
52
|
+
return f"CompositeCondition({self.operator}, {len(self.conditions)} conditions)"
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Simple condition implementation.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict
|
|
6
|
+
|
|
7
|
+
from ..evaluators import EvaluatorFactory
|
|
8
|
+
from .base import Condition
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SimpleCondition(Condition):
|
|
12
|
+
"""A simple condition that compares a field to a value."""
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self, field: str, operator: str, value: Any, case_sensitive: bool = True
|
|
16
|
+
):
|
|
17
|
+
"""
|
|
18
|
+
Initialize a simple condition.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
field: Field path to evaluate (supports dot notation)
|
|
22
|
+
operator: Comparison operator name
|
|
23
|
+
value: Value to compare against
|
|
24
|
+
case_sensitive: Whether string comparisons are case-sensitive
|
|
25
|
+
"""
|
|
26
|
+
self.field = field
|
|
27
|
+
self.operator = operator
|
|
28
|
+
self.value = value
|
|
29
|
+
self.case_sensitive = case_sensitive
|
|
30
|
+
|
|
31
|
+
def evaluate(self, data: Dict[str, Any]) -> bool:
|
|
32
|
+
"""
|
|
33
|
+
Evaluate the condition against data.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
data: Data dictionary to evaluate
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
True if condition is met, False otherwise
|
|
40
|
+
"""
|
|
41
|
+
field_value = self.get_nested_value(data, self.field)
|
|
42
|
+
evaluator = EvaluatorFactory.get(self.operator)
|
|
43
|
+
return evaluator.evaluate(field_value, self.value, self.case_sensitive)
|
|
44
|
+
|
|
45
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
46
|
+
"""
|
|
47
|
+
Serialize condition to dictionary.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Dictionary representation of condition
|
|
51
|
+
"""
|
|
52
|
+
result = {
|
|
53
|
+
"field": self.field,
|
|
54
|
+
"operator": self.operator,
|
|
55
|
+
"value": self.value,
|
|
56
|
+
}
|
|
57
|
+
if not self.case_sensitive:
|
|
58
|
+
result["case_sensitive"] = False
|
|
59
|
+
return result
|
|
60
|
+
|
|
61
|
+
def __repr__(self) -> str:
|
|
62
|
+
return f"SimpleCondition({self.field} {self.operator} {self.value})"
|
pylitmus/engine.py
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Main RuleEngine class for evaluating data against configured rules.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import time
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
from .exceptions import EvaluationError
|
|
10
|
+
from .types import AssessmentResult, Rule, RuleResult
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from .storage.base import RuleRepository
|
|
14
|
+
from .strategies.base import ScoringStrategy
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class RuleEngine:
|
|
20
|
+
"""
|
|
21
|
+
Main rules engine for evaluating data against configured rules.
|
|
22
|
+
|
|
23
|
+
Usage:
|
|
24
|
+
engine = RuleEngine(
|
|
25
|
+
repository=DatabaseRuleRepository(db_url),
|
|
26
|
+
scoring_strategy=WeightedStrategy()
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
result = engine.evaluate(claim, context)
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
DEFAULT_THRESHOLDS = {
|
|
33
|
+
"approve": 30,
|
|
34
|
+
"review": 70,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
repository: "RuleRepository",
|
|
40
|
+
scoring_strategy: Optional["ScoringStrategy"] = None,
|
|
41
|
+
decision_thresholds: Optional[Dict[str, int]] = None,
|
|
42
|
+
condition_builder: Optional[Any] = None,
|
|
43
|
+
):
|
|
44
|
+
"""
|
|
45
|
+
Initialize the rule engine.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
repository: Rule storage backend
|
|
49
|
+
scoring_strategy: Strategy for calculating scores
|
|
50
|
+
decision_thresholds: Custom thresholds for decisions
|
|
51
|
+
condition_builder: Builder for creating conditions from dicts
|
|
52
|
+
"""
|
|
53
|
+
self.repository = repository
|
|
54
|
+
self._scoring_strategy = scoring_strategy
|
|
55
|
+
self._thresholds = decision_thresholds or self.DEFAULT_THRESHOLDS.copy()
|
|
56
|
+
self._condition_builder = condition_builder
|
|
57
|
+
self._rules_cache: Optional[List[Rule]] = None
|
|
58
|
+
|
|
59
|
+
logger.info("RuleEngine initialized")
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def scoring_strategy(self) -> "ScoringStrategy":
|
|
63
|
+
"""Get the scoring strategy, creating default if needed."""
|
|
64
|
+
if self._scoring_strategy is None:
|
|
65
|
+
from .strategies import SumStrategy
|
|
66
|
+
|
|
67
|
+
self._scoring_strategy = SumStrategy()
|
|
68
|
+
return self._scoring_strategy
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def condition_builder(self) -> Any:
|
|
72
|
+
"""Get the condition builder, creating default if needed."""
|
|
73
|
+
if self._condition_builder is None:
|
|
74
|
+
from .conditions import ConditionBuilder
|
|
75
|
+
|
|
76
|
+
self._condition_builder = ConditionBuilder()
|
|
77
|
+
return self._condition_builder
|
|
78
|
+
|
|
79
|
+
def evaluate(
|
|
80
|
+
self,
|
|
81
|
+
data: Dict[str, Any],
|
|
82
|
+
context: Optional[Dict[str, Any]] = None,
|
|
83
|
+
filters: Optional[Dict[str, Any]] = None,
|
|
84
|
+
) -> AssessmentResult:
|
|
85
|
+
"""
|
|
86
|
+
Evaluate data against all applicable rules.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
data: Data to evaluate
|
|
90
|
+
context: Additional context for evaluation
|
|
91
|
+
filters: Filters to apply when fetching rules
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
AssessmentResult with score, decision, and triggered rules
|
|
95
|
+
"""
|
|
96
|
+
start_time = time.time()
|
|
97
|
+
context = context or {}
|
|
98
|
+
|
|
99
|
+
logger.debug(f"Starting evaluation with {len(data)} data fields")
|
|
100
|
+
|
|
101
|
+
# Get applicable rules
|
|
102
|
+
rules = self.repository.get_enabled(filters)
|
|
103
|
+
|
|
104
|
+
triggered_results: List[RuleResult] = []
|
|
105
|
+
all_results: List[RuleResult] = []
|
|
106
|
+
|
|
107
|
+
for rule in rules:
|
|
108
|
+
try:
|
|
109
|
+
result = self.evaluate_rule(rule, data, context)
|
|
110
|
+
all_results.append(result)
|
|
111
|
+
|
|
112
|
+
if result.triggered:
|
|
113
|
+
triggered_results.append(result)
|
|
114
|
+
logger.debug(
|
|
115
|
+
f"Rule {rule.code} triggered with score {result.score}"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
except Exception as e:
|
|
119
|
+
logger.error(f"Error evaluating rule {rule.code}: {e}")
|
|
120
|
+
raise EvaluationError(
|
|
121
|
+
f"Failed to evaluate rule {rule.code}: {e}"
|
|
122
|
+
) from e
|
|
123
|
+
|
|
124
|
+
# Calculate total score
|
|
125
|
+
total_score = self.scoring_strategy.calculate(triggered_results)
|
|
126
|
+
|
|
127
|
+
# Determine decision
|
|
128
|
+
decision = self._make_decision(total_score)
|
|
129
|
+
|
|
130
|
+
processing_time = (time.time() - start_time) * 1000
|
|
131
|
+
|
|
132
|
+
result = AssessmentResult(
|
|
133
|
+
total_score=total_score,
|
|
134
|
+
decision=decision,
|
|
135
|
+
triggered_rules=triggered_results,
|
|
136
|
+
all_rules_evaluated=len(rules),
|
|
137
|
+
processing_time_ms=processing_time,
|
|
138
|
+
metadata={"context": context, "filters": filters},
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
logger.info(
|
|
142
|
+
f"Evaluation complete: score={total_score}, decision={decision}, "
|
|
143
|
+
f"triggered={len(triggered_results)}/{len(rules)}, time={processing_time:.2f}ms"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
return result
|
|
147
|
+
|
|
148
|
+
def evaluate_rule(
|
|
149
|
+
self, rule: Rule, data: Dict[str, Any], context: Optional[Dict[str, Any]] = None
|
|
150
|
+
) -> RuleResult:
|
|
151
|
+
"""
|
|
152
|
+
Evaluate a single rule against data.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
rule: Rule to evaluate
|
|
156
|
+
data: Data to evaluate against
|
|
157
|
+
context: Additional context
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
RuleResult indicating if rule triggered
|
|
161
|
+
"""
|
|
162
|
+
context = context or {}
|
|
163
|
+
|
|
164
|
+
# Check if rule is effective
|
|
165
|
+
if not rule.is_effective():
|
|
166
|
+
return RuleResult(
|
|
167
|
+
rule_code=rule.code,
|
|
168
|
+
rule_name=rule.name,
|
|
169
|
+
triggered=False,
|
|
170
|
+
score=0,
|
|
171
|
+
severity=rule.severity,
|
|
172
|
+
category=rule.category,
|
|
173
|
+
explanation="Rule is not currently effective",
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# Build condition from rule definition
|
|
177
|
+
condition = self.condition_builder.build(rule.conditions)
|
|
178
|
+
|
|
179
|
+
# Evaluate condition
|
|
180
|
+
triggered = condition.evaluate(data)
|
|
181
|
+
|
|
182
|
+
return RuleResult(
|
|
183
|
+
rule_code=rule.code,
|
|
184
|
+
rule_name=rule.name,
|
|
185
|
+
triggered=triggered,
|
|
186
|
+
score=rule.score if triggered else 0,
|
|
187
|
+
severity=rule.severity,
|
|
188
|
+
category=rule.category,
|
|
189
|
+
explanation=rule.description if triggered else "Condition not met",
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
def _make_decision(self, score: int) -> str:
|
|
193
|
+
"""
|
|
194
|
+
Make a decision based on the total score.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
score: Total calculated score
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
Decision string: 'APPROVE', 'REVIEW', or 'FLAG'
|
|
201
|
+
"""
|
|
202
|
+
approve_threshold = self._thresholds.get("approve", 30)
|
|
203
|
+
review_threshold = self._thresholds.get("review", 70)
|
|
204
|
+
|
|
205
|
+
if score < approve_threshold:
|
|
206
|
+
return "APPROVE"
|
|
207
|
+
elif score < review_threshold:
|
|
208
|
+
return "REVIEW"
|
|
209
|
+
else:
|
|
210
|
+
return "FLAG"
|
|
211
|
+
|
|
212
|
+
def reload_rules(self) -> None:
|
|
213
|
+
"""Force reload rules from repository."""
|
|
214
|
+
self._rules_cache = None
|
|
215
|
+
|
|
216
|
+
# If repository has cache, invalidate it
|
|
217
|
+
if hasattr(self.repository, "invalidate"):
|
|
218
|
+
self.repository.invalidate()
|
|
219
|
+
|
|
220
|
+
logger.info("Rules cache invalidated")
|
|
221
|
+
|
|
222
|
+
def get_rules(self, filters: Optional[Dict[str, Any]] = None) -> List[Rule]:
|
|
223
|
+
"""
|
|
224
|
+
Get all rules, optionally filtered.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
filters: Optional filters to apply
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
List of rules
|
|
231
|
+
"""
|
|
232
|
+
return self.repository.get_all(filters)
|
|
233
|
+
|
|
234
|
+
def get_rule(self, code: str) -> Optional[Rule]:
|
|
235
|
+
"""
|
|
236
|
+
Get a specific rule by code.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
code: Rule code
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
Rule if found, None otherwise
|
|
243
|
+
"""
|
|
244
|
+
return self.repository.get_by_code(code)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base evaluator class for the CMAP Rules Engine.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Evaluator(ABC):
|
|
10
|
+
"""Abstract base class for condition evaluators."""
|
|
11
|
+
|
|
12
|
+
@abstractmethod
|
|
13
|
+
def evaluate(
|
|
14
|
+
self, field_value: Any, compare_value: Any, case_sensitive: bool = True
|
|
15
|
+
) -> bool:
|
|
16
|
+
"""
|
|
17
|
+
Evaluate a condition.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
field_value: The actual value from data
|
|
21
|
+
compare_value: The value to compare against
|
|
22
|
+
case_sensitive: Whether to use case-sensitive comparison
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
True if condition is met, False otherwise
|
|
26
|
+
"""
|
|
27
|
+
pass
|