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.
@@ -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
+ ]