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,234 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Flask extension for cmap-rules-engine.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
|
|
7
|
+
from ...engine import RuleEngine
|
|
8
|
+
from ...storage import CachedRuleRepository, InMemoryRuleRepository
|
|
9
|
+
from ...strategies import MaxStrategy, SumStrategy, WeightedStrategy
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
from flask import Flask, current_app
|
|
13
|
+
|
|
14
|
+
HAS_FLASK = True
|
|
15
|
+
except ImportError:
|
|
16
|
+
HAS_FLASK = False
|
|
17
|
+
Flask = Any
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def create_repository(config: Dict[str, Any]) -> Any:
|
|
21
|
+
"""
|
|
22
|
+
Create a rule repository from Flask config.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
config: Flask app configuration
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
RuleRepository instance
|
|
29
|
+
"""
|
|
30
|
+
backend = config.get("CMAP_RULES_STORAGE", "memory")
|
|
31
|
+
|
|
32
|
+
if backend == "memory":
|
|
33
|
+
return InMemoryRuleRepository()
|
|
34
|
+
|
|
35
|
+
elif backend == "database":
|
|
36
|
+
from ...storage import DatabaseRuleRepository
|
|
37
|
+
|
|
38
|
+
db_url = config.get("CMAP_RULES_DATABASE_URL")
|
|
39
|
+
if not db_url:
|
|
40
|
+
raise ValueError("CMAP_RULES_DATABASE_URL required for database backend")
|
|
41
|
+
|
|
42
|
+
# Note: User needs to provide their own session factory
|
|
43
|
+
session_factory = config.get("CMAP_RULES_SESSION_FACTORY")
|
|
44
|
+
if not session_factory:
|
|
45
|
+
raise ValueError("CMAP_RULES_SESSION_FACTORY required for database backend")
|
|
46
|
+
|
|
47
|
+
model_class = config.get("CMAP_RULES_MODEL_CLASS")
|
|
48
|
+
return DatabaseRuleRepository(session_factory, model_class)
|
|
49
|
+
|
|
50
|
+
elif backend == "file":
|
|
51
|
+
from ...storage import FileRuleRepository
|
|
52
|
+
|
|
53
|
+
file_path = config.get("CMAP_RULES_FILE_PATH")
|
|
54
|
+
if not file_path:
|
|
55
|
+
raise ValueError("CMAP_RULES_FILE_PATH required for file backend")
|
|
56
|
+
return FileRuleRepository(file_path)
|
|
57
|
+
|
|
58
|
+
else:
|
|
59
|
+
raise ValueError(f"Unknown storage backend: {backend}")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def create_strategy(config: Dict[str, Any]) -> Any:
|
|
63
|
+
"""
|
|
64
|
+
Create a scoring strategy from Flask config.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
config: Flask app configuration
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
ScoringStrategy instance
|
|
71
|
+
"""
|
|
72
|
+
strategy_name = config.get("CMAP_RULES_SCORING_STRATEGY", "sum")
|
|
73
|
+
|
|
74
|
+
strategies = {
|
|
75
|
+
"sum": SumStrategy,
|
|
76
|
+
"weighted": WeightedStrategy,
|
|
77
|
+
"max": MaxStrategy,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if strategy_name not in strategies:
|
|
81
|
+
raise ValueError(f"Unknown strategy: {strategy_name}")
|
|
82
|
+
|
|
83
|
+
return strategies[strategy_name]()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class CmapRulesEngine:
|
|
87
|
+
"""
|
|
88
|
+
Flask extension for cmap-rules-engine.
|
|
89
|
+
|
|
90
|
+
Usage:
|
|
91
|
+
# Initialize
|
|
92
|
+
rules_engine = CmapRulesEngine()
|
|
93
|
+
|
|
94
|
+
# With app factory
|
|
95
|
+
def create_app():
|
|
96
|
+
app = Flask(__name__)
|
|
97
|
+
rules_engine.init_app(app)
|
|
98
|
+
return app
|
|
99
|
+
|
|
100
|
+
# Or direct initialization
|
|
101
|
+
app = Flask(__name__)
|
|
102
|
+
rules_engine = CmapRulesEngine(app)
|
|
103
|
+
|
|
104
|
+
# In routes
|
|
105
|
+
@app.route('/evaluate')
|
|
106
|
+
def evaluate():
|
|
107
|
+
engine = get_engine()
|
|
108
|
+
result = engine.evaluate(data)
|
|
109
|
+
return jsonify(result)
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
def __init__(self, app: Optional[Flask] = None):
|
|
113
|
+
"""
|
|
114
|
+
Initialize the Flask extension.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
app: Flask application instance (optional)
|
|
118
|
+
"""
|
|
119
|
+
if not HAS_FLASK:
|
|
120
|
+
raise ImportError(
|
|
121
|
+
"Flask is required for the Flask integration. "
|
|
122
|
+
"Install it with: pip install cmap-rules-engine[flask]"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
self._engine: Optional[RuleEngine] = None
|
|
126
|
+
|
|
127
|
+
if app is not None:
|
|
128
|
+
self.init_app(app)
|
|
129
|
+
|
|
130
|
+
def init_app(self, app: Flask) -> None:
|
|
131
|
+
"""
|
|
132
|
+
Initialize extension with Flask app.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
app: Flask application instance
|
|
136
|
+
"""
|
|
137
|
+
# Store reference in extensions
|
|
138
|
+
if not hasattr(app, "extensions"):
|
|
139
|
+
app.extensions = {}
|
|
140
|
+
app.extensions["cmap_rules_engine"] = self
|
|
141
|
+
|
|
142
|
+
# Set default configuration
|
|
143
|
+
app.config.setdefault("CMAP_RULES_STORAGE", "memory")
|
|
144
|
+
app.config.setdefault("CMAP_RULES_CACHE", "memory")
|
|
145
|
+
app.config.setdefault("CMAP_RULES_SCORING_STRATEGY", "sum")
|
|
146
|
+
app.config.setdefault("CMAP_RULES_CACHE_TTL", 300)
|
|
147
|
+
app.config.setdefault("CMAP_RULES_THRESHOLDS", {"approve": 30, "review": 70})
|
|
148
|
+
|
|
149
|
+
# Build engine
|
|
150
|
+
self._engine = self._create_engine(app.config)
|
|
151
|
+
|
|
152
|
+
# Register teardown
|
|
153
|
+
app.teardown_appcontext(self._teardown)
|
|
154
|
+
|
|
155
|
+
def _create_engine(self, config: Dict[str, Any]) -> RuleEngine:
|
|
156
|
+
"""
|
|
157
|
+
Create engine from Flask config.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
config: Flask app configuration
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Configured RuleEngine
|
|
164
|
+
"""
|
|
165
|
+
# Create repository
|
|
166
|
+
repository = create_repository(config)
|
|
167
|
+
|
|
168
|
+
# Wrap with cache if enabled
|
|
169
|
+
cache_backend = config.get("CMAP_RULES_CACHE", "memory")
|
|
170
|
+
if cache_backend != "none":
|
|
171
|
+
cache_url = config.get("CMAP_RULES_CACHE_URL")
|
|
172
|
+
cache_ttl = config.get("CMAP_RULES_CACHE_TTL", 300)
|
|
173
|
+
|
|
174
|
+
repository = CachedRuleRepository(
|
|
175
|
+
repository=repository,
|
|
176
|
+
cache_backend=cache_backend,
|
|
177
|
+
cache_url=cache_url,
|
|
178
|
+
ttl_seconds=cache_ttl,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# Create strategy
|
|
182
|
+
strategy = create_strategy(config)
|
|
183
|
+
|
|
184
|
+
# Get thresholds
|
|
185
|
+
thresholds = config.get("CMAP_RULES_THRESHOLDS")
|
|
186
|
+
|
|
187
|
+
return RuleEngine(
|
|
188
|
+
repository=repository,
|
|
189
|
+
scoring_strategy=strategy,
|
|
190
|
+
decision_thresholds=thresholds,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
@property
|
|
194
|
+
def engine(self) -> RuleEngine:
|
|
195
|
+
"""Get the rule engine instance."""
|
|
196
|
+
if self._engine is None:
|
|
197
|
+
raise RuntimeError(
|
|
198
|
+
"CmapRulesEngine not initialized. Call init_app() first."
|
|
199
|
+
)
|
|
200
|
+
return self._engine
|
|
201
|
+
|
|
202
|
+
def _teardown(self, exception: Optional[Exception]) -> None:
|
|
203
|
+
"""Cleanup on request teardown."""
|
|
204
|
+
pass
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def get_engine() -> RuleEngine:
|
|
208
|
+
"""
|
|
209
|
+
Get rule engine from current Flask app.
|
|
210
|
+
|
|
211
|
+
Usage:
|
|
212
|
+
from cmap_rules_engine.integrations.flask import get_engine
|
|
213
|
+
|
|
214
|
+
@app.route('/assess')
|
|
215
|
+
def assess():
|
|
216
|
+
engine = get_engine()
|
|
217
|
+
result = engine.evaluate(request.json)
|
|
218
|
+
return jsonify(result)
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
RuleEngine instance
|
|
222
|
+
|
|
223
|
+
Raises:
|
|
224
|
+
RuntimeError: If not in a Flask application context
|
|
225
|
+
"""
|
|
226
|
+
if not HAS_FLASK:
|
|
227
|
+
raise ImportError("Flask is required for the Flask integration")
|
|
228
|
+
|
|
229
|
+
if "cmap_rules_engine" not in current_app.extensions:
|
|
230
|
+
raise RuntimeError(
|
|
231
|
+
"CmapRulesEngine not initialized for this app. Did you call init_app()?"
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
return current_app.extensions["cmap_rules_engine"].engine
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pattern matching module for the CMAP Rules Engine.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .base import PatternMatcher
|
|
6
|
+
from .engine import EnhancedPatternEngine
|
|
7
|
+
from .exact import ExactMatcher
|
|
8
|
+
from .fuzzy import FuzzyMatcher
|
|
9
|
+
from .glob import GlobMatcher
|
|
10
|
+
from .range import RangeMatcher
|
|
11
|
+
from .regex import RegexMatcher
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"PatternMatcher",
|
|
15
|
+
"ExactMatcher",
|
|
16
|
+
"RegexMatcher",
|
|
17
|
+
"FuzzyMatcher",
|
|
18
|
+
"RangeMatcher",
|
|
19
|
+
"GlobMatcher",
|
|
20
|
+
"EnhancedPatternEngine",
|
|
21
|
+
]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base pattern matcher class.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PatternMatcher(ABC):
|
|
10
|
+
"""Base class for pattern matchers."""
|
|
11
|
+
|
|
12
|
+
@abstractmethod
|
|
13
|
+
def match(self, value: Any, pattern: Any, **options) -> bool:
|
|
14
|
+
"""
|
|
15
|
+
Check if value matches pattern.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
value: Value to check
|
|
19
|
+
pattern: Pattern to match against
|
|
20
|
+
**options: Additional matcher-specific options
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
True if matches, False otherwise
|
|
24
|
+
"""
|
|
25
|
+
pass
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Enhanced pattern engine aggregating all pattern matchers.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict
|
|
6
|
+
|
|
7
|
+
from .base import PatternMatcher
|
|
8
|
+
from .exact import ExactMatcher
|
|
9
|
+
from .fuzzy import FuzzyMatcher
|
|
10
|
+
from .glob import GlobMatcher
|
|
11
|
+
from .range import RangeMatcher
|
|
12
|
+
from .regex import RegexMatcher
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class EnhancedPatternEngine:
|
|
16
|
+
"""Aggregator for all pattern matchers."""
|
|
17
|
+
|
|
18
|
+
def __init__(self):
|
|
19
|
+
"""Initialize with default matchers."""
|
|
20
|
+
self.matchers: Dict[str, PatternMatcher] = {
|
|
21
|
+
"exact": ExactMatcher(),
|
|
22
|
+
"regex": RegexMatcher(),
|
|
23
|
+
"fuzzy": FuzzyMatcher(),
|
|
24
|
+
"range": RangeMatcher(),
|
|
25
|
+
"glob": GlobMatcher(),
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
def match(
|
|
29
|
+
self, value: Any, pattern: Any, pattern_type: str = "exact", **options
|
|
30
|
+
) -> bool:
|
|
31
|
+
"""
|
|
32
|
+
Match value against pattern using specified matcher.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
value: Value to check
|
|
36
|
+
pattern: Pattern to match
|
|
37
|
+
pattern_type: Type of pattern matching
|
|
38
|
+
**options: Matcher-specific options
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
True if matches
|
|
42
|
+
|
|
43
|
+
Raises:
|
|
44
|
+
ValueError: If pattern_type is not registered
|
|
45
|
+
"""
|
|
46
|
+
matcher = self.matchers.get(pattern_type)
|
|
47
|
+
if not matcher:
|
|
48
|
+
raise ValueError(f"Unknown pattern type: {pattern_type}")
|
|
49
|
+
|
|
50
|
+
return matcher.match(value, pattern, **options)
|
|
51
|
+
|
|
52
|
+
def register(self, name: str, matcher: PatternMatcher) -> None:
|
|
53
|
+
"""
|
|
54
|
+
Register a custom pattern matcher.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
name: Matcher name
|
|
58
|
+
matcher: PatternMatcher instance
|
|
59
|
+
"""
|
|
60
|
+
self.matchers[name] = matcher
|
|
61
|
+
|
|
62
|
+
def get_matcher(self, pattern_type: str) -> PatternMatcher:
|
|
63
|
+
"""
|
|
64
|
+
Get a specific matcher.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
pattern_type: Type of matcher
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
PatternMatcher instance
|
|
71
|
+
|
|
72
|
+
Raises:
|
|
73
|
+
ValueError: If pattern_type is not registered
|
|
74
|
+
"""
|
|
75
|
+
matcher = self.matchers.get(pattern_type)
|
|
76
|
+
if not matcher:
|
|
77
|
+
raise ValueError(f"Unknown pattern type: {pattern_type}")
|
|
78
|
+
return matcher
|
|
79
|
+
|
|
80
|
+
def get_supported_types(self) -> list:
|
|
81
|
+
"""Get list of supported pattern types."""
|
|
82
|
+
return list(self.matchers.keys())
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Exact pattern matcher.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .base import PatternMatcher
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ExactMatcher(PatternMatcher):
|
|
11
|
+
"""Exact match pattern matcher."""
|
|
12
|
+
|
|
13
|
+
def match(self, value: Any, pattern: Any, **options) -> bool:
|
|
14
|
+
"""
|
|
15
|
+
Check for exact match.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
value: Value to check
|
|
19
|
+
pattern: Pattern to match
|
|
20
|
+
case_insensitive: If True, ignore case for strings
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
True if exact match
|
|
24
|
+
"""
|
|
25
|
+
if value is None or pattern is None:
|
|
26
|
+
return value == pattern
|
|
27
|
+
|
|
28
|
+
case_insensitive = options.get("case_insensitive", False)
|
|
29
|
+
|
|
30
|
+
if isinstance(value, str) and isinstance(pattern, str):
|
|
31
|
+
if case_insensitive:
|
|
32
|
+
return value.lower() == pattern.lower()
|
|
33
|
+
|
|
34
|
+
return value == pattern
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Fuzzy pattern matcher using difflib.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from difflib import SequenceMatcher
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .base import PatternMatcher
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class FuzzyMatcher(PatternMatcher):
|
|
12
|
+
"""Fuzzy string matcher using difflib."""
|
|
13
|
+
|
|
14
|
+
DEFAULT_THRESHOLD = 0.8
|
|
15
|
+
|
|
16
|
+
def match(self, value: Any, pattern: Any, **options) -> bool:
|
|
17
|
+
"""
|
|
18
|
+
Check if value fuzzy matches pattern.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
value: Value to check
|
|
22
|
+
pattern: Pattern to match
|
|
23
|
+
threshold: Minimum similarity ratio (0.0 to 1.0)
|
|
24
|
+
case_insensitive: If True, ignore case
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
True if similarity >= threshold
|
|
28
|
+
"""
|
|
29
|
+
if value is None or pattern is None:
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
threshold = options.get("threshold", self.DEFAULT_THRESHOLD)
|
|
33
|
+
case_insensitive = options.get("case_insensitive", True)
|
|
34
|
+
|
|
35
|
+
str_value = str(value)
|
|
36
|
+
str_pattern = str(pattern)
|
|
37
|
+
|
|
38
|
+
if case_insensitive:
|
|
39
|
+
str_value = str_value.lower()
|
|
40
|
+
str_pattern = str_pattern.lower()
|
|
41
|
+
|
|
42
|
+
ratio = SequenceMatcher(None, str_value, str_pattern).ratio()
|
|
43
|
+
return ratio >= threshold
|
|
44
|
+
|
|
45
|
+
def get_ratio(
|
|
46
|
+
self, value: Any, pattern: Any, case_insensitive: bool = True
|
|
47
|
+
) -> float:
|
|
48
|
+
"""
|
|
49
|
+
Get the similarity ratio between value and pattern.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
value: Value to check
|
|
53
|
+
pattern: Pattern to compare
|
|
54
|
+
case_insensitive: If True, ignore case
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Similarity ratio (0.0 to 1.0)
|
|
58
|
+
"""
|
|
59
|
+
if value is None or pattern is None:
|
|
60
|
+
return 0.0
|
|
61
|
+
|
|
62
|
+
str_value = str(value)
|
|
63
|
+
str_pattern = str(pattern)
|
|
64
|
+
|
|
65
|
+
if case_insensitive:
|
|
66
|
+
str_value = str_value.lower()
|
|
67
|
+
str_pattern = str_pattern.lower()
|
|
68
|
+
|
|
69
|
+
return SequenceMatcher(None, str_value, str_pattern).ratio()
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Glob pattern matcher.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import fnmatch
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .base import PatternMatcher
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class GlobMatcher(PatternMatcher):
|
|
12
|
+
"""Glob/wildcard pattern matcher using fnmatch."""
|
|
13
|
+
|
|
14
|
+
def match(self, value: Any, pattern: Any, **options) -> bool:
|
|
15
|
+
"""
|
|
16
|
+
Check if value matches glob pattern.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
value: Value to check
|
|
20
|
+
pattern: Glob pattern (e.g., "*.txt", "file_*")
|
|
21
|
+
case_insensitive: If True, ignore case
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
True if matches glob pattern
|
|
25
|
+
"""
|
|
26
|
+
if value is None or pattern is None:
|
|
27
|
+
return False
|
|
28
|
+
|
|
29
|
+
case_insensitive = options.get("case_insensitive", False)
|
|
30
|
+
|
|
31
|
+
str_value = str(value)
|
|
32
|
+
str_pattern = str(pattern)
|
|
33
|
+
|
|
34
|
+
if case_insensitive:
|
|
35
|
+
str_value = str_value.lower()
|
|
36
|
+
str_pattern = str_pattern.lower()
|
|
37
|
+
|
|
38
|
+
return fnmatch.fnmatch(str_value, str_pattern)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Range pattern matcher.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .base import PatternMatcher
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RangeMatcher(PatternMatcher):
|
|
11
|
+
"""Numeric range matcher."""
|
|
12
|
+
|
|
13
|
+
def match(self, value: Any, pattern: Any, **options) -> bool:
|
|
14
|
+
"""
|
|
15
|
+
Check if value is within range.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
value: Value to check
|
|
19
|
+
pattern: Range as [min, max] or {'min': x, 'max': y}
|
|
20
|
+
inclusive: If True (default), range is inclusive
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
True if value is within range
|
|
24
|
+
"""
|
|
25
|
+
if value is None:
|
|
26
|
+
return False
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
num_value = float(value)
|
|
30
|
+
except (ValueError, TypeError):
|
|
31
|
+
return False
|
|
32
|
+
|
|
33
|
+
# Parse pattern
|
|
34
|
+
if isinstance(pattern, (list, tuple)) and len(pattern) == 2:
|
|
35
|
+
min_val, max_val = pattern
|
|
36
|
+
elif isinstance(pattern, dict):
|
|
37
|
+
min_val = pattern.get("min", float("-inf"))
|
|
38
|
+
max_val = pattern.get("max", float("inf"))
|
|
39
|
+
else:
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
inclusive = options.get("inclusive", True)
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
min_val = float(min_val) if min_val is not None else float("-inf")
|
|
46
|
+
max_val = float(max_val) if max_val is not None else float("inf")
|
|
47
|
+
except (ValueError, TypeError):
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
if inclusive:
|
|
51
|
+
return min_val <= num_value <= max_val
|
|
52
|
+
else:
|
|
53
|
+
return min_val < num_value < max_val
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Regex pattern matcher.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from functools import lru_cache
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from .base import PatternMatcher
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RegexMatcher(PatternMatcher):
|
|
13
|
+
"""Regex pattern matcher with compiled pattern cache."""
|
|
14
|
+
|
|
15
|
+
@staticmethod
|
|
16
|
+
@lru_cache(maxsize=1000)
|
|
17
|
+
def _compile(pattern: str, flags: int = 0) -> re.Pattern:
|
|
18
|
+
"""Compile and cache regex pattern."""
|
|
19
|
+
return re.compile(pattern, flags)
|
|
20
|
+
|
|
21
|
+
def match(self, value: Any, pattern: Any, **options) -> bool:
|
|
22
|
+
"""
|
|
23
|
+
Check if value matches regex pattern.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
value: Value to check
|
|
27
|
+
pattern: Regex pattern
|
|
28
|
+
case_insensitive: If True, ignore case
|
|
29
|
+
full_match: If True, require full string match
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
True if matches regex
|
|
33
|
+
"""
|
|
34
|
+
if value is None:
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
case_insensitive = options.get("case_insensitive", False)
|
|
38
|
+
full_match = options.get("full_match", False)
|
|
39
|
+
|
|
40
|
+
flags = re.IGNORECASE if case_insensitive else 0
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
compiled = self._compile(str(pattern), flags)
|
|
44
|
+
str_value = str(value)
|
|
45
|
+
|
|
46
|
+
if full_match:
|
|
47
|
+
return bool(compiled.fullmatch(str_value))
|
|
48
|
+
else:
|
|
49
|
+
return bool(compiled.search(str_value))
|
|
50
|
+
except re.error:
|
|
51
|
+
return False
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Storage backends module for the CMAP Rules Engine.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .base import RuleRepository
|
|
6
|
+
from .cached import CachedRuleRepository
|
|
7
|
+
from .database import DatabaseRuleRepository
|
|
8
|
+
from .file import FileRuleRepository
|
|
9
|
+
from .memory import InMemoryRuleRepository
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"RuleRepository",
|
|
13
|
+
"InMemoryRuleRepository",
|
|
14
|
+
"FileRuleRepository",
|
|
15
|
+
"CachedRuleRepository",
|
|
16
|
+
"DatabaseRuleRepository",
|
|
17
|
+
]
|