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.
@@ -1,13 +1,12 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pyspecification
3
- Version: 0.1.4
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
- - generated schemas make rule metadata portable and machine-readable
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, RuleSchema
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"] == "and" else or_(False),
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": "or",
320
+ "operator": "any",
323
321
  "expressions": [
324
- {"-is_admin": []},
325
- {"age__ge": [18]},
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(RuleSchema(**filter_rule_data).model_dump())
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, RuleSchema, object_rule
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"] == "and", operator="logical"),
443
+ lambda schema: Predicate(lambda _: schema["operator"] == "all", operator="logical"),
434
444
  )
435
445
 
436
446
  rule_data = {
437
- "operator": "or",
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
- {"name__istartswith": "admin"},
443
- {"age__between": [18, 30]},
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(RuleSchema(**rule_data).model_dump())
450
- print(predicate(User("admin", 25, True))) # True
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
- ```json
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
- The normalized schema is convenient when rule definitions arrive as JSON:
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
- ### Expression wrapper schema
493
+ print(repr(predicate)) # Predicate(age__between)
494
+ ```
520
495
 
521
- Use a wrapper to combine predicates. A wrapper has an `operator`, an
522
- `expressions` list, and an `inverse` flag:
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": "and",
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
- "inverse": false
542
- }
515
+ ]
516
+ } // is_admin() & age__between(18, 30)
543
517
  ```
544
518
 
545
- `operator` must be either `"and"` or `"or"`. Expressions can be nested to
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": "or",
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": "and",
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
- "inverse": true
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 schema, it raises `TypeError`. The error
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
- `get_json_schema` inspects a rule function and returns JSON-schema-like metadata for parameters and return value.
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 get_json_schema, object_rule
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(get_json_schema(name__istartswith))
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, RuleSchema
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"] == "and" else or_(False),
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": "or",
682
+ "operator": "any",
775
683
  "expressions": [
776
- {"is_admin": []},
777
- {"age__ge": [18]},
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(RuleSchema(**filter_rule).model_dump())
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 and schema APIs make it easy to store or receive rules as structured data.
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 schemas.
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
- - schema serialization
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, PredicateCompiler, RuleSchema
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": "or",
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
- {"name__istartswith": "admin"},
935
- {"age__between": [18, 30]},
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(schema.model_dump())
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, schema validation, and runtime compilation in a compact library designed around functional and declarative rule authoring.
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
- - validate dynamic rule payloads
895
+ - compile dynamic rule payloads
965
896
  - support filtering and policy evaluation
966
897
  - keep rule logic easy to test and maintain
967
898