GeneralManager 0.1.1__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. {generalmanager-0.1.1 → generalmanager-0.2.0}/GeneralManager.egg-info/PKG-INFO +1 -1
  2. {generalmanager-0.1.1 → generalmanager-0.2.0}/GeneralManager.egg-info/SOURCES.txt +4 -0
  3. {generalmanager-0.1.1 → generalmanager-0.2.0}/PKG-INFO +1 -1
  4. {generalmanager-0.1.1 → generalmanager-0.2.0}/pyproject.toml +1 -1
  5. generalmanager-0.2.0/src/general_manager/auxiliary/filterParser.py +136 -0
  6. generalmanager-0.2.0/src/general_manager/auxiliary/noneToZero.py +21 -0
  7. {generalmanager-0.1.1 → generalmanager-0.2.0}/src/general_manager/interface/databaseInterface.py +25 -2
  8. {generalmanager-0.1.1 → generalmanager-0.2.0}/src/general_manager/manager/input.py +29 -4
  9. generalmanager-0.2.0/src/general_manager/rule/handler.py +234 -0
  10. {generalmanager-0.1.1 → generalmanager-0.2.0}/src/general_manager/rule/rule.py +4 -2
  11. generalmanager-0.2.0/tests/test_filterParser.py +196 -0
  12. generalmanager-0.2.0/tests/test_input.py +165 -0
  13. generalmanager-0.2.0/tests/test_noneToZero.py +17 -0
  14. generalmanager-0.2.0/tests/test_rule_handler.py +435 -0
  15. generalmanager-0.1.1/src/general_manager/auxiliary/filterParser.py +0 -97
  16. generalmanager-0.1.1/src/general_manager/auxiliary/noneToZero.py +0 -12
  17. generalmanager-0.1.1/src/general_manager/rule/handler.py +0 -122
  18. {generalmanager-0.1.1 → generalmanager-0.2.0}/GeneralManager.egg-info/dependency_links.txt +0 -0
  19. {generalmanager-0.1.1 → generalmanager-0.2.0}/GeneralManager.egg-info/requires.txt +0 -0
  20. {generalmanager-0.1.1 → generalmanager-0.2.0}/GeneralManager.egg-info/top_level.txt +0 -0
  21. {generalmanager-0.1.1 → generalmanager-0.2.0}/LICENSE +0 -0
  22. {generalmanager-0.1.1 → generalmanager-0.2.0}/README.md +0 -0
  23. {generalmanager-0.1.1 → generalmanager-0.2.0}/setup.cfg +0 -0
  24. {generalmanager-0.1.1 → generalmanager-0.2.0}/src/general_manager/__init__.py +0 -0
  25. {generalmanager-0.1.1 → generalmanager-0.2.0}/src/general_manager/api/graphql.py +0 -0
  26. {generalmanager-0.1.1 → generalmanager-0.2.0}/src/general_manager/api/mutation.py +0 -0
  27. {generalmanager-0.1.1 → generalmanager-0.2.0}/src/general_manager/api/property.py +0 -0
  28. {generalmanager-0.1.1 → generalmanager-0.2.0}/src/general_manager/apps.py +0 -0
  29. {generalmanager-0.1.1 → generalmanager-0.2.0}/src/general_manager/auxiliary/__init__.py +0 -0
  30. {generalmanager-0.1.1 → generalmanager-0.2.0}/src/general_manager/auxiliary/argsToKwargs.py +0 -0
  31. {generalmanager-0.1.1 → generalmanager-0.2.0}/src/general_manager/cache/cacheDecorator.py +0 -0
  32. {generalmanager-0.1.1 → generalmanager-0.2.0}/src/general_manager/cache/cacheTracker.py +0 -0
  33. {generalmanager-0.1.1 → generalmanager-0.2.0}/src/general_manager/cache/dependencyIndex.py +0 -0
  34. {generalmanager-0.1.1 → generalmanager-0.2.0}/src/general_manager/cache/pathMapping.py +0 -0
  35. {generalmanager-0.1.1 → generalmanager-0.2.0}/src/general_manager/cache/signals.py +0 -0
  36. {generalmanager-0.1.1 → generalmanager-0.2.0}/src/general_manager/factory/__init__.py +0 -0
  37. {generalmanager-0.1.1 → generalmanager-0.2.0}/src/general_manager/factory/factories.py +0 -0
  38. {generalmanager-0.1.1 → generalmanager-0.2.0}/src/general_manager/factory/lazy_methods.py +0 -0
  39. {generalmanager-0.1.1 → generalmanager-0.2.0}/src/general_manager/interface/__init__.py +0 -0
  40. {generalmanager-0.1.1 → generalmanager-0.2.0}/src/general_manager/interface/baseInterface.py +0 -0
  41. {generalmanager-0.1.1 → generalmanager-0.2.0}/src/general_manager/interface/calculationInterface.py +0 -0
  42. {generalmanager-0.1.1 → generalmanager-0.2.0}/src/general_manager/manager/__init__.py +0 -0
  43. {generalmanager-0.1.1 → generalmanager-0.2.0}/src/general_manager/manager/generalManager.py +0 -0
  44. {generalmanager-0.1.1 → generalmanager-0.2.0}/src/general_manager/manager/groupManager.py +0 -0
  45. {generalmanager-0.1.1 → generalmanager-0.2.0}/src/general_manager/manager/meta.py +0 -0
  46. {generalmanager-0.1.1 → generalmanager-0.2.0}/src/general_manager/measurement/__init__.py +0 -0
  47. {generalmanager-0.1.1 → generalmanager-0.2.0}/src/general_manager/measurement/measurement.py +0 -0
  48. {generalmanager-0.1.1 → generalmanager-0.2.0}/src/general_manager/measurement/measurementField.py +0 -0
  49. {generalmanager-0.1.1 → generalmanager-0.2.0}/src/general_manager/permission/__init__.py +0 -0
  50. {generalmanager-0.1.1 → generalmanager-0.2.0}/src/general_manager/permission/basePermission.py +0 -0
  51. {generalmanager-0.1.1 → generalmanager-0.2.0}/src/general_manager/permission/fileBasedPermission.py +0 -0
  52. {generalmanager-0.1.1 → generalmanager-0.2.0}/src/general_manager/permission/managerBasedPermission.py +0 -0
  53. {generalmanager-0.1.1 → generalmanager-0.2.0}/src/general_manager/permission/permissionChecks.py +0 -0
  54. {generalmanager-0.1.1 → generalmanager-0.2.0}/src/general_manager/permission/permissionDataManager.py +0 -0
  55. {generalmanager-0.1.1 → generalmanager-0.2.0}/src/general_manager/rule/__init__.py +0 -0
  56. {generalmanager-0.1.1 → generalmanager-0.2.0}/tests/test_argsToKwargs.py +0 -0
  57. {generalmanager-0.1.1 → generalmanager-0.2.0}/tests/test_basePermission.py +0 -0
  58. {generalmanager-0.1.1 → generalmanager-0.2.0}/tests/test_graph_ql.py +0 -0
  59. {generalmanager-0.1.1 → generalmanager-0.2.0}/tests/test_managerBasedPermission.py +0 -0
  60. {generalmanager-0.1.1 → generalmanager-0.2.0}/tests/test_measurement.py +0 -0
  61. {generalmanager-0.1.1 → generalmanager-0.2.0}/tests/test_measurement_field.py +0 -0
  62. {generalmanager-0.1.1 → generalmanager-0.2.0}/tests/test_rules.py +0 -0
  63. {generalmanager-0.1.1 → generalmanager-0.2.0}/tests/test_settings.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GeneralManager
3
- Version: 0.1.1
3
+ Version: 0.2.0
4
4
  Summary: Kurzbeschreibung deines Pakets
5
5
  Author-email: Tim Kleindick <tkleindick@yahoo.de>
6
6
  License: Non-Commercial MIT License
@@ -46,9 +46,13 @@ src/general_manager/rule/handler.py
46
46
  src/general_manager/rule/rule.py
47
47
  tests/test_argsToKwargs.py
48
48
  tests/test_basePermission.py
49
+ tests/test_filterParser.py
49
50
  tests/test_graph_ql.py
51
+ tests/test_input.py
50
52
  tests/test_managerBasedPermission.py
51
53
  tests/test_measurement.py
52
54
  tests/test_measurement_field.py
55
+ tests/test_noneToZero.py
56
+ tests/test_rule_handler.py
53
57
  tests/test_rules.py
54
58
  tests/test_settings.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GeneralManager
3
- Version: 0.1.1
3
+ Version: 0.2.0
4
4
  Summary: Kurzbeschreibung deines Pakets
5
5
  Author-email: Tim Kleindick <tkleindick@yahoo.de>
6
6
  License: Non-Commercial MIT License
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
7
7
 
8
8
  [project]
9
9
  name = "GeneralManager"
10
- version = "0.1.1"
10
+ version = "0.2.0"
11
11
  description = "Kurzbeschreibung deines Pakets"
12
12
  readme = "README.md"
13
13
  authors = [
@@ -0,0 +1,136 @@
1
+ from __future__ import annotations
2
+ from typing import Any, Callable
3
+ from general_manager.manager.input import Input
4
+
5
+
6
+ def parse_filters(
7
+ filter_kwargs: dict[str, Any], possible_values: dict[str, Input]
8
+ ) -> dict[str, dict]:
9
+ """
10
+ Parses filter keyword arguments and constructs filter criteria for input fields.
11
+
12
+ For each filter key-value pair, determines the target field and lookup type, validates the field, and generates either filter keyword arguments or filter functions depending on the field's type. Returns a dictionary mapping field names to filter criteria, supporting both direct lookups and dynamic filter functions.
13
+
14
+ Args:
15
+ filter_kwargs: Dictionary of filter keys and their corresponding values.
16
+ possible_values: Mapping of field names to Input definitions used for validation and casting.
17
+
18
+ Returns:
19
+ A dictionary where each key is a field name and each value is a dictionary containing either 'filter_kwargs' for direct lookups or 'filter_funcs' for dynamic filtering.
20
+ """
21
+ from general_manager.manager.generalManager import GeneralManager
22
+
23
+ filters = {}
24
+ for kwarg, value in filter_kwargs.items():
25
+ parts = kwarg.split("__")
26
+ field_name = parts[0]
27
+ if field_name not in possible_values:
28
+ raise ValueError(f"Unknown input field '{field_name}' in filter")
29
+ input_field = possible_values[field_name]
30
+
31
+ lookup = "__".join(parts[1:]) if len(parts) > 1 else ""
32
+
33
+ if issubclass(input_field.type, GeneralManager):
34
+ # Sammle die Filter-Keyword-Argumente für das InputField
35
+ if lookup == "":
36
+ lookup = "id"
37
+ if not isinstance(value, GeneralManager):
38
+ value = input_field.cast(value)
39
+ value = getattr(value, "id", value)
40
+ filters.setdefault(field_name, {}).setdefault("filter_kwargs", {})[
41
+ lookup
42
+ ] = value
43
+ else:
44
+ # Erstelle Filterfunktionen für Nicht-Bucket-Typen
45
+ if isinstance(value, (list, tuple)) and not isinstance(
46
+ value, input_field.type
47
+ ):
48
+ casted_value = [input_field.cast(v) for v in value]
49
+ else:
50
+ casted_value = input_field.cast(value)
51
+ filter_func = create_filter_function(lookup, casted_value)
52
+ filters.setdefault(field_name, {}).setdefault("filter_funcs", []).append(
53
+ filter_func
54
+ )
55
+ return filters
56
+
57
+
58
+ def create_filter_function(lookup_str: str, value: Any) -> Callable[[Any], bool]:
59
+ """
60
+ Creates a filter function based on an attribute path and lookup operation.
61
+
62
+ The returned function checks whether an object's nested attribute(s) satisfy a specified comparison or matching operation against a given value.
63
+
64
+ Args:
65
+ lookup_str: Attribute path and lookup operation, separated by double underscores (e.g., "age__gte", "name__contains").
66
+ value: The value to compare against.
67
+
68
+ Returns:
69
+ A function that takes an object and returns True if the object's attribute(s) match the filter condition, otherwise False.
70
+ """
71
+ parts = lookup_str.split("__") if lookup_str else []
72
+ if parts and parts[-1] in [
73
+ "exact",
74
+ "lt",
75
+ "lte",
76
+ "gt",
77
+ "gte",
78
+ "contains",
79
+ "startswith",
80
+ "endswith",
81
+ "in",
82
+ ]:
83
+ lookup = parts[-1]
84
+ attr_path = parts[:-1]
85
+ else:
86
+ lookup = "exact"
87
+ attr_path = parts
88
+
89
+ def filter_func(x):
90
+ for attr in attr_path:
91
+ if hasattr(x, attr):
92
+ x = getattr(x, attr)
93
+ else:
94
+ return False
95
+ return apply_lookup(x, lookup, value)
96
+
97
+ return filter_func
98
+
99
+
100
+ def apply_lookup(value_to_check: Any, lookup: str, filter_value: Any) -> bool:
101
+ """
102
+ Evaluates whether a value satisfies a specified lookup condition against a filter value.
103
+
104
+ Supports comparison and string operations such as "exact", "lt", "lte", "gt", "gte", "contains", "startswith", "endswith", and "in". Returns False for unsupported lookups or if a TypeError occurs.
105
+
106
+ Args:
107
+ value_to_check: The value to be compared or checked.
108
+ lookup: The lookup operation to perform.
109
+ filter_value: The value to compare against.
110
+
111
+ Returns:
112
+ True if the lookup condition is satisfied; otherwise, False.
113
+ """
114
+ try:
115
+ if lookup == "exact":
116
+ return value_to_check == filter_value
117
+ elif lookup == "lt":
118
+ return value_to_check < filter_value
119
+ elif lookup == "lte":
120
+ return value_to_check <= filter_value
121
+ elif lookup == "gt":
122
+ return value_to_check > filter_value
123
+ elif lookup == "gte":
124
+ return value_to_check >= filter_value
125
+ elif lookup == "contains" and isinstance(value_to_check, str):
126
+ return filter_value in value_to_check
127
+ elif lookup == "startswith" and isinstance(value_to_check, str):
128
+ return value_to_check.startswith(filter_value)
129
+ elif lookup == "endswith" and isinstance(value_to_check, str):
130
+ return value_to_check.endswith(filter_value)
131
+ elif lookup == "in":
132
+ return value_to_check in filter_value
133
+ else:
134
+ return False
135
+ except TypeError as e:
136
+ return False
@@ -0,0 +1,21 @@
1
+ from typing import Optional, TypeVar, Literal
2
+ from general_manager.measurement import Measurement
3
+
4
+ NUMBERVALUE = TypeVar("NUMBERVALUE", int, float, Measurement)
5
+
6
+
7
+ def noneToZero(
8
+ value: Optional[NUMBERVALUE],
9
+ ) -> NUMBERVALUE | Literal[0]:
10
+ """
11
+ Returns zero if the input is None; otherwise, returns the original value.
12
+
13
+ Args:
14
+ value: An integer, float, or Measurement, or None.
15
+
16
+ Returns:
17
+ The original value if not None, otherwise 0.
18
+ """
19
+ if value is None:
20
+ return 0
21
+ return value
@@ -645,6 +645,15 @@ class DatabaseBucket(Bucket[GeneralManagerType]):
645
645
  def __mergeFilterDefinitions(
646
646
  self, basis: dict[str, list[Any]], **kwargs: Any
647
647
  ) -> dict[str, list[Any]]:
648
+ """
649
+ Merges filter definitions by combining existing filter criteria with additional keyword arguments.
650
+
651
+ Args:
652
+ basis: A dictionary mapping filter keys to lists of values.
653
+
654
+ Returns:
655
+ A dictionary where each key maps to a list of all values from both the original basis and the new keyword arguments.
656
+ """
648
657
  kwarg_filter: dict[str, list[Any]] = {}
649
658
  for key, value in basis.items():
650
659
  kwarg_filter[key] = value
@@ -654,7 +663,12 @@ class DatabaseBucket(Bucket[GeneralManagerType]):
654
663
  kwarg_filter[key].append(value)
655
664
  return kwarg_filter
656
665
 
657
- def filter(self, **kwargs: Any) -> DatabaseBucket:
666
+ def filter(self, **kwargs: Any) -> DatabaseBucket[GeneralManagerType]:
667
+ """
668
+ Returns a new bucket containing manager instances matching the given filter criteria.
669
+
670
+ Additional filter keyword arguments are merged with existing filters to refine the queryset.
671
+ """
658
672
  merged_filter = self.__mergeFilterDefinitions(self.filters, **kwargs)
659
673
  return self.__class__(
660
674
  self._data.filter(**kwargs),
@@ -663,7 +677,16 @@ class DatabaseBucket(Bucket[GeneralManagerType]):
663
677
  self.excludes,
664
678
  )
665
679
 
666
- def exclude(self, **kwargs: Any) -> DatabaseBucket:
680
+ def exclude(self, **kwargs: Any) -> DatabaseBucket[GeneralManagerType]:
681
+ """
682
+ Returns a new bucket excluding items matching the given filter criteria.
683
+
684
+ Args:
685
+ **kwargs: Field lookups to exclude from the queryset.
686
+
687
+ Returns:
688
+ A DatabaseBucket containing items not matching the specified filters.
689
+ """
667
690
  merged_exclude = self.__mergeFilterDefinitions(self.excludes, **kwargs)
668
691
  return self.__class__(
669
692
  self._data.exclude(**kwargs),
@@ -17,6 +17,14 @@ class Input(Generic[INPUT_TYPE]):
17
17
  possible_values: Optional[Callable | Iterable] = None,
18
18
  depends_on: Optional[List[str]] = None,
19
19
  ):
20
+ """
21
+ Initializes an Input instance with type information, possible values, and dependencies.
22
+
23
+ Args:
24
+ type: The expected type for the input value.
25
+ possible_values: An optional iterable or callable that defines allowed values.
26
+ depends_on: An optional list of dependency names. If not provided and possible_values is callable, dependencies are inferred from its parameters.
27
+ """
20
28
  self.type = type
21
29
  self.possible_values = possible_values
22
30
  self.is_manager = issubclass(type, GeneralManager)
@@ -33,16 +41,33 @@ class Input(Generic[INPUT_TYPE]):
33
41
  self.depends_on = []
34
42
 
35
43
  def cast(self, value: Any) -> Any:
44
+ """
45
+ Casts the input value to the type specified by this Input instance.
46
+
47
+ Handles special cases for date, datetime, GeneralManager subclasses, and Measurement types.
48
+ If the value is already of the target type, it is returned unchanged. Otherwise, attempts to
49
+ convert or construct the value as appropriate for the target type.
50
+
51
+ Args:
52
+ value: The value to be cast or converted.
53
+
54
+ Returns:
55
+ The value converted to the target type, or an instance of the target type.
56
+ """
57
+ if self.type == date:
58
+ if isinstance(value, datetime) and type(value) is not date:
59
+ return value.date()
60
+ return date.fromisoformat(value)
61
+ if self.type == datetime:
62
+ if isinstance(value, date):
63
+ return datetime.combine(value, datetime.min.time())
64
+ return datetime.fromisoformat(value)
36
65
  if isinstance(value, self.type):
37
66
  return value
38
67
  if issubclass(self.type, GeneralManager):
39
68
  if isinstance(value, dict):
40
69
  return self.type(**value) # type: ignore
41
70
  return self.type(id=value) # type: ignore
42
- if self.type == date:
43
- return date.fromisoformat(value)
44
- if self.type == datetime:
45
- return datetime.fromisoformat(value)
46
71
  if self.type == Measurement and isinstance(value, str):
47
72
  return Measurement.from_string(value)
48
73
  return self.type(value)
@@ -0,0 +1,234 @@
1
+ # generalManager/src/rule/handlers.py
2
+
3
+ from __future__ import annotations
4
+ import ast
5
+ from typing import Dict, Optional, TYPE_CHECKING
6
+ from abc import ABC, abstractmethod
7
+
8
+ if TYPE_CHECKING:
9
+ from general_manager.rule.rule import Rule
10
+
11
+
12
+ class BaseRuleHandler(ABC):
13
+ """Schnittstelle für Rule-Handler."""
14
+
15
+ function_name: str # ClassVar, der Name, unter dem dieser Handler registriert wird
16
+
17
+ @abstractmethod
18
+ def handle(
19
+ self,
20
+ node: ast.AST,
21
+ left: Optional[ast.expr],
22
+ right: Optional[ast.expr],
23
+ op: Optional[ast.cmpop],
24
+ var_values: Dict[str, Optional[object]],
25
+ rule: Rule,
26
+ ) -> Dict[str, str]:
27
+ """
28
+ Erstelle Fehlermeldungen für den Vergleichs- oder Funktionsaufruf.
29
+ """
30
+ pass
31
+
32
+
33
+ class FunctionHandler(BaseRuleHandler, ABC):
34
+ """
35
+ Handler für Funktionsaufrufe wie len(), max(), min(), sum().
36
+ """
37
+
38
+ def handle(
39
+ self,
40
+ node: ast.AST,
41
+ left: Optional[ast.expr],
42
+ right: Optional[ast.expr],
43
+ op: Optional[ast.cmpop],
44
+ var_values: Dict[str, Optional[object]],
45
+ rule: Rule,
46
+ ) -> Dict[str, str]:
47
+ if not isinstance(node, ast.Compare):
48
+ return {}
49
+ compare_node = node
50
+
51
+ left_node = compare_node.left
52
+ right_node = compare_node.comparators[0]
53
+ op_symbol = rule._get_op_symbol(op)
54
+
55
+ if not (isinstance(left_node, ast.Call) and left_node.args):
56
+ raise ValueError(f"Invalid left node for {self.function_name}() function")
57
+ arg_node = left_node.args[0]
58
+
59
+ return self.aggregate(
60
+ arg_node,
61
+ right_node,
62
+ op_symbol,
63
+ var_values,
64
+ rule,
65
+ )
66
+
67
+ @abstractmethod
68
+ def aggregate(
69
+ self,
70
+ arg_node: ast.expr,
71
+ right_node: ast.expr,
72
+ op_symbol: str,
73
+ var_values: Dict[str, Optional[object]],
74
+ rule: Rule,
75
+ ) -> Dict[str, str]:
76
+ """
77
+ Aggregiere die Werte und erstelle eine Fehlermeldung.
78
+ """
79
+ raise NotImplementedError("Subclasses should implement this method")
80
+
81
+
82
+ class LenHandler(FunctionHandler):
83
+ function_name = "len"
84
+
85
+ def aggregate(
86
+ self,
87
+ arg_node: ast.expr,
88
+ right_node: ast.expr,
89
+ op_symbol: str,
90
+ var_values: Dict[str, Optional[object]],
91
+ rule: Rule,
92
+ ) -> Dict[str, str]:
93
+
94
+ var_name = rule._get_node_name(arg_node)
95
+ var_value = var_values.get(var_name)
96
+
97
+ # --- Hier der Typ-Guard für right_value ---
98
+ raw = rule._eval_node(right_node)
99
+ if not isinstance(raw, (int, float)):
100
+ raise ValueError("Invalid arguments for len function")
101
+ right_value: int | float = raw
102
+
103
+ if op_symbol == ">":
104
+ threshold = right_value + 1
105
+ elif op_symbol == ">=":
106
+ threshold = right_value
107
+ elif op_symbol == "<":
108
+ threshold = right_value - 1
109
+ elif op_symbol == "<=":
110
+ threshold = right_value
111
+ else:
112
+ threshold = right_value
113
+
114
+ # Fehlermeldung formulieren
115
+ if op_symbol in (">", ">="):
116
+ msg = f"[{var_name}] ({var_value}) is too short (min length {threshold})!"
117
+ elif op_symbol in ("<", "<="):
118
+ msg = f"[{var_name}] ({var_value}) is too long (max length {threshold})!"
119
+ else:
120
+ msg = f"[{var_name}] ({var_value}) must have a length of {right_value}!"
121
+
122
+ return {var_name: msg}
123
+
124
+
125
+ class SumHandler(FunctionHandler):
126
+ function_name = "sum"
127
+
128
+ def aggregate(
129
+ self,
130
+ arg_node: ast.expr,
131
+ right_node: ast.expr,
132
+ op_symbol: str,
133
+ var_values: Dict[str, Optional[object]],
134
+ rule: Rule,
135
+ ) -> Dict[str, str]:
136
+
137
+ # Name und Wert holen
138
+ var_name = rule._get_node_name(arg_node)
139
+ raw_iter = var_values.get(var_name)
140
+ if not isinstance(raw_iter, (list, tuple)):
141
+ raise ValueError("sum expects an iterable of numbers")
142
+ if not all(isinstance(x, (int, float)) for x in raw_iter):
143
+ raise ValueError("sum expects an iterable of numbers")
144
+ total = sum(raw_iter)
145
+
146
+ # Schwellenwert aus dem rechten Knoten
147
+ raw = rule._eval_node(right_node)
148
+ if not isinstance(raw, (int, float)):
149
+ raise ValueError("Invalid arguments for sum function")
150
+ right_value = raw
151
+
152
+ # Message formulieren
153
+ if op_symbol in (">", ">="):
154
+ msg = (
155
+ f"[{var_name}] (sum={total}) is too small ({op_symbol} {right_value})!"
156
+ )
157
+ elif op_symbol in ("<", "<="):
158
+ msg = (
159
+ f"[{var_name}] (sum={total}) is too large ({op_symbol} {right_value})!"
160
+ )
161
+ else:
162
+ msg = f"[{var_name}] (sum={total}) must be {right_value}!"
163
+
164
+ return {var_name: msg}
165
+
166
+
167
+ class MaxHandler(FunctionHandler):
168
+ function_name = "max"
169
+
170
+ def aggregate(
171
+ self,
172
+ arg_node: ast.expr,
173
+ right_node: ast.expr,
174
+ op_symbol: str,
175
+ var_values: Dict[str, Optional[object]],
176
+ rule: Rule,
177
+ ) -> Dict[str, str]:
178
+
179
+ var_name = rule._get_node_name(arg_node)
180
+ raw_iter = var_values.get(var_name)
181
+ if not isinstance(raw_iter, (list, tuple)) or len(raw_iter) == 0:
182
+ raise ValueError("max expects a non-empty iterable")
183
+ if not all(isinstance(x, (int, float)) for x in raw_iter):
184
+ raise ValueError("max expects an iterable of numbers")
185
+ current = max(raw_iter)
186
+
187
+ raw = rule._eval_node(right_node)
188
+ if not isinstance(raw, (int, float)):
189
+ raise ValueError("Invalid arguments for max function")
190
+ right_value = raw
191
+
192
+ if op_symbol in (">", ">="):
193
+ msg = f"[{var_name}] (max={current}) is too small ({op_symbol} {right_value})!"
194
+ elif op_symbol in ("<", "<="):
195
+ msg = f"[{var_name}] (max={current}) is too large ({op_symbol} {right_value})!"
196
+ else:
197
+ msg = f"[{var_name}] (max={current}) must be {right_value}!"
198
+
199
+ return {var_name: msg}
200
+
201
+
202
+ class MinHandler(FunctionHandler):
203
+ function_name = "min"
204
+
205
+ def aggregate(
206
+ self,
207
+ arg_node: ast.expr,
208
+ right_node: ast.expr,
209
+ op_symbol: str,
210
+ var_values: Dict[str, Optional[object]],
211
+ rule: Rule,
212
+ ) -> Dict[str, str]:
213
+
214
+ var_name = rule._get_node_name(arg_node)
215
+ raw_iter = var_values.get(var_name)
216
+ if not isinstance(raw_iter, (list, tuple)) or len(raw_iter) == 0:
217
+ raise ValueError("min expects a non-empty iterable")
218
+ if not all(isinstance(x, (int, float)) for x in raw_iter):
219
+ raise ValueError("min expects an iterable of numbers")
220
+ current = min(raw_iter)
221
+
222
+ raw = rule._eval_node(right_node)
223
+ if not isinstance(raw, (int, float)):
224
+ raise ValueError("Invalid arguments for min function")
225
+ right_value = raw
226
+
227
+ if op_symbol in (">", ">="):
228
+ msg = f"[{var_name}] (min={current}) is too small ({op_symbol} {right_value})!"
229
+ elif op_symbol in ("<", "<="):
230
+ msg = f"[{var_name}] (min={current}) is too large ({op_symbol} {right_value})!"
231
+ else:
232
+ msg = f"[{var_name}] (min={current}) must be {right_value}!"
233
+
234
+ return {var_name: msg}
@@ -22,7 +22,9 @@ from django.utils.module_loading import import_string
22
22
  from general_manager.rule.handler import (
23
23
  BaseRuleHandler,
24
24
  LenHandler,
25
- IntersectionCheckHandler,
25
+ MaxHandler,
26
+ MinHandler,
27
+ SumHandler,
26
28
  )
27
29
  from general_manager.manager.generalManager import GeneralManager
28
30
 
@@ -75,7 +77,7 @@ class Rule(Generic[GeneralManagerType]):
75
77
 
76
78
  # 4) Handler registrieren
77
79
  self._handlers = {} # type: Dict[str, BaseRuleHandler]
78
- for cls in (LenHandler, IntersectionCheckHandler):
80
+ for cls in (LenHandler, MaxHandler, MinHandler, SumHandler):
79
81
  inst = cls()
80
82
  self._handlers[inst.function_name] = inst
81
83
  for path in getattr(settings, "RULE_HANDLERS", []):