pyspecification 0.1.4__tar.gz → 2.0.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.
- {pyspecification-0.1.4 → pyspecification-2.0.0}/PKG-INFO +116 -185
- {pyspecification-0.1.4 → pyspecification-2.0.0}/README.md +115 -183
- {pyspecification-0.1.4 → pyspecification-2.0.0}/pyproject.toml +2 -2
- {pyspecification-0.1.4 → pyspecification-2.0.0}/pyproject.toml.orig +2 -2
- {pyspecification-0.1.4 → pyspecification-2.0.0}/src/pyspecification/__init__.py +2 -4
- {pyspecification-0.1.4 → pyspecification-2.0.0}/src/pyspecification/compilers.py +15 -23
- pyspecification-2.0.0/src/pyspecification/json_schema.py +116 -0
- {pyspecification-0.1.4 → pyspecification-2.0.0}/src/pyspecification/predicate.py +5 -1
- {pyspecification-0.1.4 → pyspecification-2.0.0}/src/pyspecification/registry.py +4 -20
- {pyspecification-0.1.4 → pyspecification-2.0.0}/src/pyspecification/rules.py +3 -14
- pyspecification-0.1.4/src/pyspecification/constants.py +0 -1
- pyspecification-0.1.4/src/pyspecification/json_schema.py +0 -64
- pyspecification-0.1.4/src/pyspecification/schemas.py +0 -135
- {pyspecification-0.1.4 → pyspecification-2.0.0}/src/pyspecification/exceptions.py +0 -0
- {pyspecification-0.1.4 → pyspecification-2.0.0}/src/pyspecification/processors.py +0 -0
- {pyspecification-0.1.4 → pyspecification-2.0.0}/src/pyspecification/validators.py +0 -0
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: pyspecification
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
Summary: A lightweight, typed Python library for composing business rules as reusable, executable predicates.
|
|
5
5
|
Author: aaltatan
|
|
6
6
|
Author-email: aaltatan <a.altatan@gmail.com>
|
|
7
7
|
Classifier: Programming Language :: Python :: 3
|
|
8
8
|
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
9
9
|
Classifier: Operating System :: OS Independent
|
|
10
|
-
Requires-Dist: pydantic==2.5.3
|
|
11
10
|
Requires-Python: >=3.12
|
|
12
11
|
Project-URL: Homepage, https://github.com/aaltatan/pyspecification
|
|
13
12
|
Project-URL: Issues, https://github.com/aaltatan/pyspecification/issues
|
|
@@ -25,7 +24,7 @@ This project was inspired by the work and ideas shared by [ArjanCodes](https://g
|
|
|
25
24
|
- predicates compose with `&`, `|`, and `~`
|
|
26
25
|
- registry-based registration keeps rules organized
|
|
27
26
|
- structured rule definitions can be compiled from dictionaries or JSON-like payloads
|
|
28
|
-
-
|
|
27
|
+
- structured rule definitions make rule metadata portable and machine-readable
|
|
29
28
|
|
|
30
29
|
It is especially useful for filtering, validation, authorization checks, and declarative rule engines without introducing a heavy framework.
|
|
31
30
|
|
|
@@ -68,7 +67,6 @@ This keeps your logic:
|
|
|
68
67
|
- Registry pattern for rule registration and lookup
|
|
69
68
|
- Custom argument processors for coercion and normalization
|
|
70
69
|
- `PredicateCompiler` for compiling structured rule dictionaries into executable predicates
|
|
71
|
-
- `RuleSchema` validation for declarative rule payloads
|
|
72
70
|
- JSON schema generation for rule arguments and return types
|
|
73
71
|
- Support for both logical and bitwise operator modes
|
|
74
72
|
- Hidden rules and custom naming for internal/private rule registration
|
|
@@ -251,7 +249,7 @@ One of the strongest real-world use cases is turning rule definitions into SQLAl
|
|
|
251
249
|
```python
|
|
252
250
|
from typing import Any
|
|
253
251
|
|
|
254
|
-
from pyspecification import ObjectRulesRegistry, Predicate, PredicateCompiler
|
|
252
|
+
from pyspecification import ObjectRulesRegistry, Predicate, PredicateCompiler
|
|
255
253
|
from sqlalchemy import ColumnElement, and_, create_engine, or_
|
|
256
254
|
from sqlalchemy.orm import DeclarativeBase, Mapped, Session, mapped_column, sessionmaker
|
|
257
255
|
|
|
@@ -313,24 +311,36 @@ def age__le(model: type[User], value: int) -> ColumnElement[bool]:
|
|
|
313
311
|
compiler = PredicateCompiler(
|
|
314
312
|
rules.rules,
|
|
315
313
|
lambda schema: Predicate(
|
|
316
|
-
lambda _: and_(True) if schema["operator"] == "
|
|
314
|
+
lambda _: and_(True) if schema["operator"] == "all" else or_(False),
|
|
317
315
|
operator="bitwise",
|
|
318
316
|
),
|
|
319
317
|
)
|
|
320
318
|
|
|
321
319
|
filter_rule_data = {
|
|
322
|
-
"operator": "
|
|
320
|
+
"operator": "any",
|
|
323
321
|
"expressions": [
|
|
324
|
-
{
|
|
325
|
-
|
|
322
|
+
{
|
|
323
|
+
"name": "is_admin",
|
|
324
|
+
"args": [],
|
|
325
|
+
"kwargs": {},
|
|
326
|
+
"inverse": False,
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
"name": "age__ge",
|
|
330
|
+
"args": [18],
|
|
331
|
+
"kwargs": {},
|
|
332
|
+
"inverse": False,
|
|
333
|
+
}
|
|
326
334
|
],
|
|
327
335
|
}
|
|
328
336
|
|
|
329
|
-
predicate = compiler.compile(
|
|
337
|
+
predicate = compiler.compile(filter_rule_data)
|
|
338
|
+
# is_admin() | age__ge(18)
|
|
339
|
+
# User.is_admin | User.age >= 18
|
|
330
340
|
|
|
331
341
|
with SessionLocal() as session:
|
|
332
342
|
users = session.query(User).filter(predicate(User)).all()
|
|
333
|
-
print([user.name for user in users])
|
|
343
|
+
print([user.name for user in users]) # ['Abdullah', 'Bob', 'Charlie', 'Eve']
|
|
334
344
|
```
|
|
335
345
|
|
|
336
346
|
This pattern is especially useful when you want:
|
|
@@ -397,7 +407,7 @@ If conversion fails, the library raises `ProcessArgumentError`.
|
|
|
397
407
|
```python
|
|
398
408
|
from dataclasses import dataclass
|
|
399
409
|
|
|
400
|
-
from pyspecification import Predicate, PredicateCompiler,
|
|
410
|
+
from pyspecification import Predicate, PredicateCompiler, object_rule
|
|
401
411
|
|
|
402
412
|
|
|
403
413
|
@dataclass
|
|
@@ -430,83 +440,48 @@ rules = {
|
|
|
430
440
|
|
|
431
441
|
compiler = PredicateCompiler(
|
|
432
442
|
rules,
|
|
433
|
-
lambda schema: Predicate(lambda _: schema["operator"] == "
|
|
443
|
+
lambda schema: Predicate(lambda _: schema["operator"] == "all", operator="logical"),
|
|
434
444
|
)
|
|
435
445
|
|
|
436
446
|
rule_data = {
|
|
437
|
-
"operator": "
|
|
447
|
+
"operator": "any",
|
|
438
448
|
"expressions": [
|
|
439
|
-
{"is_admin": []},
|
|
440
449
|
{
|
|
450
|
+
"name": "is_admin",
|
|
451
|
+
"args": [],
|
|
452
|
+
"kwargs": {},
|
|
453
|
+
"inverse": False,
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
"operator": "all",
|
|
441
457
|
"expressions": [
|
|
442
|
-
{
|
|
443
|
-
|
|
444
|
-
|
|
458
|
+
{
|
|
459
|
+
"name": "name__istartswith",
|
|
460
|
+
"args": ["admin"],
|
|
461
|
+
"kwargs": {},
|
|
462
|
+
"inverse": False,
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
"name": "age__between",
|
|
466
|
+
"args": [18, 30],
|
|
467
|
+
"kwargs": {},
|
|
468
|
+
"inverse": False,
|
|
469
|
+
},
|
|
470
|
+
],
|
|
445
471
|
},
|
|
446
472
|
],
|
|
447
473
|
}
|
|
448
474
|
|
|
449
|
-
predicate = compiler.compile(
|
|
450
|
-
|
|
451
|
-
```
|
|
452
|
-
|
|
453
|
-
### Predicate schema
|
|
454
|
-
|
|
455
|
-
`PredicateCompiler.compile()` consumes the normalized dictionary produced by
|
|
456
|
-
`RuleSchema(...).model_dump()`. A predicate schema always has four keys:
|
|
457
|
-
|
|
458
|
-
```json
|
|
459
|
-
{
|
|
460
|
-
"name": "rule_name",
|
|
461
|
-
"args": [],
|
|
462
|
-
"kwargs": {},
|
|
463
|
-
"inverse": false
|
|
464
|
-
}
|
|
465
|
-
```
|
|
466
|
-
|
|
467
|
-
The `name` must be present in the dictionary of rules passed to the compiler.
|
|
468
|
-
Use `args` for positional arguments and `kwargs` for keyword arguments. These
|
|
469
|
-
examples assume the rules from the previous section:
|
|
470
|
-
|
|
471
|
-
```json
|
|
472
|
-
{
|
|
473
|
-
"name": "is_admin",
|
|
474
|
-
"args": [],
|
|
475
|
-
"kwargs": {},
|
|
476
|
-
"inverse": false
|
|
477
|
-
}
|
|
478
|
-
```
|
|
479
|
-
|
|
480
|
-
```json
|
|
481
|
-
{
|
|
482
|
-
"name": "name__istartswith",
|
|
483
|
-
"args": ["admin"],
|
|
484
|
-
"kwargs": {},
|
|
485
|
-
"inverse": false
|
|
486
|
-
}
|
|
487
|
-
```
|
|
488
|
-
|
|
489
|
-
```json
|
|
490
|
-
{
|
|
491
|
-
"name": "age__between",
|
|
492
|
-
"args": [],
|
|
493
|
-
"kwargs": {"min_age": 18, "max_age": 30},
|
|
494
|
-
"inverse": false
|
|
495
|
-
}
|
|
496
|
-
```
|
|
497
|
-
|
|
498
|
-
Set `inverse` to `true` to negate one predicate:
|
|
475
|
+
predicate = compiler.compile(rule_data)
|
|
476
|
+
# is_admin() | (name__istartswith("admin") & age__between(18, 30))
|
|
499
477
|
|
|
500
|
-
|
|
501
|
-
{
|
|
502
|
-
"name": "is_admin",
|
|
503
|
-
"args": [],
|
|
504
|
-
"kwargs": {},
|
|
505
|
-
"inverse": true
|
|
506
|
-
}
|
|
478
|
+
print(predicate(User("admin", 25, True))) # True
|
|
507
479
|
```
|
|
508
480
|
|
|
509
|
-
|
|
481
|
+
`PredicateCompiler.compile()` consumes a normalized dictionary. The `name`
|
|
482
|
+
must be present in the dictionary of rules passed to the compiler. Use `args`
|
|
483
|
+
for positional arguments, `kwargs` for keyword arguments, and `inverse` to
|
|
484
|
+
negate one predicate:
|
|
510
485
|
|
|
511
486
|
```python
|
|
512
487
|
import json
|
|
@@ -514,16 +489,16 @@ import json
|
|
|
514
489
|
rule_json = '{"name": "age__between", "args": [18, 30], "kwargs": {}, "inverse": false}'
|
|
515
490
|
rule_data = json.loads(rule_json)
|
|
516
491
|
predicate = compiler.compile(rule_data)
|
|
517
|
-
```
|
|
518
492
|
|
|
519
|
-
|
|
493
|
+
print(repr(predicate)) # Predicate(age__between)
|
|
494
|
+
```
|
|
520
495
|
|
|
521
|
-
Use a wrapper to combine predicates. A wrapper has an `operator
|
|
522
|
-
`expressions` list
|
|
496
|
+
Use a wrapper to combine predicates. A wrapper has an `operator` and an
|
|
497
|
+
`expressions` list:
|
|
523
498
|
|
|
524
499
|
```json
|
|
525
500
|
{
|
|
526
|
-
"operator": "
|
|
501
|
+
"operator": "all",
|
|
527
502
|
"expressions": [
|
|
528
503
|
{
|
|
529
504
|
"name": "is_admin",
|
|
@@ -537,17 +512,16 @@ Use a wrapper to combine predicates. A wrapper has an `operator`, an
|
|
|
537
512
|
"kwargs": {},
|
|
538
513
|
"inverse": false
|
|
539
514
|
}
|
|
540
|
-
]
|
|
541
|
-
|
|
542
|
-
}
|
|
515
|
+
]
|
|
516
|
+
} // is_admin() & age__between(18, 30)
|
|
543
517
|
```
|
|
544
518
|
|
|
545
|
-
`operator` must be either `"
|
|
519
|
+
`operator` must be either `"all"` or `"any"`. Expressions can be nested to
|
|
546
520
|
represent more complex logic:
|
|
547
521
|
|
|
548
522
|
```json
|
|
549
523
|
{
|
|
550
|
-
"operator": "
|
|
524
|
+
"operator": "any",
|
|
551
525
|
"expressions": [
|
|
552
526
|
{
|
|
553
527
|
"name": "is_admin",
|
|
@@ -556,7 +530,7 @@ represent more complex logic:
|
|
|
556
530
|
"inverse": false
|
|
557
531
|
},
|
|
558
532
|
{
|
|
559
|
-
"operator": "
|
|
533
|
+
"operator": "all",
|
|
560
534
|
"expressions": [
|
|
561
535
|
{
|
|
562
536
|
"name": "name__istartswith",
|
|
@@ -570,90 +544,27 @@ represent more complex logic:
|
|
|
570
544
|
"kwargs": {"min_age": 18, "max_age": 30},
|
|
571
545
|
"inverse": false
|
|
572
546
|
}
|
|
573
|
-
]
|
|
574
|
-
"inverse": false
|
|
575
|
-
}
|
|
576
|
-
],
|
|
577
|
-
"inverse": false
|
|
578
|
-
}
|
|
579
|
-
```
|
|
580
|
-
|
|
581
|
-
You can also invert a complete wrapper:
|
|
582
|
-
|
|
583
|
-
```json
|
|
584
|
-
{
|
|
585
|
-
"operator": "or",
|
|
586
|
-
"expressions": [
|
|
587
|
-
{
|
|
588
|
-
"name": "is_admin",
|
|
589
|
-
"args": [],
|
|
590
|
-
"kwargs": {},
|
|
591
|
-
"inverse": false
|
|
592
|
-
},
|
|
593
|
-
{
|
|
594
|
-
"name": "age__between",
|
|
595
|
-
"args": [18, 30],
|
|
596
|
-
"kwargs": {},
|
|
597
|
-
"inverse": false
|
|
547
|
+
]
|
|
598
548
|
}
|
|
599
|
-
]
|
|
600
|
-
|
|
601
|
-
}
|
|
602
|
-
```
|
|
603
|
-
|
|
604
|
-
For shorthand forms, validate the payload with `RuleSchema` first. It converts
|
|
605
|
-
them into the normalized predicate and wrapper schemas:
|
|
606
|
-
|
|
607
|
-
```python
|
|
608
|
-
{"is_admin": []}
|
|
609
|
-
{"-is_admin": []}
|
|
610
|
-
{"name__istartswith": "admin"}
|
|
611
|
-
{"age__between": [18, 30]}
|
|
612
|
-
{"name__startswith": {"value": "admin"}}
|
|
549
|
+
]
|
|
550
|
+
} // is_admin() | (name__istartswith("admin") & age__between(min_age=18, max_age=30))
|
|
613
551
|
```
|
|
614
552
|
|
|
615
553
|
The compiler raises `RuleDoesNotExistError` when a predicate name is not in the
|
|
616
554
|
compiler's rule mapping. If the dictionary passed directly to `compile()` does
|
|
617
|
-
not match a predicate or wrapper
|
|
618
|
-
includes the location of the invalid expression, the expected schemas, and the
|
|
619
|
-
received value. Validate external or shorthand payloads with `RuleSchema`
|
|
620
|
-
before compilation.
|
|
555
|
+
not match a predicate or wrapper shape, it raises `TypeError`.
|
|
621
556
|
|
|
622
557
|
---
|
|
623
|
-
|
|
624
|
-
## Rule schema validation
|
|
625
|
-
|
|
626
|
-
`RuleSchema` validates declarative rule payloads.
|
|
627
|
-
|
|
628
|
-
```python
|
|
629
|
-
from pyspecification import RuleSchema
|
|
630
|
-
|
|
631
|
-
rule_data = {
|
|
632
|
-
"operator": "and",
|
|
633
|
-
"expressions": [
|
|
634
|
-
{"name__startswith": "admin"},
|
|
635
|
-
{"age__gt": 18},
|
|
636
|
-
],
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
schema = RuleSchema(**rule_data)
|
|
640
|
-
print(schema.model_dump())
|
|
641
|
-
```
|
|
642
|
-
|
|
643
|
-
This is useful when you want to validate incoming rule definitions before compile-time execution.
|
|
644
|
-
|
|
645
|
-
You can also use `RuleSchema` to represent nested predicate trees as typed, portable data.
|
|
646
|
-
|
|
647
558
|
---
|
|
648
559
|
|
|
649
560
|
## JSON schema generation
|
|
650
561
|
|
|
651
|
-
`
|
|
562
|
+
`get_rule_json_schema` inspects a rule function and returns JSON-schema-like metadata for parameters and return value.
|
|
652
563
|
|
|
653
564
|
```python
|
|
654
565
|
from dataclasses import dataclass
|
|
655
566
|
|
|
656
|
-
from pyspecification import
|
|
567
|
+
from pyspecification import get_rule_json_schema, object_rule
|
|
657
568
|
|
|
658
569
|
|
|
659
570
|
@dataclass
|
|
@@ -668,11 +579,8 @@ def name__istartswith(user: User, value: str) -> bool:
|
|
|
668
579
|
return user.name.lower().startswith(value.lower())
|
|
669
580
|
|
|
670
581
|
|
|
671
|
-
print(
|
|
672
|
-
# {
|
|
673
|
-
# "value": {"type": "string"},
|
|
674
|
-
# "return": {"type": "boolean"},
|
|
675
|
-
# }
|
|
582
|
+
print(get_rule_json_schema(name__istartswith))
|
|
583
|
+
# {'value': {'type': 'string'}, 'return': {'type': 'boolean'}}
|
|
676
584
|
```
|
|
677
585
|
|
|
678
586
|
This is useful for:
|
|
@@ -731,7 +639,7 @@ filtered = [user for user in users if predicate(user)]
|
|
|
731
639
|
This is one of the most useful real-world patterns for the package. You can define a reusable rule set and compile it into SQLAlchemy boolean expressions for database queries.
|
|
732
640
|
|
|
733
641
|
```python
|
|
734
|
-
from pyspecification import ObjectRulesRegistry, Predicate, PredicateCompiler
|
|
642
|
+
from pyspecification import ObjectRulesRegistry, Predicate, PredicateCompiler
|
|
735
643
|
from sqlalchemy import ColumnElement, and_, or_
|
|
736
644
|
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
|
737
645
|
|
|
@@ -765,20 +673,32 @@ def age__ge(model: type[User], value: int) -> ColumnElement[bool]:
|
|
|
765
673
|
compiler = PredicateCompiler(
|
|
766
674
|
rules.rules,
|
|
767
675
|
lambda schema: Predicate(
|
|
768
|
-
lambda _: and_(True) if schema["operator"] == "
|
|
676
|
+
lambda _: and_(True) if schema["operator"] == "all" else or_(False),
|
|
769
677
|
operator="bitwise",
|
|
770
678
|
),
|
|
771
679
|
)
|
|
772
680
|
|
|
773
681
|
filter_rule = {
|
|
774
|
-
"operator": "
|
|
682
|
+
"operator": "any",
|
|
775
683
|
"expressions": [
|
|
776
|
-
{
|
|
777
|
-
|
|
684
|
+
{
|
|
685
|
+
"name": "is_admin",
|
|
686
|
+
"args": [],
|
|
687
|
+
"kwargs": {},
|
|
688
|
+
"inverse": False,
|
|
689
|
+
},
|
|
690
|
+
{
|
|
691
|
+
"name": "age__ge",
|
|
692
|
+
"args": [18],
|
|
693
|
+
"kwargs": {},
|
|
694
|
+
"inverse": False,
|
|
695
|
+
},
|
|
778
696
|
],
|
|
779
697
|
}
|
|
780
698
|
|
|
781
|
-
query_predicate = compiler.compile(
|
|
699
|
+
query_predicate = compiler.compile(filter_rule)
|
|
700
|
+
# is_admin() | age__ge(18)
|
|
701
|
+
# User.is_admin | User.age >= 18
|
|
782
702
|
```
|
|
783
703
|
|
|
784
704
|
This makes it easy to expose admin filters, user search rules, and role-based queries without manually stitching SQL conditions together.
|
|
@@ -795,7 +715,7 @@ This allows readable authorization checks without large condition trees.
|
|
|
795
715
|
|
|
796
716
|
### 3. Dynamic rule engines
|
|
797
717
|
|
|
798
|
-
The compiler
|
|
718
|
+
The compiler makes it easy to store or receive rules as structured data.
|
|
799
719
|
|
|
800
720
|
Examples:
|
|
801
721
|
|
|
@@ -836,10 +756,6 @@ Rules should do one thing and avoid hidden side effects.
|
|
|
836
756
|
|
|
837
757
|
If your project has many rules, registries provide structure and reduce duplication.
|
|
838
758
|
|
|
839
|
-
### Validate schema before compile
|
|
840
|
-
|
|
841
|
-
If you load rules from external sources, validate them via `RuleSchema` before compiling.
|
|
842
|
-
|
|
843
759
|
---
|
|
844
760
|
|
|
845
761
|
## Exceptions
|
|
@@ -870,7 +786,7 @@ rule. Its specialized exceptions describe the problem:
|
|
|
870
786
|
`RuleDoesNotExistError` includes the missing name and the available rule names,
|
|
871
787
|
which is useful when rules are dynamically loaded. Malformed normalized
|
|
872
788
|
compiler payloads raise `TypeError`; its message identifies the JSON-like path
|
|
873
|
-
of the invalid expression and shows the expected
|
|
789
|
+
of the invalid expression and shows the expected shapes.
|
|
874
790
|
|
|
875
791
|
---
|
|
876
792
|
|
|
@@ -880,14 +796,13 @@ This project includes runnable examples under the `scripts/` directory:
|
|
|
880
796
|
|
|
881
797
|
- `scripts/rules_example.py` — basic object-based rule composition
|
|
882
798
|
- `scripts/reg_example.py` — registry usage and compiled rule predicate patterns
|
|
883
|
-
- `scripts/json_schema_example.py` — JSON schema generation examples
|
|
884
799
|
|
|
885
800
|
The test suite under `tests/` also demonstrates behavior for:
|
|
886
801
|
|
|
887
802
|
- registry registration
|
|
888
803
|
- predicate composition
|
|
889
804
|
- compiler validation
|
|
890
|
-
-
|
|
805
|
+
- structured rule compilation
|
|
891
806
|
- filtering workflows
|
|
892
807
|
|
|
893
808
|
---
|
|
@@ -897,7 +812,7 @@ The test suite under `tests/` also demonstrates behavior for:
|
|
|
897
812
|
```python
|
|
898
813
|
from dataclasses import dataclass
|
|
899
814
|
|
|
900
|
-
from pyspecification import ObjectRulesRegistry,
|
|
815
|
+
from pyspecification import ObjectRulesRegistry, Predicate, PredicateCompiler
|
|
901
816
|
|
|
902
817
|
|
|
903
818
|
@dataclass
|
|
@@ -926,21 +841,37 @@ def age__between(user: User, min_age: int, max_age: int) -> bool:
|
|
|
926
841
|
|
|
927
842
|
|
|
928
843
|
rule_definition = {
|
|
929
|
-
"operator": "
|
|
844
|
+
"operator": "any",
|
|
930
845
|
"expressions": [
|
|
931
|
-
{"is_admin": []},
|
|
932
846
|
{
|
|
847
|
+
"name": "is_admin",
|
|
848
|
+
"args": [],
|
|
849
|
+
"kwargs": {},
|
|
850
|
+
"inverse": False,
|
|
851
|
+
},
|
|
852
|
+
{
|
|
853
|
+
"operator": "all",
|
|
933
854
|
"expressions": [
|
|
934
|
-
{
|
|
935
|
-
|
|
855
|
+
{
|
|
856
|
+
"name": "name__istartswith",
|
|
857
|
+
"args": ["admin"],
|
|
858
|
+
"kwargs": {},
|
|
859
|
+
"inverse": False,
|
|
860
|
+
},
|
|
861
|
+
{
|
|
862
|
+
"name": "age__between",
|
|
863
|
+
"args": [18, 30],
|
|
864
|
+
"kwargs": {},
|
|
865
|
+
"inverse": False,
|
|
866
|
+
},
|
|
936
867
|
]
|
|
937
868
|
},
|
|
938
869
|
],
|
|
939
870
|
}
|
|
940
871
|
|
|
941
|
-
schema = RuleSchema(**rule_definition)
|
|
942
872
|
compiler = PredicateCompiler(registry.rules, lambda spec: Predicate(lambda _: True, operator="logical"))
|
|
943
|
-
predicate = compiler.compile(
|
|
873
|
+
predicate = compiler.compile(rule_definition)
|
|
874
|
+
# is_admin() | (name__istartswith("admin") & age__between(18, 30))
|
|
944
875
|
|
|
945
876
|
users = [
|
|
946
877
|
User("Abdullah", 18, True),
|
|
@@ -948,20 +879,20 @@ users = [
|
|
|
948
879
|
User("Charlie", 12, False),
|
|
949
880
|
]
|
|
950
881
|
|
|
951
|
-
print([predicate(user) for user in users])
|
|
882
|
+
print([predicate(user) for user in users]) # [True, True, False]
|
|
952
883
|
```
|
|
953
884
|
|
|
954
885
|
---
|
|
955
886
|
|
|
956
887
|
## Summary
|
|
957
888
|
|
|
958
|
-
`pyspecification` brings together rule registration, predicate composition,
|
|
889
|
+
`pyspecification` brings together rule registration, predicate composition, and runtime compilation in a compact library designed around functional and declarative rule authoring.
|
|
959
890
|
|
|
960
891
|
It is a practical fit for projects that need to:
|
|
961
892
|
|
|
962
893
|
- express business rules clearly
|
|
963
894
|
- compose conditions without nested `if` chains
|
|
964
|
-
-
|
|
895
|
+
- compile dynamic rule payloads
|
|
965
896
|
- support filtering and policy evaluation
|
|
966
897
|
- keep rule logic easy to test and maintain
|
|
967
898
|
|