pyspecification 0.1.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.
- pyspecification/__init__.py +32 -0
- pyspecification/compilers.py +177 -0
- pyspecification/constants.py +1 -0
- pyspecification/exceptions.py +27 -0
- pyspecification/json_schema.py +64 -0
- pyspecification/predicate.py +142 -0
- pyspecification/processors.py +43 -0
- pyspecification/registry.py +430 -0
- pyspecification/rules.py +166 -0
- pyspecification/schemas.py +135 -0
- pyspecification/validators.py +14 -0
- pyspecification-0.1.0.dist-info/METADATA +788 -0
- pyspecification-0.1.0.dist-info/RECORD +15 -0
- pyspecification-0.1.0.dist-info/WHEEL +4 -0
- pyspecification-0.1.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from .compilers import ExpressionWrapperDict, PredicateCompiler, PredicateDict
|
|
2
|
+
from .exceptions import (
|
|
3
|
+
CompilationError,
|
|
4
|
+
ProcessArgumentError,
|
|
5
|
+
RuleAlreadyRegisteredError,
|
|
6
|
+
RuleKeyDoesNotExistError,
|
|
7
|
+
RuleNotRegisteredError,
|
|
8
|
+
)
|
|
9
|
+
from .json_schema import get_json_schema
|
|
10
|
+
from .predicate import OperatorType, Predicate
|
|
11
|
+
from .registry import ObjectRulesRegistry, SubscriptableRulesRegistry
|
|
12
|
+
from .rules import object_rule, subscriptable_rule
|
|
13
|
+
from .schemas import RuleSchema
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"CompilationError",
|
|
17
|
+
"ExpressionWrapperDict",
|
|
18
|
+
"ObjectRulesRegistry",
|
|
19
|
+
"OperatorType",
|
|
20
|
+
"Predicate",
|
|
21
|
+
"PredicateCompiler",
|
|
22
|
+
"PredicateDict",
|
|
23
|
+
"ProcessArgumentError",
|
|
24
|
+
"RuleAlreadyRegisteredError",
|
|
25
|
+
"RuleKeyDoesNotExistError",
|
|
26
|
+
"RuleNotRegisteredError",
|
|
27
|
+
"RuleSchema",
|
|
28
|
+
"SubscriptableRulesRegistry",
|
|
29
|
+
"get_json_schema",
|
|
30
|
+
"object_rule",
|
|
31
|
+
"subscriptable_rule",
|
|
32
|
+
]
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from typing import Any, Literal, TypedDict, TypeGuard
|
|
3
|
+
|
|
4
|
+
from .exceptions import CompilationError
|
|
5
|
+
from .predicate import Predicate, ReturnType
|
|
6
|
+
|
|
7
|
+
type ExpressionDict = ExpressionWrapperDict | PredicateDict
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
PREDICATE_DICT_KEYS = {"name", "args", "kwargs", "inverse"}
|
|
11
|
+
EXPRESSION_WRAPPER_DICT_KEYS = {"operator", "expressions", "inverse"}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PredicateDict(TypedDict):
|
|
15
|
+
"""A dictionary representation of a predicate."""
|
|
16
|
+
|
|
17
|
+
name: str
|
|
18
|
+
args: list[Any]
|
|
19
|
+
kwargs: dict[str, Any]
|
|
20
|
+
inverse: bool
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ExpressionWrapperDict(TypedDict):
|
|
24
|
+
"""A dictionary representation of an expression."""
|
|
25
|
+
|
|
26
|
+
operator: Literal["and", "or"]
|
|
27
|
+
expressions: list["ExpressionWrapperDict | PredicateDict"]
|
|
28
|
+
inverse: bool
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def is_predicate_dict(d: ExpressionDict | dict[str, Any]) -> TypeGuard[PredicateDict]:
|
|
32
|
+
return all(key in d for key in PREDICATE_DICT_KEYS) and len(d) == len(PREDICATE_DICT_KEYS)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def is_expression_wrapper_dict(
|
|
36
|
+
d: ExpressionDict | dict[str, Any],
|
|
37
|
+
) -> TypeGuard[ExpressionWrapperDict]:
|
|
38
|
+
return all(key in d for key in EXPRESSION_WRAPPER_DICT_KEYS) and len(d) == len(
|
|
39
|
+
EXPRESSION_WRAPPER_DICT_KEYS
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class PredicateCompiler[T, R: ReturnType]:
|
|
44
|
+
"""A compiler for predicates.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
rules (dict[str, Callable[..., Predicate[Any, Any]]]): The rules to use for compiling.
|
|
48
|
+
initial_predicate_factory (Callable[[ExpressionWrapperDict], Predicate[Any, Any]]): The factory to use for creating initial predicates.
|
|
49
|
+
|
|
50
|
+
Example:
|
|
51
|
+
```python
|
|
52
|
+
from pyspecification import Predicate, PredicateCompiler, object_rule
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@object_rule
|
|
56
|
+
def is_admin(user: User) -> bool:
|
|
57
|
+
return user.is_admin
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@object_rule
|
|
61
|
+
def name__istartswith(user: User, value: str) -> bool:
|
|
62
|
+
return user.name.lower().startswith(value.lower())
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@object_rule
|
|
66
|
+
def age__between(user: User, min_age: int, max_age: int) -> bool:
|
|
67
|
+
return user.age >= min_age and user.age <= max_age
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def main() -> None:
|
|
71
|
+
rules = {
|
|
72
|
+
"is_admin": is_admin,
|
|
73
|
+
"name__istartswith": name__istartswith,
|
|
74
|
+
"age__between": age__between,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
compiler = PredicateCompiler(
|
|
78
|
+
rules,
|
|
79
|
+
lambda schema: Predicate(lambda _: schema["operator"] == "and"),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
rule_data = {
|
|
83
|
+
"operator": "or",
|
|
84
|
+
"inverse": False,
|
|
85
|
+
"expressions": [
|
|
86
|
+
{
|
|
87
|
+
"name": "is_admin",
|
|
88
|
+
"inverse": False,
|
|
89
|
+
"args": [],
|
|
90
|
+
"kwargs": {},
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"operator": "and",
|
|
94
|
+
"inverse": False,
|
|
95
|
+
"expressions": [
|
|
96
|
+
{
|
|
97
|
+
"name": "name__istartswith",
|
|
98
|
+
"inverse": False,
|
|
99
|
+
"args": ["admin"],
|
|
100
|
+
"kwargs": {},
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"name": "age__between",
|
|
104
|
+
"inverse": False,
|
|
105
|
+
"args": [18, 30],
|
|
106
|
+
"kwargs": {},
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
predicate = compiler.compile(rule_data)
|
|
114
|
+
|
|
115
|
+
assert all(predicate(user) for user in users)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
if __name__ == "__main__":
|
|
119
|
+
main()
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
""" # noqa: E501
|
|
123
|
+
|
|
124
|
+
def __init__(
|
|
125
|
+
self,
|
|
126
|
+
rules: dict[str, Callable[..., Predicate[T, R]]],
|
|
127
|
+
initial_predicate_factory: Callable[[ExpressionWrapperDict], Predicate[T, R]],
|
|
128
|
+
) -> None:
|
|
129
|
+
self._rules = rules
|
|
130
|
+
self._initial_predicate_factory = initial_predicate_factory
|
|
131
|
+
|
|
132
|
+
def compile(self, expression: ExpressionDict) -> Predicate[T, R]:
|
|
133
|
+
"""Compiles an expression into a predicate."""
|
|
134
|
+
if is_expression_wrapper_dict(expression):
|
|
135
|
+
return self._compile_wrapper(expression)
|
|
136
|
+
|
|
137
|
+
if is_predicate_dict(expression):
|
|
138
|
+
return self._compile_single(expression)
|
|
139
|
+
|
|
140
|
+
msg = (
|
|
141
|
+
f"Invalid expression type: {expression}\n"
|
|
142
|
+
f"Expected one of\n"
|
|
143
|
+
"{'expressions': list[ExpressionDict], 'inverse': bool, 'operator': Literal['and', 'or']}\n" # noqa: E501
|
|
144
|
+
"or \n{'name': str, 'inverse': bool, 'args': list, 'kwargs': dict}"
|
|
145
|
+
)
|
|
146
|
+
raise CompilationError(msg)
|
|
147
|
+
|
|
148
|
+
def _compile_single(self, single: PredicateDict) -> Predicate[T, R]:
|
|
149
|
+
if single["name"] not in self._rules:
|
|
150
|
+
msg = (
|
|
151
|
+
f"Rule '{single['name']}' is not found\n"
|
|
152
|
+
f"Available rules: {', '.join(self._rules.keys())}"
|
|
153
|
+
)
|
|
154
|
+
raise CompilationError(msg)
|
|
155
|
+
|
|
156
|
+
predicate = self._rules[single["name"]](*single["args"], **single["kwargs"])
|
|
157
|
+
|
|
158
|
+
if single["inverse"]:
|
|
159
|
+
predicate = ~predicate
|
|
160
|
+
|
|
161
|
+
return predicate
|
|
162
|
+
|
|
163
|
+
def _compile_wrapper(self, wrapper: ExpressionWrapperDict) -> Predicate[T, R]:
|
|
164
|
+
predicate = self._initial_predicate_factory(wrapper)
|
|
165
|
+
|
|
166
|
+
for expression in wrapper["expressions"]:
|
|
167
|
+
compiled_predicate = self.compile(expression)
|
|
168
|
+
|
|
169
|
+
if wrapper["operator"] == "and":
|
|
170
|
+
predicate &= compiled_predicate
|
|
171
|
+
else:
|
|
172
|
+
predicate |= compiled_predicate
|
|
173
|
+
|
|
174
|
+
if wrapper["inverse"]:
|
|
175
|
+
predicate = ~predicate
|
|
176
|
+
|
|
177
|
+
return predicate
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
RESERVED_WORDS = ("name", "expressions", "operator", "inverse", "args", "kwargs")
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
class RuleNotRegisteredError(Exception):
|
|
2
|
+
"""Exception raised when a rule is not registered in registry class."""
|
|
3
|
+
|
|
4
|
+
def __init__(self, name: str) -> None:
|
|
5
|
+
super().__init__(f"Rule '{name}' is not registered")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RuleAlreadyRegisteredError(Exception):
|
|
9
|
+
"""Exception raised when a rule is already registered in registry class."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, name: str) -> None:
|
|
12
|
+
super().__init__(f"Rule '{name}' is already registered")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class RuleKeyDoesNotExistError(Exception):
|
|
16
|
+
"""Exception raised when a key does not exist in the object of rule."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, key: str, rule_name: str) -> None:
|
|
19
|
+
super().__init__(f"Key '{key}' does not exist in the object of rule '{rule_name}'")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class CompilationError(Exception):
|
|
23
|
+
"""Exception raised when a rule compilation fails."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ProcessArgumentError(Exception):
|
|
27
|
+
"""Exception raised when a process argument fails in registry class."""
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from inspect import get_annotations
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic import TypeAdapter
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_json_schema(rule: Callable[..., Any]) -> dict[str, Any]:
|
|
9
|
+
"""Get the JSON schema for a rule.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
rule (Callable): The rule to get the JSON schema for.
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
dict: The JSON schema for the rule.
|
|
16
|
+
|
|
17
|
+
Example:
|
|
18
|
+
```python
|
|
19
|
+
from pyspecification import get_json_schema, object_rule
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@object_rule
|
|
23
|
+
def is_admin(user: User) -> bool:
|
|
24
|
+
return user.is_admin
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@object_rule
|
|
28
|
+
def name__istartswith(user: User, value: str) -> bool:
|
|
29
|
+
return user.name.lower().startswith(value.lower())
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@object_rule
|
|
33
|
+
def age__between(user: User, min_age: int, max_age: int) -> bool:
|
|
34
|
+
return user.age >= min_age and user.age <= max_age
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def main() -> None:
|
|
38
|
+
schema = get_json_schema(is_admin)
|
|
39
|
+
print(schema)
|
|
40
|
+
# {"return": {"type": "boolean"}}
|
|
41
|
+
|
|
42
|
+
schema = get_json_schema(name__istartswith)
|
|
43
|
+
print(schema)
|
|
44
|
+
# {"value": {"type": "string"}, "return": {"type": "boolean"}}
|
|
45
|
+
|
|
46
|
+
schema = get_json_schema(age__between)
|
|
47
|
+
print(schema)
|
|
48
|
+
# {
|
|
49
|
+
# "min_age": {"type": "integer"},
|
|
50
|
+
# "max_age": {"type": "integer"},
|
|
51
|
+
# "return": {"type": "boolean"},
|
|
52
|
+
# }
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
if __name__ == "__main__":
|
|
56
|
+
main()
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
"""
|
|
60
|
+
return {
|
|
61
|
+
arg: TypeAdapter(typ).json_schema()
|
|
62
|
+
for idx, (arg, typ) in enumerate(get_annotations(rule).items())
|
|
63
|
+
if idx > 0
|
|
64
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# ruff: noqa: PGH003, SLF001
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from typing import Any, Literal, Protocol
|
|
5
|
+
|
|
6
|
+
type OperatorType = Literal["bitwise", "logical"]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ReturnType(Protocol):
|
|
10
|
+
def __and__(self, value: Any, /) -> Any: ...
|
|
11
|
+
def __or__(self, value: Any, /) -> Any: ...
|
|
12
|
+
def __invert__(self) -> Any: ...
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Predicate[T, R: ReturnType]:
|
|
16
|
+
"""A predicate is a function that takes an object of type T and returns a value of type R.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
fn (Callable[[T], R]): The function to wrap.
|
|
20
|
+
operator (Literal["bitwise", "logical"]): The operator to use for combining predicates.
|
|
21
|
+
description (str, optional): A description of the predicate. Defaults to None.
|
|
22
|
+
|
|
23
|
+
Example:
|
|
24
|
+
```python
|
|
25
|
+
from pyspecification import Predicate
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def is_admin(user: User) -> bool:
|
|
29
|
+
return user.is_admin
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def name__istartswith(user: User, value: str) -> bool:
|
|
33
|
+
return user.name.lower().startswith(value.lower())
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def age__between(user: User, min_age: int, max_age: int) -> bool:
|
|
37
|
+
return user.age >= min_age and user.age <= max_age
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
is_admin: Predicate[User, bool] = Predicate(
|
|
41
|
+
is_admin,
|
|
42
|
+
operator="logical",
|
|
43
|
+
)
|
|
44
|
+
name__istartswith: Predicate[User, bool] = Predicate(
|
|
45
|
+
name__istartswith,
|
|
46
|
+
operator="logical",
|
|
47
|
+
)
|
|
48
|
+
age__between: Predicate[User, bool] = Predicate(
|
|
49
|
+
age__between,
|
|
50
|
+
operator="logical",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
rule = is_admin | (name__istartswith("admin") & age__between(18, 30))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def main() -> None:
|
|
58
|
+
assert rule(User(name="Abdullah", age=18, is_admin=True))
|
|
59
|
+
assert rule(User(name="Abdullah", age=16, is_admin=True))
|
|
60
|
+
assert rule(User(name="admin", age=20, is_admin=False))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
if __name__ == "__main__":
|
|
64
|
+
main()
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def __init__(
|
|
70
|
+
self,
|
|
71
|
+
fn: Callable[[T], R],
|
|
72
|
+
/,
|
|
73
|
+
*,
|
|
74
|
+
operator: OperatorType,
|
|
75
|
+
description: str | None = None,
|
|
76
|
+
) -> None:
|
|
77
|
+
self._fn = fn
|
|
78
|
+
self._description = description
|
|
79
|
+
self._operator = operator
|
|
80
|
+
|
|
81
|
+
def __call__(self, obj: T) -> R:
|
|
82
|
+
return self._fn(obj)
|
|
83
|
+
|
|
84
|
+
def __and__(self, other: "Predicate[T, R]") -> "Predicate[T, R]":
|
|
85
|
+
self._insure_matching_operators(other)
|
|
86
|
+
|
|
87
|
+
if self._operator == "bitwise":
|
|
88
|
+
return Predicate(
|
|
89
|
+
lambda obj: self(obj) & other(obj),
|
|
90
|
+
description=f"({self} & {other})",
|
|
91
|
+
operator="bitwise",
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return Predicate(
|
|
95
|
+
lambda obj: self(obj) and other(obj),
|
|
96
|
+
description=f"({self} AND {other})",
|
|
97
|
+
operator="logical",
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def __or__(self, other: "Predicate[T, R]") -> "Predicate[T, R]":
|
|
101
|
+
self._insure_matching_operators(other)
|
|
102
|
+
|
|
103
|
+
if self._operator == "bitwise":
|
|
104
|
+
return Predicate(
|
|
105
|
+
lambda obj: self(obj) | other(obj),
|
|
106
|
+
description=f"({self} | {other})",
|
|
107
|
+
operator="bitwise",
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
return Predicate(
|
|
111
|
+
lambda obj: self(obj) or other(obj),
|
|
112
|
+
description=f"({self} OR {other})",
|
|
113
|
+
operator="logical",
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def __invert__(self) -> "Predicate[T, R]":
|
|
117
|
+
if self._operator == "bitwise":
|
|
118
|
+
return Predicate(
|
|
119
|
+
lambda obj: ~self(obj),
|
|
120
|
+
description=f"~{self}",
|
|
121
|
+
operator="bitwise",
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
return Predicate(
|
|
125
|
+
lambda obj: not self(obj), # type: ignore
|
|
126
|
+
description=f"NOT {self}",
|
|
127
|
+
operator="logical",
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def __str__(self) -> str:
|
|
131
|
+
return self._description or self._fn.__name__ or "Predicate"
|
|
132
|
+
|
|
133
|
+
def __repr__(self) -> str:
|
|
134
|
+
return f"Predicate({self})"
|
|
135
|
+
|
|
136
|
+
def _insure_matching_operators(self, other: "Predicate[T, R]") -> None:
|
|
137
|
+
if self._operator != other._operator:
|
|
138
|
+
msg = (
|
|
139
|
+
"Cannot combine predicates with different operators:"
|
|
140
|
+
f" {self._operator} != {other._operator}"
|
|
141
|
+
)
|
|
142
|
+
raise ValueError(msg)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from .exceptions import ProcessArgumentError
|
|
5
|
+
|
|
6
|
+
type ProcessFn = Callable[[Any], Any]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
DEFAULT_PROCESSORS: tuple[ProcessFn, dict[str, ProcessFn]] = (lambda value: value, {})
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def process_arguments(
|
|
13
|
+
processors: tuple[ProcessFn, dict[str, ProcessFn]],
|
|
14
|
+
*args: Any,
|
|
15
|
+
**kwargs: Any,
|
|
16
|
+
) -> tuple[tuple[Any, ...], dict[str, Any]]:
|
|
17
|
+
default_fn, processors_map = processors
|
|
18
|
+
|
|
19
|
+
processed_args = []
|
|
20
|
+
current_arg = None
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
for arg in args:
|
|
24
|
+
current_arg = arg
|
|
25
|
+
processed_args.append(default_fn(arg))
|
|
26
|
+
except Exception as e:
|
|
27
|
+
msg = f"Argument '{current_arg}' failed to process, {e}"
|
|
28
|
+
raise ProcessArgumentError(msg) from e
|
|
29
|
+
|
|
30
|
+
processed_kwargs = {}
|
|
31
|
+
current_key, current_value = None, None
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
for key, value in kwargs.items():
|
|
35
|
+
current_key, current_value = key, value
|
|
36
|
+
processed_kwargs[key] = processors_map.get(key, default_fn)(value)
|
|
37
|
+
except Exception as e:
|
|
38
|
+
msg = (
|
|
39
|
+
f"Keyword argument '{current_key}' with value '{current_value}' failed to process, {e}"
|
|
40
|
+
)
|
|
41
|
+
raise ProcessArgumentError(msg) from e
|
|
42
|
+
|
|
43
|
+
return tuple(processed_args), processed_kwargs
|