pyspecification 2.0.2__tar.gz → 3.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,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pyspecification
3
- Version: 2.0.2
3
+ Version: 3.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>
@@ -65,9 +65,9 @@ This keeps your logic:
65
65
  - Subscriptable rules for dictionaries, lists, and generic lookup-based data
66
66
  - `Predicate` objects that support logical composition
67
67
  - Registry pattern for rule registration and lookup
68
- - Custom argument processors for coercion and normalization
68
+ - Custom argument processors for coercion and normalization, validated when a rule is registered
69
69
  - `PredicateCompiler` for compiling structured rule dictionaries into executable predicates
70
- - JSON schema generation for rule arguments and return types
70
+ - JSON schema generation for single rules and for whole compilable expressions
71
71
  - Support for both logical and bitwise operator modes
72
72
  - Hidden rules and custom naming for internal/private rule registration
73
73
 
@@ -490,7 +490,10 @@ This pattern is especially useful when you want:
490
490
 
491
491
  ## Custom processors
492
492
 
493
- Rules can apply argument processors to coerce values before evaluation.
493
+ Rules can apply argument processors to coerce values before evaluation. This is
494
+ most useful when rules come from JSON, where every value is a string, number,
495
+ boolean, list, dict or null, and your rule functions expect richer types such as
496
+ dates or decimals.
494
497
 
495
498
  ```python
496
499
  from datetime import datetime
@@ -501,7 +504,7 @@ from pyspecification import SubscriptableRulesRegistry
501
504
  rules = SubscriptableRulesRegistry[dict[str, object], str, bool](operator="logical")
502
505
 
503
506
 
504
- @rules.rule(processors=(lambda value: datetime.strptime(value, "%Y-%m-%d"), {}))
507
+ @rules.rule(processors={"value": lambda value: datetime.strptime(value, "%Y-%m-%d")})
505
508
  def datetime__gt(obj: dict[str, object], key: str, value: datetime) -> bool:
506
509
  return obj[key] > value
507
510
 
@@ -510,27 +513,45 @@ predicate = rules["datetime__gt"]("birthdate", "2001-06-01")
510
513
  print(predicate({"birthdate": datetime(2005, 1, 1)})) # True
511
514
  ```
512
515
 
513
- The processor tuple format is:
516
+ The `processors` option accepts:
514
517
 
515
- ```python
516
- (default_processor, named_processors_map)
517
- ```
518
+ - `None` (the default): arguments are passed through untouched.
519
+ - a callable: applied to every argument, e.g. `processors=int`.
520
+ - a mapping of parameter name to callable: applied to those arguments only,
521
+ e.g. `processors={"min_age": int}`. The key `...` (Ellipsis) means "every
522
+ argument not named here", e.g. `processors={"min_age": int, ...: str.strip}`.
523
+
524
+ Processors are matched by parameter name, so they apply whether a value was
525
+ passed positionally or by keyword. For `*args` the processor runs on each item,
526
+ and for `**kwargs` each extra keyword name is looked up individually. Default
527
+ values you did not pass are left as they are.
528
+
529
+ The object under test and, for subscriptable rules, the `key` are not
530
+ arguments: they are never processed and cannot be named in the mapping.
518
531
 
519
- For example:
532
+ Processors are validated when the rule is registered, so a typo fails
533
+ immediately instead of silently never running:
520
534
 
521
535
  ```python
522
- processors = (
523
- int,
524
- {"value": str},
525
- )
536
+ @rules.rule(processors={"valu": str}) # InvalidProcessorsError: unknown parameters ['valu']
537
+ def string__eq(obj: dict[str, object], key: str, value: str) -> bool: ...
526
538
  ```
527
539
 
528
- This means:
540
+ `InvalidProcessorsError` is raised when the option is not a callable or a
541
+ mapping, when a mapping value is not callable, or when a key names no parameter
542
+ of the rule (rules that accept `**kwargs` allow any key).
543
+
544
+ If a processor raises, the library raises `ProcessArgumentError` naming the
545
+ parameter and value, with the original exception chained:
529
546
 
530
- - positional args are passed through `int`
531
- - keyword args with name `"value"` are passed through `str`
547
+ ```text
548
+ Argument 'value' with value '06-01-2001' failed to process, time data '06-01-2001' does not match format '%Y-%m-%d'
549
+ ```
532
550
 
533
- If conversion fails, the library raises `ProcessArgumentError`.
551
+ > **Upgrading from 2.x:** processors used to be a `(default_processor,
552
+ > named_processors_map)` tuple. Pass a callable instead of the tuple for the
553
+ > old "process everything" behavior, or a mapping for named parameters. The
554
+ > old tuple form is now rejected with `InvalidProcessorsError`.
534
555
 
535
556
  ---
536
557
 
@@ -693,7 +714,17 @@ not match a predicate or wrapper shape, it raises `TypeError`.
693
714
 
694
715
  ## JSON schema generation
695
716
 
696
- `get_rule_json_schema` inspects a rule function and returns JSON-schema-like metadata for parameters and return value.
717
+ The generated schemas describe the exact shape accepted by `PredicateCompiler`,
718
+ so they can be handed to a form builder, a validator, or an LLM that writes
719
+ rules as JSON.
720
+
721
+ ### One rule
722
+
723
+ `get_rule_json_schema(name, rule)` returns the schema of one predicate
724
+ dictionary. Positional parameters are described under `args` (via
725
+ `prefixItems`), keyword-capable parameters under `kwargs`, and the rule's
726
+ docstring becomes the description. The object under test is not part of the
727
+ schema; for subscriptable rules the `key` is the first parameter.
697
728
 
698
729
  ```python
699
730
  from dataclasses import dataclass
@@ -709,19 +740,94 @@ class User:
709
740
 
710
741
 
711
742
  @object_rule()
712
- def name__istartswith(user: User, value: str) -> bool:
713
- return user.name.lower().startswith(value.lower())
743
+ def age__between(user: User, min_age: int, max_age: int = 120) -> bool:
744
+ """Whether the age is within a range."""
745
+ return min_age <= user.age <= max_age
746
+
747
+
748
+ print(get_rule_json_schema("age__between", age__between))
749
+ # {
750
+ # "type": "object",
751
+ # "properties": {
752
+ # "name": {"const": "age__between"},
753
+ # "args": {
754
+ # "type": "array",
755
+ # "prefixItems": [{"type": "integer"}, {"type": "integer", "default": 120}],
756
+ # "items": False,
757
+ # },
758
+ # "kwargs": {
759
+ # "type": "object",
760
+ # "properties": {
761
+ # "min_age": {"type": "integer"},
762
+ # "max_age": {"type": "integer", "default": 120},
763
+ # },
764
+ # "required": [],
765
+ # "additionalProperties": False,
766
+ # },
767
+ # "inverse": {"type": "boolean"},
768
+ # },
769
+ # "required": ["name", "args", "kwargs", "inverse"],
770
+ # "additionalProperties": False,
771
+ # "description": "Whether the age is within a range.",
772
+ # }
773
+ ```
774
+
775
+ ### A whole expression
714
776
 
777
+ `get_expression_json_schema(rules)` returns the schema of everything
778
+ `PredicateCompiler.compile` accepts: a predicate dictionary for each rule, or a
779
+ nested `{"operator": "all" | "any", "expressions": [...]}` wrapper. Pass
780
+ `registry.rules` (hidden rules are left out) or any name-to-rule mapping you
781
+ give to the compiler.
715
782
 
716
- print(get_rule_json_schema(name__istartswith))
717
- # {'value': {'type': 'string'}, 'return': {'type': 'boolean'}}
783
+ ```python
784
+ from pyspecification import ObjectRulesRegistry, get_expression_json_schema
785
+
786
+ registry = ObjectRulesRegistry[User, bool](operator="logical")
787
+
788
+
789
+ @registry.rule()
790
+ def is_admin(user: User) -> bool:
791
+ return user.is_admin
792
+
793
+
794
+ schema = get_expression_json_schema(registry.rules)
795
+ print(schema["$schema"]) # https://json-schema.org/draft/2020-12/schema
718
796
  ```
719
797
 
798
+ ### Supported types
799
+
800
+ `get_json_schema(annotation)` maps Python annotations to JSON Schema. Unknown or
801
+ unannotated types map to `{}`, which accepts any value.
802
+
803
+ | Python annotation | JSON Schema |
804
+ | --- | --- |
805
+ | `str`, `int`, `float`, `bool`, `None` | `string`, `integer`, `number`, `boolean`, `null` |
806
+ | `Decimal` | `number` |
807
+ | `datetime`, `date`, `time`, `UUID` | `string` with `date-time`, `date`, `time`, `uuid` format |
808
+ | `list[T]`, `set[T]`, `frozenset[T]`, `tuple[T, ...]` | `array` of `T` |
809
+ | `dict[K, V]` | `object` with `V` as `additionalProperties` |
810
+ | `Literal[...]`, `Enum` subclasses | `enum` of the values (plus `string` type when all are strings) |
811
+ | `A \| B`, `Optional[A]` | `anyOf` |
812
+ | `TypedDict` | `object` with `properties` and `required` (`Required`/`NotRequired` honored) |
813
+ | `Annotated[T, ...]`, `type` aliases | the schema of `T` / the aliased type |
814
+
815
+ Fixed-size heterogeneous tuples such as `tuple[int, str]` are described by their
816
+ first member only. String annotations are evaluated when possible, and fall back
817
+ to `{}` when they cannot be resolved. Defaults are included when they are plain
818
+ JSON values.
819
+
820
+ > **Upgrading from 2.x:** `get_rule_json_schema(rule)` used to return a flat
821
+ > `{parameter: schema, "return": schema}` mapping. It now takes the rule name
822
+ > too and returns the predicate dictionary schema shown above. `get_json_schema`
823
+ > is now exported from the package root.
824
+
720
825
  This is useful for:
721
826
 
722
827
  - generating UIs for rule configuration
723
828
  - building admin tools and dashboards
724
- - describing rule inputs to other systems
829
+ - validating rule payloads before compiling them
830
+ - describing rule inputs to other systems and LLMs
725
831
  - documenting business rules programmatically
726
832
 
727
833
  ---
@@ -904,6 +1010,7 @@ The library raises explicit exceptions for rule issues:
904
1010
  - `UnexpectedKeywordArgumentError`
905
1011
  - `TooManyArgumentsError`
906
1012
  - `ProcessArgumentError`
1013
+ - `InvalidProcessorsError`
907
1014
 
908
1015
  `ArgumentError` is the base class for failures involving arguments passed to a
909
1016
  rule. Its specialized exceptions describe the problem:
@@ -917,6 +1024,9 @@ rule. Its specialized exceptions describe the problem:
917
1024
  - `ProcessArgumentError` means an argument processor could not convert or
918
1025
  otherwise process a value.
919
1026
 
1027
+ `InvalidProcessorsError` (a `TypeError`) is raised at registration when the
1028
+ `processors` option is malformed or names a parameter the rule does not have.
1029
+
920
1030
  `RuleDoesNotExistError` includes the missing name and the available rule names,
921
1031
  which is useful when rules are dynamically loaded. Malformed normalized
922
1032
  compiler payloads raise `TypeError`; its message identifies the JSON-like path
@@ -51,9 +51,9 @@ This keeps your logic:
51
51
  - Subscriptable rules for dictionaries, lists, and generic lookup-based data
52
52
  - `Predicate` objects that support logical composition
53
53
  - Registry pattern for rule registration and lookup
54
- - Custom argument processors for coercion and normalization
54
+ - Custom argument processors for coercion and normalization, validated when a rule is registered
55
55
  - `PredicateCompiler` for compiling structured rule dictionaries into executable predicates
56
- - JSON schema generation for rule arguments and return types
56
+ - JSON schema generation for single rules and for whole compilable expressions
57
57
  - Support for both logical and bitwise operator modes
58
58
  - Hidden rules and custom naming for internal/private rule registration
59
59
 
@@ -476,7 +476,10 @@ This pattern is especially useful when you want:
476
476
 
477
477
  ## Custom processors
478
478
 
479
- Rules can apply argument processors to coerce values before evaluation.
479
+ Rules can apply argument processors to coerce values before evaluation. This is
480
+ most useful when rules come from JSON, where every value is a string, number,
481
+ boolean, list, dict or null, and your rule functions expect richer types such as
482
+ dates or decimals.
480
483
 
481
484
  ```python
482
485
  from datetime import datetime
@@ -487,7 +490,7 @@ from pyspecification import SubscriptableRulesRegistry
487
490
  rules = SubscriptableRulesRegistry[dict[str, object], str, bool](operator="logical")
488
491
 
489
492
 
490
- @rules.rule(processors=(lambda value: datetime.strptime(value, "%Y-%m-%d"), {}))
493
+ @rules.rule(processors={"value": lambda value: datetime.strptime(value, "%Y-%m-%d")})
491
494
  def datetime__gt(obj: dict[str, object], key: str, value: datetime) -> bool:
492
495
  return obj[key] > value
493
496
 
@@ -496,27 +499,45 @@ predicate = rules["datetime__gt"]("birthdate", "2001-06-01")
496
499
  print(predicate({"birthdate": datetime(2005, 1, 1)})) # True
497
500
  ```
498
501
 
499
- The processor tuple format is:
502
+ The `processors` option accepts:
500
503
 
501
- ```python
502
- (default_processor, named_processors_map)
503
- ```
504
+ - `None` (the default): arguments are passed through untouched.
505
+ - a callable: applied to every argument, e.g. `processors=int`.
506
+ - a mapping of parameter name to callable: applied to those arguments only,
507
+ e.g. `processors={"min_age": int}`. The key `...` (Ellipsis) means "every
508
+ argument not named here", e.g. `processors={"min_age": int, ...: str.strip}`.
509
+
510
+ Processors are matched by parameter name, so they apply whether a value was
511
+ passed positionally or by keyword. For `*args` the processor runs on each item,
512
+ and for `**kwargs` each extra keyword name is looked up individually. Default
513
+ values you did not pass are left as they are.
514
+
515
+ The object under test and, for subscriptable rules, the `key` are not
516
+ arguments: they are never processed and cannot be named in the mapping.
504
517
 
505
- For example:
518
+ Processors are validated when the rule is registered, so a typo fails
519
+ immediately instead of silently never running:
506
520
 
507
521
  ```python
508
- processors = (
509
- int,
510
- {"value": str},
511
- )
522
+ @rules.rule(processors={"valu": str}) # InvalidProcessorsError: unknown parameters ['valu']
523
+ def string__eq(obj: dict[str, object], key: str, value: str) -> bool: ...
512
524
  ```
513
525
 
514
- This means:
526
+ `InvalidProcessorsError` is raised when the option is not a callable or a
527
+ mapping, when a mapping value is not callable, or when a key names no parameter
528
+ of the rule (rules that accept `**kwargs` allow any key).
529
+
530
+ If a processor raises, the library raises `ProcessArgumentError` naming the
531
+ parameter and value, with the original exception chained:
515
532
 
516
- - positional args are passed through `int`
517
- - keyword args with name `"value"` are passed through `str`
533
+ ```text
534
+ Argument 'value' with value '06-01-2001' failed to process, time data '06-01-2001' does not match format '%Y-%m-%d'
535
+ ```
518
536
 
519
- If conversion fails, the library raises `ProcessArgumentError`.
537
+ > **Upgrading from 2.x:** processors used to be a `(default_processor,
538
+ > named_processors_map)` tuple. Pass a callable instead of the tuple for the
539
+ > old "process everything" behavior, or a mapping for named parameters. The
540
+ > old tuple form is now rejected with `InvalidProcessorsError`.
520
541
 
521
542
  ---
522
543
 
@@ -679,7 +700,17 @@ not match a predicate or wrapper shape, it raises `TypeError`.
679
700
 
680
701
  ## JSON schema generation
681
702
 
682
- `get_rule_json_schema` inspects a rule function and returns JSON-schema-like metadata for parameters and return value.
703
+ The generated schemas describe the exact shape accepted by `PredicateCompiler`,
704
+ so they can be handed to a form builder, a validator, or an LLM that writes
705
+ rules as JSON.
706
+
707
+ ### One rule
708
+
709
+ `get_rule_json_schema(name, rule)` returns the schema of one predicate
710
+ dictionary. Positional parameters are described under `args` (via
711
+ `prefixItems`), keyword-capable parameters under `kwargs`, and the rule's
712
+ docstring becomes the description. The object under test is not part of the
713
+ schema; for subscriptable rules the `key` is the first parameter.
683
714
 
684
715
  ```python
685
716
  from dataclasses import dataclass
@@ -695,19 +726,94 @@ class User:
695
726
 
696
727
 
697
728
  @object_rule()
698
- def name__istartswith(user: User, value: str) -> bool:
699
- return user.name.lower().startswith(value.lower())
729
+ def age__between(user: User, min_age: int, max_age: int = 120) -> bool:
730
+ """Whether the age is within a range."""
731
+ return min_age <= user.age <= max_age
732
+
733
+
734
+ print(get_rule_json_schema("age__between", age__between))
735
+ # {
736
+ # "type": "object",
737
+ # "properties": {
738
+ # "name": {"const": "age__between"},
739
+ # "args": {
740
+ # "type": "array",
741
+ # "prefixItems": [{"type": "integer"}, {"type": "integer", "default": 120}],
742
+ # "items": False,
743
+ # },
744
+ # "kwargs": {
745
+ # "type": "object",
746
+ # "properties": {
747
+ # "min_age": {"type": "integer"},
748
+ # "max_age": {"type": "integer", "default": 120},
749
+ # },
750
+ # "required": [],
751
+ # "additionalProperties": False,
752
+ # },
753
+ # "inverse": {"type": "boolean"},
754
+ # },
755
+ # "required": ["name", "args", "kwargs", "inverse"],
756
+ # "additionalProperties": False,
757
+ # "description": "Whether the age is within a range.",
758
+ # }
759
+ ```
760
+
761
+ ### A whole expression
700
762
 
763
+ `get_expression_json_schema(rules)` returns the schema of everything
764
+ `PredicateCompiler.compile` accepts: a predicate dictionary for each rule, or a
765
+ nested `{"operator": "all" | "any", "expressions": [...]}` wrapper. Pass
766
+ `registry.rules` (hidden rules are left out) or any name-to-rule mapping you
767
+ give to the compiler.
701
768
 
702
- print(get_rule_json_schema(name__istartswith))
703
- # {'value': {'type': 'string'}, 'return': {'type': 'boolean'}}
769
+ ```python
770
+ from pyspecification import ObjectRulesRegistry, get_expression_json_schema
771
+
772
+ registry = ObjectRulesRegistry[User, bool](operator="logical")
773
+
774
+
775
+ @registry.rule()
776
+ def is_admin(user: User) -> bool:
777
+ return user.is_admin
778
+
779
+
780
+ schema = get_expression_json_schema(registry.rules)
781
+ print(schema["$schema"]) # https://json-schema.org/draft/2020-12/schema
704
782
  ```
705
783
 
784
+ ### Supported types
785
+
786
+ `get_json_schema(annotation)` maps Python annotations to JSON Schema. Unknown or
787
+ unannotated types map to `{}`, which accepts any value.
788
+
789
+ | Python annotation | JSON Schema |
790
+ | --- | --- |
791
+ | `str`, `int`, `float`, `bool`, `None` | `string`, `integer`, `number`, `boolean`, `null` |
792
+ | `Decimal` | `number` |
793
+ | `datetime`, `date`, `time`, `UUID` | `string` with `date-time`, `date`, `time`, `uuid` format |
794
+ | `list[T]`, `set[T]`, `frozenset[T]`, `tuple[T, ...]` | `array` of `T` |
795
+ | `dict[K, V]` | `object` with `V` as `additionalProperties` |
796
+ | `Literal[...]`, `Enum` subclasses | `enum` of the values (plus `string` type when all are strings) |
797
+ | `A \| B`, `Optional[A]` | `anyOf` |
798
+ | `TypedDict` | `object` with `properties` and `required` (`Required`/`NotRequired` honored) |
799
+ | `Annotated[T, ...]`, `type` aliases | the schema of `T` / the aliased type |
800
+
801
+ Fixed-size heterogeneous tuples such as `tuple[int, str]` are described by their
802
+ first member only. String annotations are evaluated when possible, and fall back
803
+ to `{}` when they cannot be resolved. Defaults are included when they are plain
804
+ JSON values.
805
+
806
+ > **Upgrading from 2.x:** `get_rule_json_schema(rule)` used to return a flat
807
+ > `{parameter: schema, "return": schema}` mapping. It now takes the rule name
808
+ > too and returns the predicate dictionary schema shown above. `get_json_schema`
809
+ > is now exported from the package root.
810
+
706
811
  This is useful for:
707
812
 
708
813
  - generating UIs for rule configuration
709
814
  - building admin tools and dashboards
710
- - describing rule inputs to other systems
815
+ - validating rule payloads before compiling them
816
+ - describing rule inputs to other systems and LLMs
711
817
  - documenting business rules programmatically
712
818
 
713
819
  ---
@@ -890,6 +996,7 @@ The library raises explicit exceptions for rule issues:
890
996
  - `UnexpectedKeywordArgumentError`
891
997
  - `TooManyArgumentsError`
892
998
  - `ProcessArgumentError`
999
+ - `InvalidProcessorsError`
893
1000
 
894
1001
  `ArgumentError` is the base class for failures involving arguments passed to a
895
1002
  rule. Its specialized exceptions describe the problem:
@@ -903,6 +1010,9 @@ rule. Its specialized exceptions describe the problem:
903
1010
  - `ProcessArgumentError` means an argument processor could not convert or
904
1011
  otherwise process a value.
905
1012
 
1013
+ `InvalidProcessorsError` (a `TypeError`) is raised at registration when the
1014
+ `processors` option is malformed or names a parameter the rule does not have.
1015
+
906
1016
  `RuleDoesNotExistError` includes the missing name and the available rule names,
907
1017
  which is useful when rules are dynamically loaded. Malformed normalized
908
1018
  compiler payloads raise `TypeError`; its message identifies the JSON-like path
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pyspecification"
3
- version = "2.0.2"
3
+ version = "3.0.0"
4
4
  description = "A lightweight, typed Python library for composing business rules as reusable, executable predicates."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pyspecification"
3
- version = "2.0.2"
3
+ version = "3.0.0"
4
4
  description = "A lightweight, typed Python library for composing business rules as reusable, executable predicates."
5
5
  readme = "README.md"
6
6
  authors = [{ name = "aaltatan", email = "a.altatan@gmail.com" }]
@@ -7,6 +7,7 @@ This project was inspired by the work and ideas shared by [ArjanCodes](https://g
7
7
  from .compilers import ExpressionWrapperDict, PredicateCompiler, PredicateDict
8
8
  from .exceptions import (
9
9
  ArgumentError,
10
+ InvalidProcessorsError,
10
11
  MissingArgumentError,
11
12
  PositionalOnlyArgumentError,
12
13
  ProcessArgumentError,
@@ -16,7 +17,7 @@ from .exceptions import (
16
17
  TooManyArgumentsError,
17
18
  UnexpectedKeywordArgumentError,
18
19
  )
19
- from .json_schema import get_rule_json_schema
20
+ from .json_schema import get_expression_json_schema, get_json_schema, get_rule_json_schema
20
21
  from .predicate import OperatorType, Predicate
21
22
  from .registry import ObjectRulesRegistry, SubscriptableRulesRegistry
22
23
  from .rules import object_rule, subscriptable_rule
@@ -24,6 +25,7 @@ from .rules import object_rule, subscriptable_rule
24
25
  __all__ = [
25
26
  "ArgumentError",
26
27
  "ExpressionWrapperDict",
28
+ "InvalidProcessorsError",
27
29
  "MissingArgumentError",
28
30
  "ObjectRulesRegistry",
29
31
  "OperatorType",
@@ -38,6 +40,8 @@ __all__ = [
38
40
  "SubscriptableRulesRegistry",
39
41
  "TooManyArgumentsError",
40
42
  "UnexpectedKeywordArgumentError",
43
+ "get_expression_json_schema",
44
+ "get_json_schema",
41
45
  "get_rule_json_schema",
42
46
  "object_rule",
43
47
  "subscriptable_rule",
@@ -2,7 +2,11 @@ import re
2
2
  from collections.abc import Iterable
3
3
 
4
4
 
5
- class RuleDoesNotExistError(Exception):
5
+ class PySpecificationError(Exception):
6
+ """Base class for all PySpecification exceptions."""
7
+
8
+
9
+ class RuleDoesNotExistError(PySpecificationError):
6
10
  """Exception raised when a rule does not exist."""
7
11
 
8
12
  def __init__(self, name: str, available_rules: Iterable[str] | None = None) -> None:
@@ -14,21 +18,21 @@ class RuleDoesNotExistError(Exception):
14
18
  super().__init__(msg)
15
19
 
16
20
 
17
- class RuleAlreadyRegisteredError(Exception):
21
+ class RuleAlreadyRegisteredError(PySpecificationError):
18
22
  """Exception raised when a rule is already registered in registry class."""
19
23
 
20
24
  def __init__(self, name: str) -> None:
21
25
  super().__init__(f"Rule '{name}' is already registered")
22
26
 
23
27
 
24
- class RuleKeyDoesNotExistError(Exception):
28
+ class RuleKeyDoesNotExistError(PySpecificationError):
25
29
  """Exception raised when a key does not exist in the object of rule."""
26
30
 
27
31
  def __init__(self, key: str, rule_name: str) -> None:
28
32
  super().__init__(f"Key '{key}' does not exist in the object of rule '{rule_name}'")
29
33
 
30
34
 
31
- class ArgumentError(TypeError):
35
+ class ArgumentError(PySpecificationError, TypeError):
32
36
  pass
33
37
 
34
38
 
@@ -72,3 +76,7 @@ def is_positional_only_argument_exception(e: TypeError) -> bool:
72
76
 
73
77
  class ProcessArgumentError(ArgumentError):
74
78
  """Exception raised when a process argument fails in registry class."""
79
+
80
+
81
+ class InvalidProcessorsError(PySpecificationError, TypeError):
82
+ """Exception raised when the `processors` option of a rule is malformed."""