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,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
+ [![PyPI version](https://badge.fury.io/py/pylitmus.svg)](https://badge.fury.io/py/pylitmus)
45
+ [![Python](https://img.shields.io/pypi/pyversions/pylitmus.svg)](https://pypi.org/project/pylitmus/)
46
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
47
+ [![Tests](https://github.com/org/pylitmus/workflows/Tests/badge.svg)](https://github.com/org/pylitmus/actions)
48
+ [![Coverage](https://codecov.io/gh/org/pylitmus/branch/main/graph/badge.svg)](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.