GeneralManager 0.14.1__py3-none-any.whl → 0.15.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.
- general_manager/__init__.py +49 -0
- general_manager/api/__init__.py +36 -0
- general_manager/api/graphql.py +92 -43
- general_manager/api/mutation.py +35 -10
- general_manager/api/property.py +26 -3
- general_manager/apps.py +23 -16
- general_manager/bucket/__init__.py +32 -0
- general_manager/bucket/baseBucket.py +76 -64
- general_manager/bucket/calculationBucket.py +188 -108
- general_manager/bucket/databaseBucket.py +130 -49
- general_manager/bucket/groupBucket.py +113 -60
- general_manager/cache/__init__.py +38 -0
- general_manager/cache/cacheDecorator.py +29 -17
- general_manager/cache/cacheTracker.py +34 -15
- general_manager/cache/dependencyIndex.py +117 -33
- general_manager/cache/modelDependencyCollector.py +17 -8
- general_manager/cache/signals.py +17 -6
- general_manager/factory/__init__.py +34 -5
- general_manager/factory/autoFactory.py +57 -60
- general_manager/factory/factories.py +39 -14
- general_manager/factory/factoryMethods.py +38 -1
- general_manager/interface/__init__.py +36 -0
- general_manager/interface/baseInterface.py +71 -27
- general_manager/interface/calculationInterface.py +18 -10
- general_manager/interface/databaseBasedInterface.py +102 -71
- general_manager/interface/databaseInterface.py +66 -20
- general_manager/interface/models.py +10 -4
- general_manager/interface/readOnlyInterface.py +44 -30
- general_manager/manager/__init__.py +36 -3
- general_manager/manager/generalManager.py +73 -47
- general_manager/manager/groupManager.py +72 -17
- general_manager/manager/input.py +23 -15
- general_manager/manager/meta.py +53 -53
- general_manager/measurement/__init__.py +37 -2
- general_manager/measurement/measurement.py +135 -58
- general_manager/measurement/measurementField.py +161 -61
- general_manager/permission/__init__.py +32 -1
- general_manager/permission/basePermission.py +29 -12
- general_manager/permission/managerBasedPermission.py +32 -26
- general_manager/permission/mutationPermission.py +32 -3
- general_manager/permission/permissionChecks.py +9 -1
- general_manager/permission/permissionDataManager.py +49 -15
- general_manager/permission/utils.py +14 -3
- general_manager/rule/__init__.py +27 -1
- general_manager/rule/handler.py +90 -5
- general_manager/rule/rule.py +40 -27
- general_manager/utils/__init__.py +44 -2
- general_manager/utils/argsToKwargs.py +17 -9
- general_manager/utils/filterParser.py +29 -30
- general_manager/utils/formatString.py +2 -0
- general_manager/utils/jsonEncoder.py +14 -1
- general_manager/utils/makeCacheKey.py +18 -12
- general_manager/utils/noneToZero.py +8 -6
- general_manager/utils/pathMapping.py +92 -29
- general_manager/utils/public_api.py +49 -0
- general_manager/utils/testing.py +135 -69
- {generalmanager-0.14.1.dist-info → generalmanager-0.15.0.dist-info}/METADATA +10 -2
- generalmanager-0.15.0.dist-info/RECORD +62 -0
- generalmanager-0.14.1.dist-info/RECORD +0 -58
- {generalmanager-0.14.1.dist-info → generalmanager-0.15.0.dist-info}/WHEEL +0 -0
- {generalmanager-0.14.1.dist-info → generalmanager-0.15.0.dist-info}/licenses/LICENSE +0 -0
- {generalmanager-0.14.1.dist-info → generalmanager-0.15.0.dist-info}/top_level.txt +0 -0
@@ -1,3 +1,5 @@
|
|
1
|
+
"""Permission helper for GraphQL mutations."""
|
2
|
+
|
1
3
|
from __future__ import annotations
|
2
4
|
from django.contrib.auth.models import AbstractUser, AnonymousUser
|
3
5
|
from typing import Any
|
@@ -8,12 +10,21 @@ from general_manager.permission.utils import validatePermissionString
|
|
8
10
|
|
9
11
|
|
10
12
|
class MutationPermission:
|
13
|
+
"""Evaluate mutation permissions using class-level configuration."""
|
14
|
+
|
11
15
|
__mutate__: list[str]
|
12
16
|
|
13
17
|
def __init__(
|
14
18
|
self, data: dict[str, Any], request_user: AbstractUser | AnonymousUser
|
15
19
|
) -> None:
|
16
|
-
|
20
|
+
"""
|
21
|
+
Create a mutation permission context for the given data and user.
|
22
|
+
|
23
|
+
Parameters:
|
24
|
+
data (dict[str, Any]): Input payload for the mutation.
|
25
|
+
request_user (AbstractUser | AnonymousUser): User attempting the mutation.
|
26
|
+
"""
|
27
|
+
self._data: PermissionDataManager = PermissionDataManager(data)
|
17
28
|
self._request_user = request_user
|
18
29
|
self.__attribute_permissions = self.__getAttributePermissions()
|
19
30
|
|
@@ -21,15 +32,18 @@ class MutationPermission:
|
|
21
32
|
|
22
33
|
@property
|
23
34
|
def data(self) -> PermissionDataManager:
|
35
|
+
"""Return wrapped permission data."""
|
24
36
|
return self._data
|
25
37
|
|
26
38
|
@property
|
27
39
|
def request_user(self) -> AbstractUser | AnonymousUser:
|
40
|
+
"""Return the user whose permissions are being evaluated."""
|
28
41
|
return self._request_user
|
29
42
|
|
30
43
|
def __getAttributePermissions(
|
31
44
|
self,
|
32
45
|
) -> dict[str, list[str]]:
|
46
|
+
"""Collect attribute-specific permission expressions declared on the class."""
|
33
47
|
attribute_permissions = {}
|
34
48
|
for attribute in self.__class__.__dict__:
|
35
49
|
if not attribute.startswith("__"):
|
@@ -43,9 +57,14 @@ class MutationPermission:
|
|
43
57
|
request_user: AbstractUser | AnonymousUser | Any,
|
44
58
|
) -> None:
|
45
59
|
"""
|
46
|
-
|
60
|
+
Validate that ``request_user`` may execute the mutation for the provided data.
|
61
|
+
|
62
|
+
Parameters:
|
63
|
+
data (dict[str, Any]): Mutation payload.
|
64
|
+
request_user (AbstractUser | AnonymousUser | Any): User or user ID.
|
65
|
+
|
47
66
|
Raises:
|
48
|
-
PermissionError: If
|
67
|
+
PermissionError: If any field-level permission check fails.
|
49
68
|
"""
|
50
69
|
errors = []
|
51
70
|
if not isinstance(request_user, (AbstractUser, AnonymousUser)):
|
@@ -63,6 +82,15 @@ class MutationPermission:
|
|
63
82
|
self,
|
64
83
|
attribute: str,
|
65
84
|
) -> bool:
|
85
|
+
"""
|
86
|
+
Evaluate permissions for a specific attribute within the mutation payload.
|
87
|
+
|
88
|
+
Parameters:
|
89
|
+
attribute (str): Attribute name being validated.
|
90
|
+
|
91
|
+
Returns:
|
92
|
+
bool: True when permitted, False otherwise.
|
93
|
+
"""
|
66
94
|
|
67
95
|
has_attribute_permissions = attribute in self.__attribute_permissions
|
68
96
|
|
@@ -84,6 +112,7 @@ class MutationPermission:
|
|
84
112
|
self,
|
85
113
|
permissions: list[str],
|
86
114
|
) -> bool:
|
115
|
+
"""Return True when any permission expression evaluates to True."""
|
87
116
|
for permission in permissions:
|
88
117
|
if validatePermissionString(permission, self.data, self.request_user):
|
89
118
|
return True
|
@@ -1,3 +1,5 @@
|
|
1
|
+
"""Registry of reusable permission checks and their queryset filters."""
|
2
|
+
|
1
3
|
from typing import Any, Callable, TYPE_CHECKING, TypedDict, Literal
|
2
4
|
|
3
5
|
if TYPE_CHECKING:
|
@@ -25,6 +27,8 @@ type permission_method = Callable[
|
|
25
27
|
|
26
28
|
|
27
29
|
class PermissionDict(TypedDict):
|
30
|
+
"""Typed dictionary describing a registered permission function."""
|
31
|
+
|
28
32
|
permission_method: permission_method
|
29
33
|
permission_filter: permission_filter
|
30
34
|
|
@@ -47,10 +51,14 @@ permission_functions: dict[str, PermissionDict] = {
|
|
47
51
|
"filter": {f"{config[0]}__endswith": config[1]}
|
48
52
|
},
|
49
53
|
},
|
50
|
-
"
|
54
|
+
"isAdmin": {
|
51
55
|
"permission_method": lambda instance, user, config: user.is_staff,
|
52
56
|
"permission_filter": lambda user, config: None,
|
53
57
|
},
|
58
|
+
"isSelf": {
|
59
|
+
"permission_method": lambda instance, user, config: instance.creator == user, # type: ignore
|
60
|
+
"permission_filter": lambda user, config: {"filter": {"creator_id": user.id}}, # type: ignore
|
61
|
+
},
|
54
62
|
"isAuthenticated": {
|
55
63
|
"permission_method": lambda instance, user, config: user.is_authenticated,
|
56
64
|
"permission_filter": lambda user, config: None,
|
@@ -1,5 +1,7 @@
|
|
1
|
+
"""Wrapper for accessing permission-relevant data across manager operations."""
|
2
|
+
|
1
3
|
from __future__ import annotations
|
2
|
-
from typing import Callable,
|
4
|
+
from typing import Callable, Optional, TypeVar, Generic, cast
|
3
5
|
from django.contrib.auth.models import AbstractUser
|
4
6
|
|
5
7
|
from general_manager.manager.generalManager import GeneralManager
|
@@ -8,22 +10,41 @@ GeneralManagerData = TypeVar("GeneralManagerData", bound=GeneralManager)
|
|
8
10
|
|
9
11
|
|
10
12
|
class PermissionDataManager(Generic[GeneralManagerData]):
|
13
|
+
"""Adapter that exposes permission-related data as a unified interface."""
|
14
|
+
|
11
15
|
def __init__(
|
12
16
|
self,
|
13
|
-
permission_data:
|
17
|
+
permission_data: dict[str, object] | GeneralManagerData,
|
14
18
|
manager: Optional[type[GeneralManagerData]] = None,
|
15
|
-
):
|
16
|
-
|
19
|
+
) -> None:
|
20
|
+
"""
|
21
|
+
Create a permission data manager wrapping either a dict or a manager instance.
|
22
|
+
|
23
|
+
Parameters:
|
24
|
+
permission_data (dict[str, Any] | GeneralManager): Raw data or manager instance supplying field values.
|
25
|
+
manager (type[GeneralManager] | None): Manager class when `permission_data` is a dict.
|
26
|
+
|
27
|
+
Raises:
|
28
|
+
TypeError: If `permission_data` is neither a dict nor a `GeneralManager`.
|
29
|
+
"""
|
30
|
+
self.getData: Callable[[str], object]
|
17
31
|
self._permission_data = permission_data
|
32
|
+
self._manager: type[GeneralManagerData] | None
|
18
33
|
if isinstance(permission_data, GeneralManager):
|
19
|
-
|
20
|
-
|
21
|
-
)
|
22
|
-
|
34
|
+
gm_instance = permission_data
|
35
|
+
|
36
|
+
def manager_getter(name: str) -> object:
|
37
|
+
return getattr(gm_instance, name)
|
38
|
+
|
39
|
+
self.getData = manager_getter
|
40
|
+
self._manager = cast(type[GeneralManagerData], permission_data.__class__)
|
23
41
|
elif isinstance(permission_data, dict):
|
24
|
-
|
25
|
-
|
26
|
-
)
|
42
|
+
data_mapping = permission_data
|
43
|
+
|
44
|
+
def dict_getter(name: str) -> object:
|
45
|
+
return data_mapping.get(name)
|
46
|
+
|
47
|
+
self.getData = dict_getter
|
27
48
|
self._manager = manager
|
28
49
|
else:
|
29
50
|
raise TypeError(
|
@@ -34,18 +55,31 @@ class PermissionDataManager(Generic[GeneralManagerData]):
|
|
34
55
|
def forUpdate(
|
35
56
|
cls,
|
36
57
|
base_data: GeneralManagerData,
|
37
|
-
update_data:
|
58
|
+
update_data: dict[str, object],
|
38
59
|
) -> PermissionDataManager:
|
39
|
-
|
60
|
+
"""
|
61
|
+
Create a data manager that reflects a pending update to an existing manager.
|
62
|
+
|
63
|
+
Parameters:
|
64
|
+
base_data (GeneralManager): Existing manager instance.
|
65
|
+
update_data (dict[str, Any]): Fields being updated.
|
66
|
+
|
67
|
+
Returns:
|
68
|
+
PermissionDataManager: Wrapper exposing merged data for permission checks.
|
69
|
+
"""
|
70
|
+
merged_data: dict[str, object] = {**dict(base_data), **update_data}
|
40
71
|
return cls(merged_data, base_data.__class__)
|
41
72
|
|
42
73
|
@property
|
43
|
-
def permission_data(self) ->
|
74
|
+
def permission_data(self) -> dict[str, object] | GeneralManagerData:
|
75
|
+
"""Return the underlying permission payload."""
|
44
76
|
return self._permission_data
|
45
77
|
|
46
78
|
@property
|
47
79
|
def manager(self) -> type[GeneralManagerData] | None:
|
80
|
+
"""Return the manager class associated with the permission data."""
|
48
81
|
return self._manager
|
49
82
|
|
50
|
-
def __getattr__(self, name: str) ->
|
83
|
+
def __getattr__(self, name: str) -> object:
|
84
|
+
"""Proxy attribute access to the wrapped permission data."""
|
51
85
|
return self.getData(name)
|
@@ -1,3 +1,5 @@
|
|
1
|
+
"""Utility helpers for evaluating permission expressions."""
|
2
|
+
|
1
3
|
from general_manager.permission.permissionChecks import (
|
2
4
|
permission_functions,
|
3
5
|
)
|
@@ -13,9 +15,18 @@ def validatePermissionString(
|
|
13
15
|
data: PermissionDataManager | GeneralManager | GeneralManagerMeta,
|
14
16
|
request_user: AbstractUser | AnonymousUser,
|
15
17
|
) -> bool:
|
16
|
-
|
17
|
-
|
18
|
-
|
18
|
+
"""
|
19
|
+
Evaluate a compound permission expression joined by ``&`` operators.
|
20
|
+
|
21
|
+
Parameters:
|
22
|
+
permission (str): Permission expression (for example, ``isAuthenticated&admin``).
|
23
|
+
data (PermissionDataManager | GeneralManager | GeneralManagerMeta): Object evaluated by the permission functions.
|
24
|
+
request_user (AbstractUser | AnonymousUser): User performing the action.
|
25
|
+
|
26
|
+
Returns:
|
27
|
+
bool: True if every sub-permission evaluates to True.
|
28
|
+
"""
|
29
|
+
|
19
30
|
def _validateSinglePermission(
|
20
31
|
permission: str,
|
21
32
|
) -> bool:
|
general_manager/rule/__init__.py
CHANGED
@@ -1 +1,27 @@
|
|
1
|
-
|
1
|
+
"""Helpers for defining rule-based validations."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from typing import Any
|
6
|
+
|
7
|
+
from general_manager.utils.public_api import build_module_dir, resolve_export
|
8
|
+
|
9
|
+
__all__ = ["Rule", "BaseRuleHandler"]
|
10
|
+
|
11
|
+
_MODULE_MAP = {
|
12
|
+
"Rule": ("general_manager.rule.rule", "Rule"),
|
13
|
+
"BaseRuleHandler": ("general_manager.rule.handler", "BaseRuleHandler"),
|
14
|
+
}
|
15
|
+
|
16
|
+
|
17
|
+
def __getattr__(name: str) -> Any:
|
18
|
+
return resolve_export(
|
19
|
+
name,
|
20
|
+
module_all=__all__,
|
21
|
+
module_map=_MODULE_MAP,
|
22
|
+
module_globals=globals(),
|
23
|
+
)
|
24
|
+
|
25
|
+
|
26
|
+
def __dir__() -> list[str]:
|
27
|
+
return build_module_dir(module_all=__all__, module_globals=globals())
|
general_manager/rule/handler.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
|
1
|
+
"""Rule handler implementations that craft error messages from AST nodes."""
|
2
2
|
|
3
3
|
from __future__ import annotations
|
4
4
|
import ast
|
@@ -10,7 +10,7 @@ if TYPE_CHECKING:
|
|
10
10
|
|
11
11
|
|
12
12
|
class BaseRuleHandler(ABC):
|
13
|
-
"""
|
13
|
+
"""Define the protocol for generating rule-specific error messages."""
|
14
14
|
|
15
15
|
function_name: str # ClassVar, der Name, unter dem dieser Handler registriert wird
|
16
16
|
|
@@ -25,14 +25,25 @@ class BaseRuleHandler(ABC):
|
|
25
25
|
rule: Rule,
|
26
26
|
) -> Dict[str, str]:
|
27
27
|
"""
|
28
|
-
|
28
|
+
Produce error messages for a comparison or function call node.
|
29
|
+
|
30
|
+
Parameters:
|
31
|
+
node (ast.AST): AST node representing the expression being evaluated.
|
32
|
+
left (ast.expr | None): Left operand when applicable.
|
33
|
+
right (ast.expr | None): Right operand when applicable.
|
34
|
+
op (ast.cmpop | None): Comparison operator node.
|
35
|
+
var_values (dict[str, object | None]): Resolved variable values used during evaluation.
|
36
|
+
rule (Rule): Rule invoking the handler.
|
37
|
+
|
38
|
+
Returns:
|
39
|
+
dict[str, str]: Mapping of variable names to error messages.
|
29
40
|
"""
|
30
41
|
pass
|
31
42
|
|
32
43
|
|
33
44
|
class FunctionHandler(BaseRuleHandler, ABC):
|
34
45
|
"""
|
35
|
-
|
46
|
+
Base class for handlers that evaluate function-call expressions such as len(), max(), or sum().
|
36
47
|
"""
|
37
48
|
|
38
49
|
def handle(
|
@@ -74,7 +85,17 @@ class FunctionHandler(BaseRuleHandler, ABC):
|
|
74
85
|
rule: Rule,
|
75
86
|
) -> Dict[str, str]:
|
76
87
|
"""
|
77
|
-
|
88
|
+
Analyse the call arguments and construct an error message payload.
|
89
|
+
|
90
|
+
Parameters:
|
91
|
+
arg_node (ast.expr): AST node representing the function argument.
|
92
|
+
right_node (ast.expr): Node representing the comparison threshold.
|
93
|
+
op_symbol (str): Symbolic representation of the comparison operator.
|
94
|
+
var_values (dict[str, object | None]): Resolved values used during evaluation.
|
95
|
+
rule (Rule): Rule requesting the aggregation.
|
96
|
+
|
97
|
+
Returns:
|
98
|
+
dict[str, str]: Mapping of variable names to error messages.
|
78
99
|
"""
|
79
100
|
raise NotImplementedError("Subclasses should implement this method")
|
80
101
|
|
@@ -90,6 +111,22 @@ class LenHandler(FunctionHandler):
|
|
90
111
|
var_values: Dict[str, Optional[object]],
|
91
112
|
rule: Rule,
|
92
113
|
) -> Dict[str, str]:
|
114
|
+
"""
|
115
|
+
Evaluate length-based limits and craft an error message when violated.
|
116
|
+
|
117
|
+
Parameters:
|
118
|
+
arg_node (ast.expr): AST node representing the iterable passed to `len`.
|
119
|
+
right_node (ast.expr): Comparison threshold node.
|
120
|
+
op_symbol (str): Operator symbol describing the comparison.
|
121
|
+
var_values (dict[str, object | None]): Evaluated variable values.
|
122
|
+
rule (Rule): Calling rule used for helper evaluations.
|
123
|
+
|
124
|
+
Returns:
|
125
|
+
dict[str, str]: Mapping containing a single error message keyed by variable name.
|
126
|
+
|
127
|
+
Raises:
|
128
|
+
ValueError: If the argument is invalid or the threshold is not numeric.
|
129
|
+
"""
|
93
130
|
|
94
131
|
var_name = rule._get_node_name(arg_node)
|
95
132
|
var_value = var_values.get(var_name)
|
@@ -133,6 +170,22 @@ class SumHandler(FunctionHandler):
|
|
133
170
|
var_values: Dict[str, Optional[object]],
|
134
171
|
rule: Rule,
|
135
172
|
) -> Dict[str, str]:
|
173
|
+
"""
|
174
|
+
Compute the sum of an iterable and compare it to the threshold.
|
175
|
+
|
176
|
+
Parameters:
|
177
|
+
arg_node (ast.expr): AST node representing the iterable passed to `sum`.
|
178
|
+
right_node (ast.expr): Node describing the threshold value.
|
179
|
+
op_symbol (str): Operator symbol describing the comparison.
|
180
|
+
var_values (dict[str, object | None]): Evaluated variable values.
|
181
|
+
rule (Rule): Calling rule used for helper evaluations.
|
182
|
+
|
183
|
+
Returns:
|
184
|
+
dict[str, str]: Mapping containing a single error message keyed by variable name.
|
185
|
+
|
186
|
+
Raises:
|
187
|
+
ValueError: If the argument is not a numeric iterable or the threshold is invalid.
|
188
|
+
"""
|
136
189
|
|
137
190
|
# Name und Wert holen
|
138
191
|
var_name = rule._get_node_name(arg_node)
|
@@ -175,6 +228,22 @@ class MaxHandler(FunctionHandler):
|
|
175
228
|
var_values: Dict[str, Optional[object]],
|
176
229
|
rule: Rule,
|
177
230
|
) -> Dict[str, str]:
|
231
|
+
"""
|
232
|
+
Compare the maximum element of an iterable against the provided threshold.
|
233
|
+
|
234
|
+
Parameters:
|
235
|
+
arg_node (ast.expr): AST node representing the iterable passed to `max`.
|
236
|
+
right_node (ast.expr): Node describing the threshold value.
|
237
|
+
op_symbol (str): Operator symbol describing the comparison.
|
238
|
+
var_values (dict[str, object | None]): Evaluated variable values.
|
239
|
+
rule (Rule): Calling rule used for helper evaluations.
|
240
|
+
|
241
|
+
Returns:
|
242
|
+
dict[str, str]: Mapping containing a single error message keyed by variable name.
|
243
|
+
|
244
|
+
Raises:
|
245
|
+
ValueError: If the iterable is empty, non-numeric, or the threshold is invalid.
|
246
|
+
"""
|
178
247
|
|
179
248
|
var_name = rule._get_node_name(arg_node)
|
180
249
|
raw_iter = var_values.get(var_name)
|
@@ -210,6 +279,22 @@ class MinHandler(FunctionHandler):
|
|
210
279
|
var_values: Dict[str, Optional[object]],
|
211
280
|
rule: Rule,
|
212
281
|
) -> Dict[str, str]:
|
282
|
+
"""
|
283
|
+
Compare the minimum element of an iterable against the provided threshold.
|
284
|
+
|
285
|
+
Parameters:
|
286
|
+
arg_node (ast.expr): AST node representing the iterable passed to `min`.
|
287
|
+
right_node (ast.expr): Node describing the threshold value.
|
288
|
+
op_symbol (str): Operator symbol describing the comparison.
|
289
|
+
var_values (dict[str, object | None]): Evaluated variable values.
|
290
|
+
rule (Rule): Calling rule used for helper evaluations.
|
291
|
+
|
292
|
+
Returns:
|
293
|
+
dict[str, str]: Mapping containing a single error message keyed by variable name.
|
294
|
+
|
295
|
+
Raises:
|
296
|
+
ValueError: If the iterable is empty, non-numeric, or the threshold is invalid.
|
297
|
+
"""
|
213
298
|
|
214
299
|
var_name = rule._get_node_name(arg_node)
|
215
300
|
raw_iter = var_values.get(var_name)
|
general_manager/rule/rule.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
|
1
|
+
"""Infrastructure for expressing validation rules against GeneralManager instances."""
|
2
2
|
|
3
3
|
from __future__ import annotations
|
4
4
|
import ast
|
@@ -33,8 +33,10 @@ GeneralManagerType = TypeVar("GeneralManagerType", bound=GeneralManager)
|
|
33
33
|
|
34
34
|
class Rule(Generic[GeneralManagerType]):
|
35
35
|
"""
|
36
|
-
|
37
|
-
|
36
|
+
Encapsulate a boolean predicate and derive contextual error messages from its AST.
|
37
|
+
|
38
|
+
When the predicate evaluates to False, the rule inspects the parsed abstract syntax tree
|
39
|
+
to determine which variables failed and crafts either autogenerated or custom error messages.
|
38
40
|
"""
|
39
41
|
|
40
42
|
_func: Callable[[GeneralManagerType], bool]
|
@@ -58,27 +60,22 @@ class Rule(Generic[GeneralManagerType]):
|
|
58
60
|
self._last_result = None
|
59
61
|
self._last_input = None
|
60
62
|
|
61
|
-
# 1)
|
62
|
-
src = inspect.getsource(func)
|
63
|
-
lines = src.splitlines()
|
64
|
-
if lines and lines[0].strip().startswith("@"):
|
65
|
-
idx = next(i for i, L in enumerate(lines) if not L.strip().startswith("@"))
|
66
|
-
src = "\n".join(lines[idx:])
|
67
|
-
src = textwrap.dedent(src)
|
63
|
+
# 1) Extract source, strip decorators, and dedent
|
64
|
+
src = textwrap.dedent(inspect.getsource(func))
|
68
65
|
|
69
|
-
# 2) AST
|
66
|
+
# 2) Parse AST and attach parent references
|
70
67
|
self._tree = ast.parse(src)
|
71
68
|
for parent in ast.walk(self._tree):
|
72
69
|
for child in ast.iter_child_nodes(parent):
|
73
70
|
setattr(child, "parent", parent)
|
74
71
|
|
75
|
-
# 3)
|
72
|
+
# 3) Extract referenced variables
|
76
73
|
self._variables = self._extract_variables()
|
77
74
|
|
78
|
-
# 4)
|
75
|
+
# 4) Register handlers
|
79
76
|
self._handlers = {} # type: Dict[str, BaseRuleHandler]
|
80
77
|
for cls in (LenHandler, MaxHandler, MinHandler, SumHandler):
|
81
|
-
inst = cls()
|
78
|
+
inst: BaseRuleHandler = cls()
|
82
79
|
self._handlers[inst.function_name] = inst
|
83
80
|
for path in getattr(settings, "RULE_HANDLERS", []):
|
84
81
|
handler_cls: type[BaseRuleHandler] = import_string(path)
|
@@ -111,8 +108,13 @@ class Rule(Generic[GeneralManagerType]):
|
|
111
108
|
|
112
109
|
def evaluate(self, x: GeneralManagerType) -> Optional[bool]:
|
113
110
|
"""
|
114
|
-
|
115
|
-
|
111
|
+
Execute the predicate against a manager instance and record the result.
|
112
|
+
|
113
|
+
Parameters:
|
114
|
+
x (GeneralManagerType): Manager instance supplied to the predicate.
|
115
|
+
|
116
|
+
Returns:
|
117
|
+
bool | None: True or False when the predicate executes; None when `ignore_if_none` is set and any referenced value is None.
|
116
118
|
"""
|
117
119
|
self._last_input = x
|
118
120
|
vals = self._extract_variable_values(x)
|
@@ -125,8 +127,13 @@ class Rule(Generic[GeneralManagerType]):
|
|
125
127
|
|
126
128
|
def validateCustomErrorMessage(self) -> None:
|
127
129
|
"""
|
128
|
-
|
129
|
-
|
130
|
+
Ensure the user-defined template references every extracted variable.
|
131
|
+
|
132
|
+
Returns:
|
133
|
+
None
|
134
|
+
|
135
|
+
Raises:
|
136
|
+
ValueError: If the custom error message omits a required placeholder.
|
130
137
|
"""
|
131
138
|
if not self._custom_error_message:
|
132
139
|
return
|
@@ -140,14 +147,20 @@ class Rule(Generic[GeneralManagerType]):
|
|
140
147
|
|
141
148
|
def getErrorMessage(self) -> Optional[Dict[str, str]]:
|
142
149
|
"""
|
143
|
-
|
150
|
+
Build a mapping of variable names to error messages for the last evaluation.
|
151
|
+
|
152
|
+
Returns:
|
153
|
+
dict[str, str] | None: Mapping describing validation failures, or None when the predicate passed.
|
154
|
+
|
155
|
+
Raises:
|
156
|
+
ValueError: If called before any input has been evaluated.
|
144
157
|
"""
|
145
158
|
if self._last_result or self._last_result is None:
|
146
159
|
return None
|
147
160
|
if self._last_input is None:
|
148
161
|
raise ValueError("No input provided for error message generation")
|
149
162
|
|
150
|
-
#
|
163
|
+
# Validate and substitute template placeholders
|
151
164
|
self.validateCustomErrorMessage()
|
152
165
|
vals = self._extract_variable_values(self._last_input)
|
153
166
|
|
@@ -229,7 +242,7 @@ class Rule(Generic[GeneralManagerType]):
|
|
229
242
|
for cmp in comparisons:
|
230
243
|
left, rights, ops = cmp.left, cmp.comparators, cmp.ops
|
231
244
|
for right, op in zip(rights, ops):
|
232
|
-
#
|
245
|
+
# Special handler?
|
233
246
|
if isinstance(left, ast.Call):
|
234
247
|
fn = self._get_node_name(left.func)
|
235
248
|
handler = self._handlers.get(fn)
|
@@ -239,7 +252,7 @@ class Rule(Generic[GeneralManagerType]):
|
|
239
252
|
)
|
240
253
|
continue
|
241
254
|
|
242
|
-
# Standard
|
255
|
+
# Standard error message
|
243
256
|
lnm = self._get_node_name(left)
|
244
257
|
rnm = self._get_node_name(right)
|
245
258
|
lval = self._eval_node(left)
|
@@ -261,11 +274,13 @@ class Rule(Generic[GeneralManagerType]):
|
|
261
274
|
|
262
275
|
return errors
|
263
276
|
|
264
|
-
#
|
277
|
+
# No comparisons present → fall back to a generic message
|
265
278
|
combo = ", ".join(f"[{v}]" for v in self._variables)
|
266
279
|
return {v: f"{combo} combination is not valid" for v in self._variables}
|
267
280
|
|
268
281
|
def _get_op_symbol(self, op: Optional[ast.cmpop]) -> str:
|
282
|
+
if op is None:
|
283
|
+
return "?"
|
269
284
|
return {
|
270
285
|
ast.Lt: "<",
|
271
286
|
ast.LtE: "<=",
|
@@ -296,15 +311,13 @@ class Rule(Generic[GeneralManagerType]):
|
|
296
311
|
args = ", ".join(self._get_node_name(a) for a in node.args)
|
297
312
|
return f"{fn}({args})"
|
298
313
|
try:
|
299
|
-
# ast.unparse
|
314
|
+
# ast.unparse returns a string representation
|
300
315
|
return ast.unparse(node)
|
301
316
|
except Exception:
|
302
317
|
return ""
|
303
318
|
|
304
319
|
def _eval_node(self, node: ast.expr) -> Optional[object]:
|
305
|
-
"""
|
306
|
-
Evaluiert einen AST-Ausdruck im Kontext von `x`.
|
307
|
-
"""
|
320
|
+
"""Evaluate an AST expression in the context of the last input."""
|
308
321
|
if not isinstance(node, ast.expr):
|
309
322
|
return None
|
310
323
|
try:
|
@@ -1,2 +1,44 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
"""Convenience re-exports for common utility helpers."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from typing import Any
|
6
|
+
|
7
|
+
from general_manager.utils.public_api import build_module_dir, resolve_export
|
8
|
+
|
9
|
+
__all__ = [
|
10
|
+
"noneToZero",
|
11
|
+
"args_to_kwargs",
|
12
|
+
"make_cache_key",
|
13
|
+
"parse_filters",
|
14
|
+
"create_filter_function",
|
15
|
+
"snake_to_pascal",
|
16
|
+
"snake_to_camel",
|
17
|
+
"pascal_to_snake",
|
18
|
+
"camel_to_snake",
|
19
|
+
]
|
20
|
+
|
21
|
+
_MODULE_MAP = {
|
22
|
+
"noneToZero": ("general_manager.utils.noneToZero", "noneToZero"),
|
23
|
+
"args_to_kwargs": ("general_manager.utils.argsToKwargs", "args_to_kwargs"),
|
24
|
+
"make_cache_key": ("general_manager.utils.makeCacheKey", "make_cache_key"),
|
25
|
+
"parse_filters": ("general_manager.utils.filterParser", "parse_filters"),
|
26
|
+
"create_filter_function": ("general_manager.utils.filterParser", "create_filter_function"),
|
27
|
+
"snake_to_pascal": ("general_manager.utils.formatString", "snake_to_pascal"),
|
28
|
+
"snake_to_camel": ("general_manager.utils.formatString", "snake_to_camel"),
|
29
|
+
"pascal_to_snake": ("general_manager.utils.formatString", "pascal_to_snake"),
|
30
|
+
"camel_to_snake": ("general_manager.utils.formatString", "camel_to_snake"),
|
31
|
+
}
|
32
|
+
|
33
|
+
|
34
|
+
def __getattr__(name: str) -> Any:
|
35
|
+
return resolve_export(
|
36
|
+
name,
|
37
|
+
module_all=__all__,
|
38
|
+
module_map=_MODULE_MAP,
|
39
|
+
module_globals=globals(),
|
40
|
+
)
|
41
|
+
|
42
|
+
|
43
|
+
def __dir__() -> list[str]:
|
44
|
+
return build_module_dir(module_all=__all__, module_globals=globals())
|
@@ -1,22 +1,30 @@
|
|
1
|
-
from typing import
|
1
|
+
from typing import Iterable, Mapping
|
2
2
|
|
3
3
|
|
4
4
|
def args_to_kwargs(
|
5
|
-
args: tuple[
|
6
|
-
|
5
|
+
args: tuple[object, ...],
|
6
|
+
keys: Iterable[str],
|
7
|
+
existing_kwargs: Mapping[str, object] | None = None,
|
8
|
+
) -> dict[str, object]:
|
7
9
|
"""
|
8
|
-
|
10
|
+
Convert positional arguments to keyword arguments and merge them into an existing mapping.
|
9
11
|
|
10
|
-
:
|
11
|
-
|
12
|
-
|
13
|
-
|
12
|
+
Parameters:
|
13
|
+
args (tuple[Any, ...]): Positional arguments that should be mapped to keyword arguments.
|
14
|
+
keys (Iterable[Any]): Keys used to map each positional argument within `args`.
|
15
|
+
existing_kwargs (dict | None): Optional keyword argument mapping to merge with the generated values.
|
16
|
+
|
17
|
+
Returns:
|
18
|
+
dict[Any, Any]: A dictionary containing the merged keyword arguments.
|
19
|
+
|
20
|
+
Raises:
|
21
|
+
TypeError: If the number of positional arguments exceeds the number of provided keys, or if any generated keyword collides with `existing_kwargs`.
|
14
22
|
"""
|
15
23
|
keys = list(keys)
|
16
24
|
if len(args) > len(keys):
|
17
25
|
raise TypeError("More positional arguments than keys provided.")
|
18
26
|
|
19
|
-
kwargs = {key: value for key, value in zip(keys, args)}
|
27
|
+
kwargs: dict[str, object] = {key: value for key, value in zip(keys, args)}
|
20
28
|
if existing_kwargs and any(key in kwargs for key in existing_kwargs):
|
21
29
|
raise TypeError("Conflicts in existing kwargs.")
|
22
30
|
if existing_kwargs:
|