GeneralManager 0.14.0__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.
Files changed (63) hide show
  1. general_manager/__init__.py +49 -0
  2. general_manager/api/__init__.py +36 -0
  3. general_manager/api/graphql.py +92 -43
  4. general_manager/api/mutation.py +35 -10
  5. general_manager/api/property.py +26 -3
  6. general_manager/apps.py +23 -16
  7. general_manager/bucket/__init__.py +32 -0
  8. general_manager/bucket/baseBucket.py +76 -64
  9. general_manager/bucket/calculationBucket.py +188 -108
  10. general_manager/bucket/databaseBucket.py +130 -49
  11. general_manager/bucket/groupBucket.py +113 -60
  12. general_manager/cache/__init__.py +38 -0
  13. general_manager/cache/cacheDecorator.py +29 -17
  14. general_manager/cache/cacheTracker.py +34 -15
  15. general_manager/cache/dependencyIndex.py +117 -33
  16. general_manager/cache/modelDependencyCollector.py +17 -8
  17. general_manager/cache/signals.py +17 -6
  18. general_manager/factory/__init__.py +34 -5
  19. general_manager/factory/autoFactory.py +57 -60
  20. general_manager/factory/factories.py +39 -14
  21. general_manager/factory/factoryMethods.py +38 -1
  22. general_manager/interface/__init__.py +36 -0
  23. general_manager/interface/baseInterface.py +71 -27
  24. general_manager/interface/calculationInterface.py +18 -10
  25. general_manager/interface/databaseBasedInterface.py +102 -71
  26. general_manager/interface/databaseInterface.py +66 -20
  27. general_manager/interface/models.py +10 -4
  28. general_manager/interface/readOnlyInterface.py +44 -30
  29. general_manager/manager/__init__.py +36 -3
  30. general_manager/manager/generalManager.py +73 -47
  31. general_manager/manager/groupManager.py +72 -17
  32. general_manager/manager/input.py +23 -15
  33. general_manager/manager/meta.py +53 -53
  34. general_manager/measurement/__init__.py +37 -2
  35. general_manager/measurement/measurement.py +135 -58
  36. general_manager/measurement/measurementField.py +161 -61
  37. general_manager/permission/__init__.py +32 -1
  38. general_manager/permission/basePermission.py +29 -12
  39. general_manager/permission/managerBasedPermission.py +32 -26
  40. general_manager/permission/mutationPermission.py +32 -3
  41. general_manager/permission/permissionChecks.py +9 -1
  42. general_manager/permission/permissionDataManager.py +49 -15
  43. general_manager/permission/utils.py +14 -3
  44. general_manager/rule/__init__.py +27 -1
  45. general_manager/rule/handler.py +90 -5
  46. general_manager/rule/rule.py +40 -27
  47. general_manager/utils/__init__.py +44 -2
  48. general_manager/utils/argsToKwargs.py +17 -9
  49. general_manager/utils/filterParser.py +29 -30
  50. general_manager/utils/formatString.py +2 -0
  51. general_manager/utils/jsonEncoder.py +14 -1
  52. general_manager/utils/makeCacheKey.py +18 -12
  53. general_manager/utils/noneToZero.py +8 -6
  54. general_manager/utils/pathMapping.py +92 -29
  55. general_manager/utils/public_api.py +49 -0
  56. general_manager/utils/testing.py +135 -69
  57. {generalmanager-0.14.0.dist-info → generalmanager-0.15.0.dist-info}/METADATA +38 -4
  58. generalmanager-0.15.0.dist-info/RECORD +62 -0
  59. generalmanager-0.15.0.dist-info/licenses/LICENSE +21 -0
  60. generalmanager-0.14.0.dist-info/RECORD +0 -58
  61. generalmanager-0.14.0.dist-info/licenses/LICENSE +0 -29
  62. {generalmanager-0.14.0.dist-info → generalmanager-0.15.0.dist-info}/WHEEL +0 -0
  63. {generalmanager-0.14.0.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
- self._data = PermissionDataManager(data)
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
- Check if the user has permission to perform the mutation based on the provided data.
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 the user does not have permission.
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
- "admin": {
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, Dict, Any, Optional, TypeVar, Generic
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: Dict[str, Any] | GeneralManagerData,
17
+ permission_data: dict[str, object] | GeneralManagerData,
14
18
  manager: Optional[type[GeneralManagerData]] = None,
15
- ):
16
- self.getData: Callable[[str], Any]
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
- self.getData = lambda name, permission_data=permission_data: getattr(
20
- permission_data, name
21
- )
22
- self._manager = permission_data.__class__
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
- self.getData = (
25
- lambda name, permission_data=permission_data: permission_data.get(name)
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: Dict[str, Any],
58
+ update_data: dict[str, object],
38
59
  ) -> PermissionDataManager:
39
- merged_data = {**dict(base_data), **update_data}
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) -> Dict[str, Any] | GeneralManagerData:
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) -> Any:
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
- # permission can be a combination of multiple permissions
17
- # separated by "&" (e.g. "isAuthenticated&isMatchingKeyAccount")
18
- # this means that all sub_permissions must be true
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:
@@ -1 +1,27 @@
1
- from .rule import Rule
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())
@@ -1,4 +1,4 @@
1
- # generalManager/src/rule/handlers.py
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
- """Schnittstelle für Rule-Handler."""
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
- Erstelle Fehlermeldungen für den Vergleichs- oder Funktionsaufruf.
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
- Handler für Funktionsaufrufe wie len(), max(), min(), sum().
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
- Aggregiere die Werte und erstelle eine Fehlermeldung.
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)
@@ -1,4 +1,4 @@
1
- # generalManager/src/rule/rule.py
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
- Rule kapselt eine boolsche Bedingungsfunktion und erzeugt bei Fehlschlag
37
- automatisierte oder benutzerdefinierte Fehlermeldungen auf Basis des AST.
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) Quelltext holen, Decorators abschneiden, Dedent
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 parsen & Elternverweise setzen
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) Variablen extrahieren
72
+ # 3) Extract referenced variables
76
73
  self._variables = self._extract_variables()
77
74
 
78
- # 4) Handler registrieren
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
- Führt die Regel aus. Gibt False bei Fehlschlag, True bei Erfolg
115
- und None, falls ignore_if_none aktiv ist und eine Variable None war.
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
- Stellt sicher, dass in der custom_error_message alle Variablen
129
- aus self._variables verwendet werden.
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
- Liefert ein Dict variable→message, oder None, wenn kein Fehler.
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
- # Validierung und Ersetzen der Template-Platzhalter
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
- # Spezial-Handler?
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-Fehler
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
- # kein Vergleichpauschale Meldung
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 gibt einen str zurück
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
- from .noneToZero import noneToZero
2
- from .argsToKwargs import args_to_kwargs
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 Any, Iterable
1
+ from typing import Iterable, Mapping
2
2
 
3
3
 
4
4
  def args_to_kwargs(
5
- args: tuple[Any, ...], keys: Iterable[Any], existing_kwargs: dict | None = None
6
- ):
5
+ args: tuple[object, ...],
6
+ keys: Iterable[str],
7
+ existing_kwargs: Mapping[str, object] | None = None,
8
+ ) -> dict[str, object]:
7
9
  """
8
- Converts *args into **kwargs and combines them with existing **kwargs.
10
+ Convert positional arguments to keyword arguments and merge them into an existing mapping.
9
11
 
10
- :param args: Tuple of positional arguments (e.g., *args).
11
- :param keys: List of keys to associate with the arguments.
12
- :param existing_kwargs: Optional dictionary of already existing key-value mappings.
13
- :return: Dictionary of combined **kwargs.
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: