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.
- general_manager/auxiliary/filterParser.py +54 -15
- general_manager/auxiliary/jsonEncoder.py +19 -0
- general_manager/auxiliary/makeCacheKey.py +30 -0
- general_manager/auxiliary/noneToZero.py +9 -0
- general_manager/cache/cacheDecorator.py +25 -52
- general_manager/cache/cacheTracker.py +21 -20
- general_manager/cache/dependencyIndex.py +3 -1
- general_manager/cache/modelDependencyCollector.py +51 -0
- general_manager/interface/databaseInterface.py +23 -0
- general_manager/manager/generalManager.py +10 -4
- general_manager/rule/handler.py +152 -40
- general_manager/rule/rule.py +5 -3
- {generalmanager-0.1.2.dist-info → generalmanager-0.3.0.dist-info}/METADATA +1 -1
- {generalmanager-0.1.2.dist-info → generalmanager-0.3.0.dist-info}/RECORD +17 -14
- {generalmanager-0.1.2.dist-info → generalmanager-0.3.0.dist-info}/WHEEL +1 -1
- {generalmanager-0.1.2.dist-info → generalmanager-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {generalmanager-0.1.2.dist-info → generalmanager-0.3.0.dist-info}/top_level.txt +0 -0
@@ -1,9 +1,23 @@
|
|
1
|
-
from
|
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,
|
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(
|
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
|
116
|
+
return value_to_check == filter_value
|
78
117
|
elif lookup == "lt":
|
79
|
-
return
|
118
|
+
return value_to_check < filter_value
|
80
119
|
elif lookup == "lte":
|
81
|
-
return
|
120
|
+
return value_to_check <= filter_value
|
82
121
|
elif lookup == "gt":
|
83
|
-
return
|
122
|
+
return value_to_check > filter_value
|
84
123
|
elif lookup == "gte":
|
85
|
-
return
|
86
|
-
elif lookup == "contains" and isinstance(
|
87
|
-
return
|
88
|
-
elif lookup == "startswith" and isinstance(
|
89
|
-
return
|
90
|
-
elif lookup == "endswith" and isinstance(
|
91
|
-
return
|
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
|
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,
|
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
|
-
|
10
|
-
|
11
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
3
|
-
|
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
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
def
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
general_manager/rule/handler.py
CHANGED
@@ -2,19 +2,19 @@
|
|
2
2
|
|
3
3
|
from __future__ import annotations
|
4
4
|
import ast
|
5
|
-
from typing import Dict, Optional, TYPE_CHECKING
|
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
|
-
|
30
|
+
pass
|
31
31
|
|
32
32
|
|
33
|
-
class
|
34
|
-
|
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
|
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
|
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
|
92
|
-
function_name = "
|
125
|
+
class SumHandler(FunctionHandler):
|
126
|
+
function_name = "sum"
|
93
127
|
|
94
|
-
def
|
128
|
+
def aggregate(
|
95
129
|
self,
|
96
|
-
|
97
|
-
|
98
|
-
|
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
|
134
|
+
rule: Rule,
|
102
135
|
) -> Dict[str, str]:
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
if
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
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}
|
general_manager/rule/rule.py
CHANGED
@@ -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
|
-
|
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,
|
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
|
|
@@ -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=
|
9
|
-
general_manager/auxiliary/
|
10
|
-
general_manager/
|
11
|
-
general_manager/
|
12
|
-
general_manager/cache/
|
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=
|
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=
|
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=
|
38
|
-
general_manager/rule/rule.py,sha256=
|
39
|
-
generalmanager-0.
|
40
|
-
generalmanager-0.
|
41
|
-
generalmanager-0.
|
42
|
-
generalmanager-0.
|
43
|
-
generalmanager-0.
|
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,,
|
File without changes
|
File without changes
|