GeneralManager 0.1.2__py3-none-any.whl → 0.3.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.
@@ -1,9 +1,23 @@
1
- from typing import Any, Callable, List
1
+ from __future__ import annotations
2
+ from typing import Any, Callable
3
+ from general_manager.manager.input import Input
2
4
 
3
5
 
4
6
  def parse_filters(
5
- filter_kwargs: dict[str, Any], possible_values: dict[str, Any]
7
+ filter_kwargs: dict[str, Any], possible_values: dict[str, Input]
6
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
+ """
7
21
  from general_manager.manager.generalManager import GeneralManager
8
22
 
9
23
  filters = {}
@@ -42,6 +56,18 @@ def parse_filters(
42
56
 
43
57
 
44
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
+ """
45
71
  parts = lookup_str.split("__") if lookup_str else []
46
72
  if parts and parts[-1] in [
47
73
  "exact",
@@ -71,26 +97,39 @@ def create_filter_function(lookup_str: str, value: Any) -> Callable[[Any], bool]
71
97
  return filter_func
72
98
 
73
99
 
74
- def apply_lookup(x: Any, lookup: str, value: Any) -> bool:
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
+ """
75
114
  try:
76
115
  if lookup == "exact":
77
- return x == value
116
+ return value_to_check == filter_value
78
117
  elif lookup == "lt":
79
- return x < value
118
+ return value_to_check < filter_value
80
119
  elif lookup == "lte":
81
- return x <= value
120
+ return value_to_check <= filter_value
82
121
  elif lookup == "gt":
83
- return x > value
122
+ return value_to_check > filter_value
84
123
  elif lookup == "gte":
85
- return x >= value
86
- elif lookup == "contains" and isinstance(x, str):
87
- return value in x
88
- elif lookup == "startswith" and isinstance(x, str):
89
- return x.startswith(value)
90
- elif lookup == "endswith" and isinstance(x, str):
91
- return x.endswith(value)
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)
92
131
  elif lookup == "in":
93
- return x in value
132
+ return value_to_check in filter_value
94
133
  else:
95
134
  return False
96
135
  except TypeError as e:
@@ -0,0 +1,19 @@
1
+ from datetime import datetime, date, time
2
+ import json
3
+ from general_manager.manager.generalManager import GeneralManager
4
+
5
+
6
+ class CustomJSONEncoder(json.JSONEncoder):
7
+ def default(self, o):
8
+
9
+ # Serialize datetime objects as ISO strings
10
+ if isinstance(o, (datetime, date, time)):
11
+ return o.isoformat()
12
+ # Handle GeneralManager instances
13
+ if isinstance(o, GeneralManager):
14
+ return f"{o.__class__.__name__}(**{o.identification})"
15
+ try:
16
+ return super().default(o)
17
+ except TypeError:
18
+ # Fallback: convert all other objects to str
19
+ return str(o)
@@ -0,0 +1,30 @@
1
+ import inspect
2
+ import json
3
+ from general_manager.auxiliary.jsonEncoder import CustomJSONEncoder
4
+ from hashlib import sha256
5
+
6
+
7
+ def make_cache_key(func, args, kwargs):
8
+ """
9
+ Generate a deterministic cache key for a function call.
10
+
11
+ Args:
12
+ func: The function being called
13
+ args: Positional arguments to the function
14
+ kwargs: Keyword arguments to the function
15
+
16
+ Returns:
17
+ str: A hexadecimal SHA-256 hash that uniquely identifies this function call
18
+ """
19
+ sig = inspect.signature(func)
20
+ bound = sig.bind_partial(*args, **kwargs)
21
+ bound.apply_defaults()
22
+ payload = {
23
+ "module": func.__module__,
24
+ "name": func.__name__,
25
+ "args": bound.arguments,
26
+ }
27
+ raw = json.dumps(
28
+ payload, sort_keys=True, default=str, cls=CustomJSONEncoder
29
+ ).encode()
30
+ return sha256(raw, usedforsecurity=False).hexdigest()
@@ -7,6 +7,15 @@ NUMBERVALUE = TypeVar("NUMBERVALUE", int, float, Measurement)
7
7
  def noneToZero(
8
8
  value: Optional[NUMBERVALUE],
9
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
+ """
10
19
  if value is None:
11
20
  return 0
12
21
  return value
@@ -1,70 +1,43 @@
1
- from typing import Callable, Optional
1
+ from typing import Any, Callable, Optional, Protocol, Set
2
2
  from functools import wraps
3
3
  from django.core.cache import cache as django_cache
4
- from hashlib import sha256
5
4
  from general_manager.cache.cacheTracker import DependencyTracker
6
- from general_manager.cache.dependencyIndex import record_dependencies, get_full_index
5
+ from general_manager.cache.dependencyIndex import record_dependencies, Dependency
6
+ from general_manager.cache.modelDependencyCollector import ModelDependencyCollector
7
+ from general_manager.auxiliary.makeCacheKey import make_cache_key
7
8
 
8
9
 
9
- def cached(timeout: Optional[int] = None) -> Callable:
10
- """
11
- Decorator to cache the result of a function for a specified timeout.
12
- If no timeout is provided, the cache will expire when a dependency is invalidated.
13
- """
10
+ class CacheBackend(Protocol):
11
+ def get(self, key: str) -> Any: ...
12
+ def set(self, key: str, value: Any, timeout: Optional[int] = None) -> None: ...
14
13
 
15
- def decorator(func: Callable) -> Callable:
16
14
 
15
+ RecordFn = Callable[[str, Set[Dependency]], None]
16
+
17
+
18
+ def cached(
19
+ timeout: Optional[int] = None,
20
+ cache_backend: CacheBackend = django_cache,
21
+ record_fn: RecordFn = record_dependencies,
22
+ ) -> Callable:
23
+ def decorator(func: Callable) -> Callable:
17
24
  @wraps(func)
18
25
  def wrapper(*args, **kwargs):
19
- from general_manager.manager.generalManager import GeneralManager, Bucket
26
+ key = make_cache_key(func, args, kwargs)
27
+
28
+ result = cache_backend.get(key)
29
+ if result is not None:
30
+ return result
20
31
 
21
- django_cache_key = sha256(
22
- f"{func.__module__}.{func.__name__}:{args}:{kwargs}".encode(),
23
- usedforsecurity=False,
24
- ).hexdigest()
25
- cached_result = django_cache.get(django_cache_key)
26
- if cached_result is not None:
27
- return cached_result
28
- # Dependency Tracking aktivieren
29
32
  with DependencyTracker() as dependencies:
30
33
  result = func(*args, **kwargs)
34
+ ModelDependencyCollector.addArgs(dependencies, args, kwargs)
31
35
 
32
- def collect_model_dependencies(obj):
33
- """Rekursiv Django-Model-Instanzen im Objekt finden."""
34
- if isinstance(obj, GeneralManager):
35
- yield (
36
- obj.__class__.__name__,
37
- "identification",
38
- f"{obj.identification}",
39
- )
40
- elif isinstance(obj, Bucket):
41
- yield (obj._manager_class.__name__, "filter", f"{obj.filters}")
42
- yield (obj._manager_class.__name__, "exclude", f"{obj.excludes}")
43
- elif isinstance(obj, dict):
44
- for v in obj.values():
45
- yield from collect_model_dependencies(v)
46
- elif isinstance(obj, (list, tuple, set)):
47
- for item in obj:
48
- yield from collect_model_dependencies(item)
49
-
50
- if args and isinstance(args[0], GeneralManager):
51
- self = args[0]
52
- for attr_val in self.__dict__.values():
53
- for dependency_tuple in collect_model_dependencies(attr_val):
54
- dependencies.add(dependency_tuple)
55
-
56
- for dependency_tuple in collect_model_dependencies(args):
57
- dependencies.add(dependency_tuple)
58
- for dependency_tuple in collect_model_dependencies(kwargs):
59
- dependencies.add(dependency_tuple)
36
+ cache_backend.set(key, result, timeout)
60
37
 
61
- django_cache.set(django_cache_key, result, timeout)
38
+ if dependencies and timeout is None:
39
+ record_fn(key, dependencies)
62
40
 
63
- if dependencies and not timeout:
64
- record_dependencies(
65
- django_cache_key,
66
- dependencies,
67
- )
68
41
  return result
69
42
 
70
43
  return wrapper
@@ -1,7 +1,9 @@
1
1
  import threading
2
- from general_manager.cache.dependencyIndex import general_manager_name
3
- from typing import Any, Literal
4
-
2
+ from general_manager.cache.dependencyIndex import (
3
+ general_manager_name,
4
+ Dependency,
5
+ filter_type,
6
+ )
5
7
 
6
8
  # Thread-lokale Variable zur Speicherung der Abhängigkeiten
7
9
  _dependency_storage = threading.local()
@@ -10,24 +12,23 @@ _dependency_storage = threading.local()
10
12
  class DependencyTracker:
11
13
  def __enter__(
12
14
  self,
13
- ) -> set[
14
- tuple[general_manager_name, Literal["filter", "exclude", "identification"], str]
15
- ]:
15
+ ) -> set[Dependency]:
16
16
  _dependency_storage.dependencies = set()
17
17
  return _dependency_storage.dependencies
18
18
 
19
19
  def __exit__(self, exc_type, exc_val, exc_tb):
20
- # Optional: Aufräumen oder weitere Verarbeitung
21
- pass
22
-
23
-
24
- def addDependency(class_name: str, operation: str, identifier: str) -> None:
25
- """
26
- Adds a dependency to the dependency storage.
27
- """
28
- if hasattr(_dependency_storage, "dependencies"):
29
- dependencies: set[
30
- tuple[general_manager_name, Literal["filter", "exclude", "id"], str]
31
- ] = _dependency_storage.dependencies
32
-
33
- _dependency_storage.dependencies.add((class_name, operation, identifier))
20
+ if hasattr(_dependency_storage, "dependencies"):
21
+ del _dependency_storage.dependencies
22
+
23
+ @staticmethod
24
+ def track(
25
+ class_name: general_manager_name,
26
+ operation: filter_type,
27
+ identifier: str,
28
+ ) -> None:
29
+ """
30
+ Adds a dependency to the dependency storage.
31
+ """
32
+ if hasattr(_dependency_storage, "dependencies"):
33
+ dependencies: set[Dependency] = _dependency_storage.dependencies
34
+ dependencies.add((class_name, operation, identifier))
@@ -6,7 +6,7 @@ import re
6
6
  from django.core.cache import cache
7
7
  from general_manager.cache.signals import post_data_change, pre_data_change
8
8
  from django.dispatch import receiver
9
- from typing import Literal, Any, Iterable, TYPE_CHECKING, Type
9
+ from typing import Literal, Any, Iterable, TYPE_CHECKING, Type, Tuple
10
10
 
11
11
  if TYPE_CHECKING:
12
12
  from general_manager.manager.generalManager import GeneralManager
@@ -24,6 +24,8 @@ type dependency_index = dict[
24
24
  ],
25
25
  ]
26
26
 
27
+ type filter_type = Literal["filter", "exclude", "identification"]
28
+ type Dependency = Tuple[general_manager_name, filter_type, str]
27
29
 
28
30
  # -----------------------------------------------------------------------------
29
31
  # CONFIG
@@ -0,0 +1,51 @@
1
+ from typing import Generator
2
+ from general_manager.manager.generalManager import GeneralManager, Bucket
3
+ from general_manager.cache.dependencyIndex import (
4
+ general_manager_name,
5
+ Dependency,
6
+ filter_type,
7
+ )
8
+
9
+
10
+ class ModelDependencyCollector:
11
+
12
+ def __init__(self, dependencies: set[Dependency]):
13
+ """
14
+ Initialize the ModelDependencyCollector with a set of dependencies.
15
+ """
16
+ self.dependencies = dependencies
17
+
18
+ @staticmethod
19
+ def collect(obj) -> Generator[tuple[general_manager_name, filter_type, str]]:
20
+ """Recursively find Django model instances in the object."""
21
+ if isinstance(obj, GeneralManager):
22
+ yield (
23
+ obj.__class__.__name__,
24
+ "identification",
25
+ f"{obj.identification}",
26
+ )
27
+ elif isinstance(obj, Bucket):
28
+ yield (obj._manager_class.__name__, "filter", f"{obj.filters}")
29
+ yield (obj._manager_class.__name__, "exclude", f"{obj.excludes}")
30
+ elif isinstance(obj, dict):
31
+ for v in obj.values():
32
+ yield from ModelDependencyCollector.collect(v)
33
+ elif isinstance(obj, (list, tuple, set)):
34
+ for item in obj:
35
+ yield from ModelDependencyCollector.collect(item)
36
+
37
+ @staticmethod
38
+ def addArgs(dependencies: set[Dependency], args: tuple, kwargs: dict) -> None:
39
+ """
40
+ Add dependencies to the dependency set.
41
+ """
42
+ if args and isinstance(args[0], GeneralManager):
43
+ inner_self = args[0]
44
+ for attr_val in inner_self.__dict__.values():
45
+ for dependency_tuple in ModelDependencyCollector.collect(attr_val):
46
+ dependencies.add(dependency_tuple)
47
+
48
+ for dependency_tuple in ModelDependencyCollector.collect(args):
49
+ dependencies.add(dependency_tuple)
50
+ for dependency_tuple in ModelDependencyCollector.collect(kwargs):
51
+ dependencies.add(dependency_tuple)
@@ -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
@@ -655,6 +664,11 @@ class DatabaseBucket(Bucket[GeneralManagerType]):
655
664
  return kwarg_filter
656
665
 
657
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),
@@ -664,6 +678,15 @@ class DatabaseBucket(Bucket[GeneralManagerType]):
664
678
  )
665
679
 
666
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),
@@ -7,7 +7,7 @@ from general_manager.interface.baseInterface import (
7
7
  GeneralManagerType,
8
8
  )
9
9
  from general_manager.api.property import GraphQLProperty
10
- from general_manager.cache.cacheTracker import addDependency
10
+ from general_manager.cache.cacheTracker import DependencyTracker
11
11
  from general_manager.cache.signals import dataChange
12
12
 
13
13
  if TYPE_CHECKING:
@@ -21,7 +21,9 @@ class GeneralManager(Generic[GeneralManagerType], metaclass=GeneralManagerMeta):
21
21
  def __init__(self, *args: Any, **kwargs: Any):
22
22
  self._interface = self.Interface(*args, **kwargs)
23
23
  self.__id: dict[str, Any] = self._interface.identification
24
- addDependency(self.__class__.__name__, "identification", f"{self.__id}")
24
+ DependencyTracker.track(
25
+ self.__class__.__name__, "identification", f"{self.__id}"
26
+ )
25
27
 
26
28
  def __str__(self):
27
29
  return f"{self.__class__.__name__}(**{self.__id})"
@@ -108,12 +110,16 @@ class GeneralManager(Generic[GeneralManagerType], metaclass=GeneralManagerMeta):
108
110
 
109
111
  @classmethod
110
112
  def filter(cls, **kwargs: Any) -> Bucket[GeneralManagerType]:
111
- addDependency(cls.__name__, "filter", f"{cls.__parse_identification(kwargs)}")
113
+ DependencyTracker.track(
114
+ cls.__name__, "filter", f"{cls.__parse_identification(kwargs)}"
115
+ )
112
116
  return cls.Interface.filter(**kwargs)
113
117
 
114
118
  @classmethod
115
119
  def exclude(cls, **kwargs: Any) -> Bucket[GeneralManagerType]:
116
- addDependency(cls.__name__, "exclude", f"{cls.__parse_identification(kwargs)}")
120
+ DependencyTracker.track(
121
+ cls.__name__, "exclude", f"{cls.__parse_identification(kwargs)}"
122
+ )
117
123
  return cls.Interface.exclude(**kwargs)
118
124
 
119
125
  @classmethod
@@ -2,19 +2,19 @@
2
2
 
3
3
  from __future__ import annotations
4
4
  import ast
5
- from typing import Dict, Optional, TYPE_CHECKING, cast
5
+ from typing import Dict, Optional, TYPE_CHECKING
6
+ from abc import ABC, abstractmethod
6
7
 
7
8
  if TYPE_CHECKING:
8
- # Forward-Reference auf Rule mit beliebigem Generic-Parameter
9
9
  from general_manager.rule.rule import Rule
10
- from general_manager.manager import GeneralManager
11
10
 
12
11
 
13
- class BaseRuleHandler:
12
+ class BaseRuleHandler(ABC):
14
13
  """Schnittstelle für Rule-Handler."""
15
14
 
16
15
  function_name: str # ClassVar, der Name, unter dem dieser Handler registriert wird
17
16
 
17
+ @abstractmethod
18
18
  def handle(
19
19
  self,
20
20
  node: ast.AST,
@@ -27,11 +27,13 @@ class BaseRuleHandler:
27
27
  """
28
28
  Erstelle Fehlermeldungen für den Vergleichs- oder Funktionsaufruf.
29
29
  """
30
- raise NotImplementedError
30
+ pass
31
31
 
32
32
 
33
- class LenHandler(BaseRuleHandler):
34
- function_name = "len"
33
+ class FunctionHandler(BaseRuleHandler, ABC):
34
+ """
35
+ Handler für Funktionsaufrufe wie len(), max(), min(), sum().
36
+ """
35
37
 
36
38
  def handle(
37
39
  self,
@@ -42,7 +44,6 @@ class LenHandler(BaseRuleHandler):
42
44
  var_values: Dict[str, Optional[object]],
43
45
  rule: Rule,
44
46
  ) -> Dict[str, str]:
45
- # Wir erwarten hier einen Compare-Knoten
46
47
  if not isinstance(node, ast.Compare):
47
48
  return {}
48
49
  compare_node = node
@@ -51,11 +52,45 @@ class LenHandler(BaseRuleHandler):
51
52
  right_node = compare_node.comparators[0]
52
53
  op_symbol = rule._get_op_symbol(op)
53
54
 
54
- # Argument von len(...)
55
55
  if not (isinstance(left_node, ast.Call) and left_node.args):
56
- raise ValueError("Invalid left node for len function")
56
+ raise ValueError(f"Invalid left node for {self.function_name}() function")
57
57
  arg_node = left_node.args[0]
58
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
+
59
94
  var_name = rule._get_node_name(arg_node)
60
95
  var_value = var_values.get(var_name)
61
96
 
@@ -65,7 +100,6 @@ class LenHandler(BaseRuleHandler):
65
100
  raise ValueError("Invalid arguments for len function")
66
101
  right_value: int | float = raw
67
102
 
68
- # Schwellenwert je nach Operator
69
103
  if op_symbol == ">":
70
104
  threshold = right_value + 1
71
105
  elif op_symbol == ">=":
@@ -83,40 +117,118 @@ class LenHandler(BaseRuleHandler):
83
117
  elif op_symbol in ("<", "<="):
84
118
  msg = f"[{var_name}] ({var_value}) is too long (max length {threshold})!"
85
119
  else:
86
- msg = f"[{var_name}] ({var_value}) must be {op_symbol} {right_value}!"
120
+ msg = f"[{var_name}] ({var_value}) must have a length of {right_value}!"
87
121
 
88
122
  return {var_name: msg}
89
123
 
90
124
 
91
- class IntersectionCheckHandler(BaseRuleHandler):
92
- function_name = "intersectionCheck"
125
+ class SumHandler(FunctionHandler):
126
+ function_name = "sum"
93
127
 
94
- def handle(
128
+ def aggregate(
95
129
  self,
96
- node: ast.AST,
97
- left: Optional[ast.expr],
98
- right: Optional[ast.expr],
99
- op: Optional[ast.cmpop],
130
+ arg_node: ast.expr,
131
+ right_node: ast.expr,
132
+ op_symbol: str,
100
133
  var_values: Dict[str, Optional[object]],
101
- rule: Rule[GeneralManager],
134
+ rule: Rule,
102
135
  ) -> Dict[str, str]:
103
- # Der Aufruf steht in `left`, nicht in `node`
104
- call_node = cast(ast.Call, left)
105
- if not isinstance(call_node, ast.Call):
106
- return {"error": "Invalid arguments for intersectionCheck"}
107
-
108
- args = call_node.args
109
- if len(args) < 2:
110
- return {"error": "Invalid arguments for intersectionCheck"}
111
-
112
- start_node, end_node = args[0], args[1]
113
- start_name = rule._get_node_name(start_node)
114
- end_name = rule._get_node_name(end_node)
115
- start_val = var_values.get(start_name)
116
- end_val = var_values.get(end_name)
117
-
118
- msg = (
119
- f"[{start_name}] ({start_val}) and "
120
- f"[{end_name}] ({end_val}) must not overlap with existing ranges."
121
- )
122
- return {start_name: msg, end_name: msg}
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,11 +77,11 @@ 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", []):
82
- handler_cls = import_string(path)
84
+ handler_cls: type[BaseRuleHandler] = import_string(path)
83
85
  inst = handler_cls()
84
86
  self._handlers[inst.function_name] = inst
85
87
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GeneralManager
3
- Version: 0.1.2
3
+ Version: 0.3.0
4
4
  Summary: Kurzbeschreibung deines Pakets
5
5
  Author-email: Tim Kleindick <tkleindick@yahoo.de>
6
6
  License: Non-Commercial MIT License
@@ -5,11 +5,14 @@ general_manager/api/mutation.py,sha256=uu5RVxc9wbb-Zrbtt4azegvyKymMqEsxk_CkerKCd
5
5
  general_manager/api/property.py,sha256=oc93p1P8dcIvrNorRuqD1EJVsd6eYttYhZuAS0s28gs,696
6
6
  general_manager/auxiliary/__init__.py,sha256=4IwKJzsNxGduF-Ej0u1BNHVaMhkql8PjHbVtx9DOTSY,76
7
7
  general_manager/auxiliary/argsToKwargs.py,sha256=kmp1xonpQp4X_y8ZJG6c5uOW7zQwo0HtPqsHWVzXRSM,921
8
- general_manager/auxiliary/filterParser.py,sha256=exmRGVwvdMPgN-65MLwAV2q6L23bI4gTWb6lWXO4ZuU,3108
9
- general_manager/auxiliary/noneToZero.py,sha256=ovopSddMlTw84GIFWRd5s91_CFahPyL-t974w78Jb7g,300
10
- general_manager/cache/cacheDecorator.py,sha256=RmUDyJfkouO2-R2KZZJF1n8RCB0FjVa2OoTeLQ0vUyg,2934
11
- general_manager/cache/cacheTracker.py,sha256=vUQCarqABIW_O02hbKeRo0thurmPD0TfrOlIUh7kneI,1045
12
- general_manager/cache/dependencyIndex.py,sha256=KUDroajKrWfJv0PO2EQVNTrYq7XxrpBT4MGKBgPwMwo,10676
8
+ general_manager/auxiliary/filterParser.py,sha256=wmR4YzsnYgjI2Co5eyvCFROldotAraHx_GiCDJo79IY,5410
9
+ general_manager/auxiliary/jsonEncoder.py,sha256=TDsgFQvheITHZgdmn-m8tk1_QCzpT0XwEHo7bY3Qe-M,638
10
+ general_manager/auxiliary/makeCacheKey.py,sha256=o2ZPe5ZjiZhHxYgSESBSMnOmQutkTxw5DLeJM7tOw84,881
11
+ general_manager/auxiliary/noneToZero.py,sha256=KfQtMQnrT6vsYST0K7lv6pVujkDcK3XL8czHYOhgqKQ,539
12
+ general_manager/cache/cacheDecorator.py,sha256=7DbPMw-2Xm1-5eCMVWyjUl_jdVtTJUKNpLW2WNJEhI0,1471
13
+ general_manager/cache/cacheTracker.py,sha256=RmUWwAXMS5LQT8-w7IiG67FTBSxF1V0w4IDH1WzgmuM,998
14
+ general_manager/cache/dependencyIndex.py,sha256=iuOjthmH5ehHCkWiM9iLbgzGlo4Cf2wRKkm-QhIQ024,10813
15
+ general_manager/cache/modelDependencyCollector.py,sha256=Lt8mNpnp-AJaPCYrTu_UB1m_Hj1prfEXaPrxRwk3Qqs,1983
13
16
  general_manager/cache/pathMapping.py,sha256=WtECIek9fI-2_nqIYI4Ux9Lan6g8P9TMO_AfthkznX8,5656
14
17
  general_manager/cache/signals.py,sha256=ZHeXKFMN7tj9t0J-vSqf_05_NhGqEF2sZtbZO3vaRqI,1234
15
18
  general_manager/factory/__init__.py,sha256=DLSQbpSBpujPtDSZcruPc43OLWzKCCtf20gbalCDYRU,91
@@ -18,9 +21,9 @@ general_manager/factory/lazy_methods.py,sha256=UJC50a4Gbe4T5IQPh7ucyQqpaS2x4zhQz
18
21
  general_manager/interface/__init__.py,sha256=6x5adQLefTugvrJeyPcAxstyqgLAYeaJ1EPdAbac9pE,213
19
22
  general_manager/interface/baseInterface.py,sha256=mvSKUlA-0fazNnaIXGBwkiZxmX8DM_sOn-SaAIpaW8I,10273
20
23
  general_manager/interface/calculationInterface.py,sha256=GzSNXjU6Z7bFz60gHyMKkI5xNUDIPuniV8wbyVtQT50,14250
21
- general_manager/interface/databaseInterface.py,sha256=VR3V8WTE6XEeM84bv09h2__XMnTWp92u8CA8WRlevWc,27377
24
+ general_manager/interface/databaseInterface.py,sha256=i3Z-rkHnoGzV8cFZjBKBmIjlWjGJ2CzdlsiQfL2JUas,28288
22
25
  general_manager/manager/__init__.py,sha256=l3RYp62aEhj3Y975_XUTIzo35LUnkTJHkb_hgChnXXI,111
23
- general_manager/manager/generalManager.py,sha256=xt7tvwTaPGhh7Z8VhbdMJuScsl78B9QDVQoMWv4amuo,5158
26
+ general_manager/manager/generalManager.py,sha256=md-3zVCOMa-vC8ToCOEMKXeDuV7JtHLlSStQXh4cgBE,5258
24
27
  general_manager/manager/groupManager.py,sha256=O4FABqbm7KlZw6t36Ot3HU1FsBYN0h6Zhmk7ktN8a-Y,10087
25
28
  general_manager/manager/input.py,sha256=iKawV3P1QICz-0AQUF00OvH7LZYxussg3svpvCUl8hE,2977
26
29
  general_manager/manager/meta.py,sha256=5wHrCVnua5c38vpVZSCesrNvgydQDH8h6pxW6_QgCDg,3107
@@ -34,10 +37,10 @@ general_manager/permission/managerBasedPermission.py,sha256=VgZJVgkXWdaLk6K7c0kY
34
37
  general_manager/permission/permissionChecks.py,sha256=T-9khBqiwM4ASBdey9p07sC_xgzceIU9EAE0reukguM,1655
35
38
  general_manager/permission/permissionDataManager.py,sha256=Ji7fsnuaKTa6M8yzCGyzrIHyGa_ZvqJM7sXR97-uTrA,1937
36
39
  general_manager/rule/__init__.py,sha256=4Har5cfPD1fmOsilTDod-ZUz3Com-tkl58jz7yY4fD0,23
37
- general_manager/rule/handler.py,sha256=O3BZbTnUE9o3LTSWpoq6hgm3tKy1sEBjRcDrRaA54Gw,3936
38
- general_manager/rule/rule.py,sha256=tu3pNe_avs6ayI16KYeaQX85o2lXcPip_LN9CJEhr8Y,10708
39
- generalmanager-0.1.2.dist-info/licenses/LICENSE,sha256=YGFm0ieb4KpkMRRt2qnWue6uFh0cUMtobwEBkHwajhc,1450
40
- generalmanager-0.1.2.dist-info/METADATA,sha256=gfnRxKVbWKkOoCdnaR4cTvT7kXEaW6c5_kFTTEkFqVU,8188
41
- generalmanager-0.1.2.dist-info/WHEEL,sha256=DnLRTWE75wApRYVsjgc6wsVswC54sMSJhAEd4xhDpBk,91
42
- generalmanager-0.1.2.dist-info/top_level.txt,sha256=sTDtExP9ga-YP3h3h42yivUY-A2Q23C2nw6LNKOho4I,16
43
- generalmanager-0.1.2.dist-info/RECORD,,
40
+ general_manager/rule/handler.py,sha256=z8SFHTIZ0LbLh3fV56Mud0V4_OvWkqJjlHvFqau7Qfk,7334
41
+ general_manager/rule/rule.py,sha256=3FVCKGL7BTVoStdgOTdWQwuoVRIxAIAilV4VOzouDpc,10759
42
+ generalmanager-0.3.0.dist-info/licenses/LICENSE,sha256=YGFm0ieb4KpkMRRt2qnWue6uFh0cUMtobwEBkHwajhc,1450
43
+ generalmanager-0.3.0.dist-info/METADATA,sha256=sKfEf7wuzdUhszsnlCJ49iydcFKidlwV58863yosGlw,8188
44
+ generalmanager-0.3.0.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
45
+ generalmanager-0.3.0.dist-info/top_level.txt,sha256=sTDtExP9ga-YP3h3h42yivUY-A2Q23C2nw6LNKOho4I,16
46
+ generalmanager-0.3.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.4.0)
2
+ Generator: setuptools (80.7.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5