GeneralManager 0.17.0__py3-none-any.whl → 0.18.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.

Potentially problematic release.


This version of GeneralManager might be problematic. Click here for more details.

Files changed (67) hide show
  1. general_manager/__init__.py +11 -1
  2. general_manager/_types/api.py +0 -1
  3. general_manager/_types/bucket.py +0 -1
  4. general_manager/_types/cache.py +0 -1
  5. general_manager/_types/factory.py +0 -1
  6. general_manager/_types/general_manager.py +0 -1
  7. general_manager/_types/interface.py +0 -1
  8. general_manager/_types/manager.py +0 -1
  9. general_manager/_types/measurement.py +0 -1
  10. general_manager/_types/permission.py +0 -1
  11. general_manager/_types/rule.py +0 -1
  12. general_manager/_types/utils.py +0 -1
  13. general_manager/api/__init__.py +13 -1
  14. general_manager/api/graphql.py +356 -221
  15. general_manager/api/graphql_subscription_consumer.py +81 -78
  16. general_manager/api/mutation.py +85 -23
  17. general_manager/api/property.py +39 -13
  18. general_manager/apps.py +188 -47
  19. general_manager/bucket/__init__.py +10 -1
  20. general_manager/bucket/calculationBucket.py +155 -53
  21. general_manager/bucket/databaseBucket.py +157 -45
  22. general_manager/bucket/groupBucket.py +133 -44
  23. general_manager/cache/__init__.py +10 -1
  24. general_manager/cache/dependencyIndex.py +143 -45
  25. general_manager/cache/signals.py +9 -2
  26. general_manager/factory/__init__.py +10 -1
  27. general_manager/factory/autoFactory.py +55 -13
  28. general_manager/factory/factories.py +110 -40
  29. general_manager/factory/factoryMethods.py +122 -34
  30. general_manager/interface/__init__.py +10 -1
  31. general_manager/interface/baseInterface.py +129 -36
  32. general_manager/interface/calculationInterface.py +35 -18
  33. general_manager/interface/databaseBasedInterface.py +71 -45
  34. general_manager/interface/databaseInterface.py +96 -38
  35. general_manager/interface/models.py +5 -5
  36. general_manager/interface/readOnlyInterface.py +94 -20
  37. general_manager/manager/__init__.py +10 -1
  38. general_manager/manager/generalManager.py +25 -16
  39. general_manager/manager/groupManager.py +20 -6
  40. general_manager/manager/meta.py +84 -16
  41. general_manager/measurement/__init__.py +10 -1
  42. general_manager/measurement/measurement.py +289 -95
  43. general_manager/measurement/measurementField.py +42 -31
  44. general_manager/permission/__init__.py +10 -1
  45. general_manager/permission/basePermission.py +120 -38
  46. general_manager/permission/managerBasedPermission.py +72 -21
  47. general_manager/permission/mutationPermission.py +14 -9
  48. general_manager/permission/permissionChecks.py +14 -12
  49. general_manager/permission/permissionDataManager.py +24 -11
  50. general_manager/permission/utils.py +34 -6
  51. general_manager/public_api_registry.py +36 -10
  52. general_manager/rule/__init__.py +10 -1
  53. general_manager/rule/handler.py +133 -44
  54. general_manager/rule/rule.py +178 -39
  55. general_manager/utils/__init__.py +10 -1
  56. general_manager/utils/argsToKwargs.py +34 -9
  57. general_manager/utils/filterParser.py +22 -7
  58. general_manager/utils/formatString.py +1 -0
  59. general_manager/utils/pathMapping.py +23 -15
  60. general_manager/utils/public_api.py +33 -2
  61. general_manager/utils/testing.py +31 -33
  62. {generalmanager-0.17.0.dist-info → generalmanager-0.18.0.dist-info}/METADATA +2 -1
  63. generalmanager-0.18.0.dist-info/RECORD +77 -0
  64. {generalmanager-0.17.0.dist-info → generalmanager-0.18.0.dist-info}/licenses/LICENSE +1 -1
  65. generalmanager-0.17.0.dist-info/RECORD +0 -77
  66. {generalmanager-0.17.0.dist-info → generalmanager-0.18.0.dist-info}/WHEEL +0 -0
  67. {generalmanager-0.17.0.dist-info → generalmanager-0.18.0.dist-info}/top_level.txt +0 -0
@@ -35,32 +35,34 @@ class PermissionDict(TypedDict):
35
35
 
36
36
  permission_functions: dict[str, PermissionDict] = {
37
37
  "public": {
38
- "permission_method": lambda instance, user, config: True,
39
- "permission_filter": lambda user, config: None,
38
+ "permission_method": lambda _instance, _user, _config: True,
39
+ "permission_filter": lambda _user, _config: None,
40
40
  },
41
41
  "matches": {
42
- "permission_method": lambda instance, user, config: getattr(instance, config[0])
42
+ "permission_method": lambda instance, _user, config: getattr(
43
+ instance, config[0]
44
+ )
43
45
  == config[1],
44
- "permission_filter": lambda user, config: {"filter": {config[0]: config[1]}},
46
+ "permission_filter": lambda _user, config: {"filter": {config[0]: config[1]}},
45
47
  },
46
48
  "ends_with": {
47
- "permission_method": lambda instance, user, config: getattr(
49
+ "permission_method": lambda instance, _user, config: getattr(
48
50
  instance, config[0]
49
51
  ).endswith(config[1]),
50
- "permission_filter": lambda user, config: {
52
+ "permission_filter": lambda _user, config: {
51
53
  "filter": {f"{config[0]}__endswith": config[1]}
52
54
  },
53
55
  },
54
56
  "isAdmin": {
55
- "permission_method": lambda instance, user, config: user.is_staff,
56
- "permission_filter": lambda user, config: None,
57
+ "permission_method": lambda _instance, user, _config: user.is_staff,
58
+ "permission_filter": lambda _user, _config: None,
57
59
  },
58
60
  "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
+ "permission_method": lambda instance, user, _config: instance.creator == user, # type: ignore
62
+ "permission_filter": lambda user, _config: {"filter": {"creator_id": user.id}}, # type: ignore
61
63
  },
62
64
  "isAuthenticated": {
63
- "permission_method": lambda instance, user, config: user.is_authenticated,
64
- "permission_filter": lambda user, config: None,
65
+ "permission_method": lambda _instance, user, _config: user.is_authenticated,
66
+ "permission_filter": lambda _user, _config: None,
65
67
  },
66
68
  }
@@ -6,6 +6,21 @@ from django.contrib.auth.models import AbstractUser
6
6
 
7
7
  from general_manager.manager.generalManager import GeneralManager
8
8
 
9
+
10
+ class InvalidPermissionDataError(TypeError):
11
+ """Raised when the permission data manager receives unsupported input."""
12
+
13
+ def __init__(self) -> None:
14
+ """
15
+ Exception raised when a permission data input is not a dict or a GeneralManager instance.
16
+
17
+ The exception carries the message: "permission_data must be either a dict or an instance of GeneralManager."
18
+ """
19
+ super().__init__(
20
+ "permission_data must be either a dict or an instance of GeneralManager."
21
+ )
22
+
23
+
9
24
  GeneralManagerData = TypeVar("GeneralManagerData", bound=GeneralManager)
10
25
 
11
26
 
@@ -18,14 +33,14 @@ class PermissionDataManager(Generic[GeneralManagerData]):
18
33
  manager: Optional[type[GeneralManagerData]] = None,
19
34
  ) -> None:
20
35
  """
21
- Create a permission data manager wrapping either a dict or a manager instance.
36
+ Wrap a mapping or GeneralManager instance to expose permission-related fields via attribute access.
22
37
 
23
38
  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.
39
+ permission_data (dict[str, object] | GeneralManager): Either a dict mapping field names to values or a GeneralManager instance whose attributes provide field values.
40
+ manager (type[GeneralManager] | None): When `permission_data` is a dict, the manager class associated with that data; otherwise ignored.
26
41
 
27
42
  Raises:
28
- TypeError: If `permission_data` is neither a dict nor a `GeneralManager`.
43
+ InvalidPermissionDataError: If `permission_data` is neither a dict nor an instance of GeneralManager.
29
44
  """
30
45
  self.getData: Callable[[str], object]
31
46
  self._permission_data = permission_data
@@ -47,9 +62,7 @@ class PermissionDataManager(Generic[GeneralManagerData]):
47
62
  self.getData = dict_getter
48
63
  self._manager = manager
49
64
  else:
50
- raise TypeError(
51
- "permission_data must be either a dict or an instance of GeneralManager"
52
- )
65
+ raise InvalidPermissionDataError()
53
66
 
54
67
  @classmethod
55
68
  def forUpdate(
@@ -58,14 +71,14 @@ class PermissionDataManager(Generic[GeneralManagerData]):
58
71
  update_data: dict[str, object],
59
72
  ) -> PermissionDataManager:
60
73
  """
61
- Create a data manager that reflects a pending update to an existing manager.
74
+ Create a PermissionDataManager representing `base_data` with `update_data` applied.
62
75
 
63
76
  Parameters:
64
- base_data (GeneralManager): Existing manager instance.
65
- update_data (dict[str, Any]): Fields being updated.
77
+ base_data (GeneralManagerData): Existing manager instance whose data will serve as the base.
78
+ update_data (dict[str, object]): Fields to add or override on the base data.
66
79
 
67
80
  Returns:
68
- PermissionDataManager: Wrapper exposing merged data for permission checks.
81
+ PermissionDataManager: Wrapper exposing the merged data where keys in `update_data` override those from `base_data`.
69
82
  """
70
83
  merged_data: dict[str, object] = {**dict(base_data), **update_data}
71
84
  return cls(merged_data, base_data.__class__)
@@ -10,29 +10,57 @@ from general_manager.manager.generalManager import GeneralManager
10
10
  from general_manager.manager.meta import GeneralManagerMeta
11
11
 
12
12
 
13
+ class PermissionNotFoundError(ValueError):
14
+ """Raised when a referenced permission function is not registered."""
15
+
16
+ def __init__(self, permission: str) -> None:
17
+ """
18
+ Exception raised when a referenced permission function cannot be found.
19
+
20
+ Parameters:
21
+ permission (str): The permission identifier that was not found; used to format the exception message.
22
+ """
23
+ super().__init__(f"Permission {permission} not found.")
24
+
25
+
13
26
  def validatePermissionString(
14
27
  permission: str,
15
28
  data: PermissionDataManager | GeneralManager | GeneralManagerMeta,
16
29
  request_user: AbstractUser | AnonymousUser,
17
30
  ) -> bool:
18
31
  """
19
- Evaluate a compound permission expression joined by ``&`` operators.
32
+ Evaluate a compound permission expression joined by '&' operators.
20
33
 
21
34
  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.
35
+ permission (str): Permission expression where sub-permissions are joined with '&'. Individual sub-permissions may include ':'-separated configuration parts (for example, "isAuthenticated&admin:level").
36
+ data (PermissionDataManager | GeneralManager | GeneralManagerMeta): Object passed to each permission function.
37
+ request_user (AbstractUser | AnonymousUser): User for whom permissions are evaluated.
25
38
 
26
39
  Returns:
27
- bool: True if every sub-permission evaluates to True.
40
+ `true` if every sub-permission evaluates to True, `false` otherwise.
41
+
42
+ Raises:
43
+ PermissionNotFoundError: If a referenced permission function is not registered.
28
44
  """
29
45
 
30
46
  def _validateSinglePermission(
31
47
  permission: str,
32
48
  ) -> bool:
49
+ """
50
+ Evaluate a single sub-permission expression against the registered permission functions.
51
+
52
+ Parameters:
53
+ permission (str): A single permission fragment in the form "permission_name[:config...]" where parts after the first colon are passed as configuration.
54
+
55
+ Returns:
56
+ bool: `true` if the referenced permission function grants the permission, `false` otherwise.
57
+
58
+ Raises:
59
+ PermissionNotFoundError: If no registered permission function matches the `permission_name`.
60
+ """
33
61
  permission_function, *config = permission.split(":")
34
62
  if permission_function not in permission_functions:
35
- raise ValueError(f"Permission {permission} not found")
63
+ raise PermissionNotFoundError(permission)
36
64
 
37
65
  return permission_functions[permission_function]["permission_method"](
38
66
  data, request_user, config
@@ -18,9 +18,18 @@ GENERAL_MANAGER_EXPORTS: LazyExportMap = {
18
18
  "graphQlMutation": ("general_manager.api.mutation", "graphQlMutation"),
19
19
  "GeneralManager": ("general_manager.manager.generalManager", "GeneralManager"),
20
20
  "Input": ("general_manager.manager.input", "Input"),
21
- "CalculationInterface": ("general_manager.interface.calculationInterface", "CalculationInterface"),
22
- "DatabaseInterface": ("general_manager.interface.databaseInterface", "DatabaseInterface"),
23
- "ReadOnlyInterface": ("general_manager.interface.readOnlyInterface", "ReadOnlyInterface"),
21
+ "CalculationInterface": (
22
+ "general_manager.interface.calculationInterface",
23
+ "CalculationInterface",
24
+ ),
25
+ "DatabaseInterface": (
26
+ "general_manager.interface.databaseInterface",
27
+ "DatabaseInterface",
28
+ ),
29
+ "ReadOnlyInterface": (
30
+ "general_manager.interface.readOnlyInterface",
31
+ "ReadOnlyInterface",
32
+ ),
24
33
  "ManagerBasedPermission": (
25
34
  "general_manager.permission.managerBasedPermission",
26
35
  "ManagerBasedPermission",
@@ -45,7 +54,10 @@ FACTORY_EXPORTS: LazyExportMap = {
45
54
  "LazyProjectName": ("general_manager.factory.factoryMethods", "LazyProjectName"),
46
55
  "LazyDateToday": ("general_manager.factory.factoryMethods", "LazyDateToday"),
47
56
  "LazyDateBetween": ("general_manager.factory.factoryMethods", "LazyDateBetween"),
48
- "LazyDateTimeBetween": ("general_manager.factory.factoryMethods", "LazyDateTimeBetween"),
57
+ "LazyDateTimeBetween": (
58
+ "general_manager.factory.factoryMethods",
59
+ "LazyDateTimeBetween",
60
+ ),
49
61
  "LazyInteger": ("general_manager.factory.factoryMethods", "LazyInteger"),
50
62
  "LazyDecimal": ("general_manager.factory.factoryMethods", "LazyDecimal"),
51
63
  "LazyChoice": ("general_manager.factory.factoryMethods", "LazyChoice"),
@@ -54,7 +66,10 @@ FACTORY_EXPORTS: LazyExportMap = {
54
66
  "LazyUUID": ("general_manager.factory.factoryMethods", "LazyUUID"),
55
67
  "LazyFakerName": ("general_manager.factory.factoryMethods", "LazyFakerName"),
56
68
  "LazyFakerEmail": ("general_manager.factory.factoryMethods", "LazyFakerEmail"),
57
- "LazyFakerSentence": ("general_manager.factory.factoryMethods", "LazyFakerSentence"),
69
+ "LazyFakerSentence": (
70
+ "general_manager.factory.factoryMethods",
71
+ "LazyFakerSentence",
72
+ ),
58
73
  "LazyFakerAddress": ("general_manager.factory.factoryMethods", "LazyFakerAddress"),
59
74
  "LazyFakerUrl": ("general_manager.factory.factoryMethods", "LazyFakerUrl"),
60
75
  }
@@ -64,7 +79,10 @@ MEASUREMENT_EXPORTS: LazyExportMap = {
64
79
  "Measurement": ("general_manager.measurement.measurement", "Measurement"),
65
80
  "ureg": ("general_manager.measurement.measurement", "ureg"),
66
81
  "currency_units": ("general_manager.measurement.measurement", "currency_units"),
67
- "MeasurementField": ("general_manager.measurement.measurementField", "MeasurementField"),
82
+ "MeasurementField": (
83
+ "general_manager.measurement.measurementField",
84
+ "MeasurementField",
85
+ ),
68
86
  }
69
87
 
70
88
 
@@ -73,7 +91,10 @@ UTILS_EXPORTS: LazyExportMap = {
73
91
  "args_to_kwargs": ("general_manager.utils.argsToKwargs", "args_to_kwargs"),
74
92
  "make_cache_key": ("general_manager.utils.makeCacheKey", "make_cache_key"),
75
93
  "parse_filters": ("general_manager.utils.filterParser", "parse_filters"),
76
- "create_filter_function": ("general_manager.utils.filterParser", "create_filter_function"),
94
+ "create_filter_function": (
95
+ "general_manager.utils.filterParser",
96
+ "create_filter_function",
97
+ ),
77
98
  "snake_to_pascal": ("general_manager.utils.formatString", "snake_to_pascal"),
78
99
  "snake_to_camel": ("general_manager.utils.formatString", "snake_to_camel"),
79
100
  "pascal_to_snake": ("general_manager.utils.formatString", "pascal_to_snake"),
@@ -89,7 +110,10 @@ PERMISSION_EXPORTS: LazyExportMap = {
89
110
  "general_manager.permission.managerBasedPermission",
90
111
  "ManagerBasedPermission",
91
112
  ),
92
- "MutationPermission": ("general_manager.permission.mutationPermission", "MutationPermission"),
113
+ "MutationPermission": (
114
+ "general_manager.permission.mutationPermission",
115
+ "MutationPermission",
116
+ ),
93
117
  }
94
118
 
95
119
 
@@ -124,7 +148,10 @@ CACHE_EXPORTS: LazyExportMap = {
124
148
  BUCKET_EXPORTS: LazyExportMap = {
125
149
  "Bucket": ("general_manager.bucket.baseBucket", "Bucket"),
126
150
  "DatabaseBucket": ("general_manager.bucket.databaseBucket", "DatabaseBucket"),
127
- "CalculationBucket": ("general_manager.bucket.calculationBucket", "CalculationBucket"),
151
+ "CalculationBucket": (
152
+ "general_manager.bucket.calculationBucket",
153
+ "CalculationBucket",
154
+ ),
128
155
  "GroupBucket": ("general_manager.bucket.groupBucket", "GroupBucket"),
129
156
  }
130
157
 
@@ -157,4 +184,3 @@ EXPORT_REGISTRY: Mapping[str, LazyExportMap] = {
157
184
  "general_manager.manager": MANAGER_EXPORTS,
158
185
  "general_manager.rule": RULE_EXPORTS,
159
186
  }
160
-
@@ -12,10 +12,19 @@ __all__ = list(RULE_EXPORTS)
12
12
  _MODULE_MAP = RULE_EXPORTS
13
13
 
14
14
  if TYPE_CHECKING:
15
- from general_manager._types.rule import * # noqa: F401,F403
15
+ from general_manager._types.rule import * # noqa: F403
16
16
 
17
17
 
18
18
  def __getattr__(name: str) -> Any:
19
+ """
20
+ Dynamically resolve a missing module attribute using the module's export registry.
21
+
22
+ Parameters:
23
+ name (str): The attribute name being accessed on the module.
24
+
25
+ Returns:
26
+ The attribute value associated with `name` from the module's export registry, or a fallback value if the name cannot be resolved.
27
+ """
19
28
  return resolve_export(
20
29
  name,
21
30
  module_all=__all__,
@@ -9,6 +9,71 @@ if TYPE_CHECKING:
9
9
  from general_manager.rule.rule import Rule
10
10
 
11
11
 
12
+ class InvalidFunctionNodeError(ValueError):
13
+ """Raised when a rule handler receives an invalid AST node for its function."""
14
+
15
+ def __init__(self, function_name: str) -> None:
16
+ """
17
+ Initialize the exception for an invalid left-hand AST node used with a function call.
18
+
19
+ Parameters:
20
+ function_name (str): Name of the function with the invalid left node; stored on the exception and used to form the message "Invalid left node for {function_name}() function."
21
+ """
22
+ self.function_name = function_name
23
+ super().__init__(f"Invalid left node for {function_name}() function.")
24
+
25
+
26
+ class InvalidLenThresholdError(TypeError):
27
+ """Raised when len() comparisons use a non-numeric threshold."""
28
+
29
+ def __init__(self) -> None:
30
+ """
31
+ Exception raised when a len() threshold is not a numeric value.
32
+
33
+ Initializes the exception with a default message indicating invalid arguments for the len function.
34
+ """
35
+ super().__init__("Invalid arguments for len function.")
36
+
37
+
38
+ class InvalidNumericThresholdError(TypeError):
39
+ """Raised when aggregate handlers use a non-numeric threshold."""
40
+
41
+ def __init__(self, function_name: str) -> None:
42
+ """
43
+ Create an InvalidFunctionNodeError with a formatted message for the given function name.
44
+
45
+ Parameters:
46
+ function_name (str): Name of the aggregate function (e.g., "sum", "max", "min") included in the message.
47
+ """
48
+ super().__init__(f"Invalid arguments for {function_name} function.")
49
+
50
+
51
+ class NonEmptyIterableError(ValueError):
52
+ """Raised when an aggregate function expects a non-empty iterable."""
53
+
54
+ def __init__(self, function_name: str) -> None:
55
+ """
56
+ Initialize the error indicating an aggregate function received an empty iterable.
57
+
58
+ Parameters:
59
+ function_name (str): Name of the aggregate function (e.g., 'sum', 'max', 'len') used to build the error message.
60
+ """
61
+ super().__init__(f"{function_name} expects a non-empty iterable.")
62
+
63
+
64
+ class NumericIterableError(TypeError):
65
+ """Raised when an aggregate function expects numeric elements."""
66
+
67
+ def __init__(self, function_name: str) -> None:
68
+ """
69
+ Initialize the exception indicating that a function expected an iterable of numeric values.
70
+
71
+ Parameters:
72
+ function_name (str): Name of the function included in the exception message (message: "<function_name> expects an iterable of numbers.").
73
+ """
74
+ super().__init__(f"{function_name} expects an iterable of numbers.")
75
+
76
+
12
77
  class BaseRuleHandler(ABC):
13
78
  """Define the protocol for generating rule-specific error messages."""
14
79
 
@@ -55,6 +120,23 @@ class FunctionHandler(BaseRuleHandler, ABC):
55
120
  var_values: Dict[str, Optional[object]],
56
121
  rule: Rule,
57
122
  ) -> Dict[str, str]:
123
+ """
124
+ Handle a comparison AST node whose left side is a function call and delegate analysis to the subclass aggregate method.
125
+
126
+ Parameters:
127
+ node (ast.AST): The AST node to inspect; processing only occurs if it is an `ast.Compare`.
128
+ left (Optional[ast.expr]): Original left operand from the rule (may be unused by this handler).
129
+ right (Optional[ast.expr]): Original right operand from the rule (may be unused by this handler).
130
+ op (Optional[ast.cmpop]): Comparison operator from the rule; used to determine operator symbol.
131
+ var_values (Dict[str, Optional[object]]): Mapping of variable names to their resolved values for message construction.
132
+ rule (Rule): Rule instance used to obtain the textual operator symbol and any rule-specific context.
133
+
134
+ Returns:
135
+ Dict[str, str]: Mapping from variable name to generated error message; empty if `node` is not an `ast.Compare`.
136
+
137
+ Raises:
138
+ InvalidFunctionNodeError: If the compare left-hand side is not a function call with at least one argument.
139
+ """
58
140
  if not isinstance(node, ast.Compare):
59
141
  return {}
60
142
  compare_node = node
@@ -64,7 +146,7 @@ class FunctionHandler(BaseRuleHandler, ABC):
64
146
  op_symbol = rule._get_op_symbol(op)
65
147
 
66
148
  if not (isinstance(left_node, ast.Call) and left_node.args):
67
- raise ValueError(f"Invalid left node for {self.function_name}() function")
149
+ raise InvalidFunctionNodeError(self.function_name)
68
150
  arg_node = left_node.args[0]
69
151
 
70
152
  return self.aggregate(
@@ -112,20 +194,20 @@ class LenHandler(FunctionHandler):
112
194
  rule: Rule,
113
195
  ) -> Dict[str, str]:
114
196
  """
115
- Evaluate length-based limits and craft an error message when violated.
197
+ Produce an error message for a len() comparison against a numeric threshold.
116
198
 
117
199
  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.
200
+ arg_node (ast.expr): AST node representing the value passed to `len`.
201
+ right_node (ast.expr): AST node representing the comparison threshold.
202
+ op_symbol (str): Comparison operator symbol (e.g., ">", ">=", "<", "<=").
203
+ var_values (Dict[str, Optional[object]]): Runtime values for variables keyed by name.
204
+ rule (Rule): Rule helper used to resolve node names and evaluate nodes.
123
205
 
124
206
  Returns:
125
- dict[str, str]: Mapping containing a single error message keyed by variable name.
207
+ Dict[str, str]: A single-entry mapping from the variable name to a human-readable violation message.
126
208
 
127
209
  Raises:
128
- ValueError: If the argument is invalid or the threshold is not numeric.
210
+ InvalidLenThresholdError: If the comparison threshold cannot be interpreted as a number.
129
211
  """
130
212
 
131
213
  var_name = rule._get_node_name(arg_node)
@@ -134,7 +216,7 @@ class LenHandler(FunctionHandler):
134
216
  # --- Hier der Typ-Guard für right_value ---
135
217
  raw = rule._eval_node(right_node)
136
218
  if not isinstance(raw, (int, float)):
137
- raise ValueError("Invalid arguments for len function")
219
+ raise InvalidLenThresholdError()
138
220
  right_value: int | float = raw
139
221
 
140
222
  if op_symbol == ">":
@@ -171,35 +253,38 @@ class SumHandler(FunctionHandler):
171
253
  rule: Rule,
172
254
  ) -> Dict[str, str]:
173
255
  """
174
- Compute the sum of an iterable and compare it to the threshold.
256
+ Evaluate the sum of the iterable referenced by `arg_node` and produce a descriptive error message when the sum does not satisfy the comparison against the provided threshold.
175
257
 
176
258
  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.
259
+ arg_node (ast.expr): AST node identifying the iterable variable whose elements will be summed.
260
+ right_node (ast.expr): AST node representing the threshold value to compare against.
261
+ op_symbol (str): Comparison operator symbol (e.g., ">", "<=", "==") used to form the message.
262
+ var_values (Dict[str, Optional[object]]): Mapping of variable names to their evaluated runtime values.
263
+ rule (Rule): Rule helper used to resolve node names and evaluate AST nodes.
182
264
 
183
265
  Returns:
184
- dict[str, str]: Mapping containing a single error message keyed by variable name.
266
+ Dict[str, str]: A mapping containing a single entry: the variable name mapped to a human-readable message
267
+ describing how the computed sum relates to the threshold (too small, too large, or must equal).
185
268
 
186
269
  Raises:
187
- ValueError: If the argument is not a numeric iterable or the threshold is invalid.
270
+ NonEmptyIterableError: If the referenced iterable is missing or empty.
271
+ NumericIterableError: If the iterable contains non-numeric elements.
272
+ InvalidNumericThresholdError: If the threshold evaluated from `right_node` is not numeric.
188
273
  """
189
274
 
190
275
  # Name und Wert holen
191
276
  var_name = rule._get_node_name(arg_node)
192
277
  raw_iter = var_values.get(var_name)
193
- if not isinstance(raw_iter, (list, tuple)):
194
- raise ValueError("sum expects an iterable of numbers")
278
+ if not isinstance(raw_iter, (list, tuple)) or len(raw_iter) == 0:
279
+ raise NonEmptyIterableError("sum")
195
280
  if not all(isinstance(x, (int, float)) for x in raw_iter):
196
- raise ValueError("sum expects an iterable of numbers")
281
+ raise NumericIterableError("sum")
197
282
  total = sum(raw_iter)
198
283
 
199
284
  # Schwellenwert aus dem rechten Knoten
200
285
  raw = rule._eval_node(right_node)
201
286
  if not isinstance(raw, (int, float)):
202
- raise ValueError("Invalid arguments for sum function")
287
+ raise InvalidNumericThresholdError("sum")
203
288
  right_value = raw
204
289
 
205
290
  # Message formulieren
@@ -229,33 +314,35 @@ class MaxHandler(FunctionHandler):
229
314
  rule: Rule,
230
315
  ) -> Dict[str, str]:
231
316
  """
232
- Compare the maximum element of an iterable against the provided threshold.
317
+ Compare the maximum element of an iterable variable against a numeric threshold and produce an error message when the comparison fails.
233
318
 
234
319
  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.
320
+ arg_node (ast.expr): AST node identifying the iterable variable passed to `max`.
321
+ right_node (ast.expr): AST node that evaluates to the numeric threshold to compare against.
322
+ op_symbol (str): Comparison operator symbol (e.g., ">", ">=", "<", "<=", "==") used to shape the message.
323
+ var_values (Dict[str, Optional[object]]): Mapping of variable names to their evaluated runtime values.
324
+ rule (Rule): Rule helper used to resolve node names and evaluate `right_node`.
240
325
 
241
326
  Returns:
242
- dict[str, str]: Mapping containing a single error message keyed by variable name.
327
+ Dict[str, str]: Single-item mapping from the variable name to a human-readable message describing the max value and the threshold comparison.
243
328
 
244
329
  Raises:
245
- ValueError: If the iterable is empty, non-numeric, or the threshold is invalid.
330
+ NonEmptyIterableError: If the resolved iterable is not a non-empty list or tuple.
331
+ NumericIterableError: If any element of the iterable is not an int or float.
332
+ InvalidNumericThresholdError: If the evaluated threshold is not an int or float.
246
333
  """
247
334
 
248
335
  var_name = rule._get_node_name(arg_node)
249
336
  raw_iter = var_values.get(var_name)
250
337
  if not isinstance(raw_iter, (list, tuple)) or len(raw_iter) == 0:
251
- raise ValueError("max expects a non-empty iterable")
338
+ raise NonEmptyIterableError("max")
252
339
  if not all(isinstance(x, (int, float)) for x in raw_iter):
253
- raise ValueError("max expects an iterable of numbers")
340
+ raise NumericIterableError("max")
254
341
  current = max(raw_iter)
255
342
 
256
343
  raw = rule._eval_node(right_node)
257
344
  if not isinstance(raw, (int, float)):
258
- raise ValueError("Invalid arguments for max function")
345
+ raise InvalidNumericThresholdError("max")
259
346
  right_value = raw
260
347
 
261
348
  if op_symbol in (">", ">="):
@@ -280,33 +367,35 @@ class MinHandler(FunctionHandler):
280
367
  rule: Rule,
281
368
  ) -> Dict[str, str]:
282
369
  """
283
- Compare the minimum element of an iterable against the provided threshold.
370
+ Compare the minimum element of an iterable against a numeric threshold and produce an error message describing the violation.
284
371
 
285
372
  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.
373
+ arg_node (ast.expr): AST node for the iterable argument passed to `min`.
374
+ right_node (ast.expr): AST node for the threshold value to compare against.
375
+ op_symbol (str): Comparison operator symbol (e.g., ">", ">=", "<", "<=", "==").
376
+ var_values (Dict[str, Optional[object]]): Mapping of variable names to their evaluated values.
377
+ rule (Rule): Rule instance used to evaluate AST nodes and obtain variable names.
291
378
 
292
379
  Returns:
293
- dict[str, str]: Mapping containing a single error message keyed by variable name.
380
+ dict[str, str]: Mapping with the variable name as key and the generated error message as value.
294
381
 
295
382
  Raises:
296
- ValueError: If the iterable is empty, non-numeric, or the threshold is invalid.
383
+ NonEmptyIterableError: If the iterable is not a non-empty list/tuple.
384
+ NumericIterableError: If the iterable contains non-numeric elements.
385
+ InvalidNumericThresholdError: If the evaluated threshold is not numeric.
297
386
  """
298
387
 
299
388
  var_name = rule._get_node_name(arg_node)
300
389
  raw_iter = var_values.get(var_name)
301
390
  if not isinstance(raw_iter, (list, tuple)) or len(raw_iter) == 0:
302
- raise ValueError("min expects a non-empty iterable")
391
+ raise NonEmptyIterableError("min")
303
392
  if not all(isinstance(x, (int, float)) for x in raw_iter):
304
- raise ValueError("min expects an iterable of numbers")
393
+ raise NumericIterableError("min")
305
394
  current = min(raw_iter)
306
395
 
307
396
  raw = rule._eval_node(right_node)
308
397
  if not isinstance(raw, (int, float)):
309
- raise ValueError("Invalid arguments for min function")
398
+ raise InvalidNumericThresholdError("min")
310
399
  right_value = raw
311
400
 
312
401
  if op_symbol in (">", ">="):