GeneralManager 0.3.0__tar.gz → 0.3.2__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.
- {generalmanager-0.3.0 → generalmanager-0.3.2}/GeneralManager.egg-info/PKG-INFO +1 -1
- {generalmanager-0.3.0 → generalmanager-0.3.2}/GeneralManager.egg-info/SOURCES.txt +8 -2
- {generalmanager-0.3.0 → generalmanager-0.3.2}/PKG-INFO +1 -1
- {generalmanager-0.3.0 → generalmanager-0.3.2}/pyproject.toml +1 -1
- {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/auxiliary/makeCacheKey.py +11 -8
- {generalmanager-0.3.0/src/general_manager/cache → generalmanager-0.3.2/src/general_manager/auxiliary}/pathMapping.py +31 -6
- generalmanager-0.3.2/src/general_manager/cache/cacheDecorator.py +81 -0
- generalmanager-0.3.2/src/general_manager/cache/cacheTracker.py +74 -0
- {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/cache/dependencyIndex.py +41 -45
- {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/cache/modelDependencyCollector.py +15 -7
- generalmanager-0.3.2/tests/test_cacheDecorator.py +396 -0
- generalmanager-0.3.2/tests/test_cacheTracker.py +76 -0
- generalmanager-0.3.2/tests/test_dependencyIndex.py +967 -0
- generalmanager-0.3.2/tests/test_makeCacheKey.py +372 -0
- generalmanager-0.3.2/tests/test_modelDependencyCollector.py +119 -0
- generalmanager-0.3.2/tests/test_signals.py +109 -0
- generalmanager-0.3.0/src/general_manager/cache/cacheDecorator.py +0 -45
- generalmanager-0.3.0/src/general_manager/cache/cacheTracker.py +0 -34
- {generalmanager-0.3.0 → generalmanager-0.3.2}/GeneralManager.egg-info/dependency_links.txt +0 -0
- {generalmanager-0.3.0 → generalmanager-0.3.2}/GeneralManager.egg-info/requires.txt +0 -0
- {generalmanager-0.3.0 → generalmanager-0.3.2}/GeneralManager.egg-info/top_level.txt +0 -0
- {generalmanager-0.3.0 → generalmanager-0.3.2}/LICENSE +0 -0
- {generalmanager-0.3.0 → generalmanager-0.3.2}/README.md +0 -0
- {generalmanager-0.3.0 → generalmanager-0.3.2}/setup.cfg +0 -0
- {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/__init__.py +0 -0
- {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/api/graphql.py +0 -0
- {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/api/mutation.py +0 -0
- {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/api/property.py +0 -0
- {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/apps.py +0 -0
- {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/auxiliary/__init__.py +0 -0
- {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/auxiliary/argsToKwargs.py +0 -0
- {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/auxiliary/filterParser.py +0 -0
- {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/auxiliary/jsonEncoder.py +0 -0
- {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/auxiliary/noneToZero.py +0 -0
- {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/cache/signals.py +0 -0
- {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/factory/__init__.py +0 -0
- {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/factory/factories.py +0 -0
- {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/factory/lazy_methods.py +0 -0
- {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/interface/__init__.py +0 -0
- {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/interface/baseInterface.py +0 -0
- {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/interface/calculationInterface.py +0 -0
- {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/interface/databaseInterface.py +0 -0
- {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/manager/__init__.py +0 -0
- {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/manager/generalManager.py +0 -0
- {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/manager/groupManager.py +0 -0
- {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/manager/input.py +0 -0
- {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/manager/meta.py +0 -0
- {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/measurement/__init__.py +0 -0
- {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/measurement/measurement.py +0 -0
- {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/measurement/measurementField.py +0 -0
- {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/permission/__init__.py +0 -0
- {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/permission/basePermission.py +0 -0
- {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/permission/fileBasedPermission.py +0 -0
- {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/permission/managerBasedPermission.py +0 -0
- {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/permission/permissionChecks.py +0 -0
- {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/permission/permissionDataManager.py +0 -0
- {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/rule/__init__.py +0 -0
- {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/rule/handler.py +0 -0
- {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/rule/rule.py +0 -0
- {generalmanager-0.3.0 → generalmanager-0.3.2}/tests/test_argsToKwargs.py +0 -0
- {generalmanager-0.3.0 → generalmanager-0.3.2}/tests/test_basePermission.py +0 -0
- {generalmanager-0.3.0 → generalmanager-0.3.2}/tests/test_filterParser.py +0 -0
- {generalmanager-0.3.0 → generalmanager-0.3.2}/tests/test_graph_ql.py +0 -0
- {generalmanager-0.3.0 → generalmanager-0.3.2}/tests/test_input.py +0 -0
- {generalmanager-0.3.0 → generalmanager-0.3.2}/tests/test_jsonEncoder.py +0 -0
- {generalmanager-0.3.0 → generalmanager-0.3.2}/tests/test_managerBasedPermission.py +0 -0
- {generalmanager-0.3.0 → generalmanager-0.3.2}/tests/test_measurement.py +0 -0
- {generalmanager-0.3.0 → generalmanager-0.3.2}/tests/test_measurement_field.py +0 -0
- {generalmanager-0.3.0 → generalmanager-0.3.2}/tests/test_noneToZero.py +0 -0
- {generalmanager-0.3.0 → generalmanager-0.3.2}/tests/test_rule_handler.py +0 -0
- {generalmanager-0.3.0 → generalmanager-0.3.2}/tests/test_rules.py +0 -0
- {generalmanager-0.3.0 → generalmanager-0.3.2}/tests/test_settings.py +0 -0
@@ -17,11 +17,11 @@ src/general_manager/auxiliary/filterParser.py
|
|
17
17
|
src/general_manager/auxiliary/jsonEncoder.py
|
18
18
|
src/general_manager/auxiliary/makeCacheKey.py
|
19
19
|
src/general_manager/auxiliary/noneToZero.py
|
20
|
+
src/general_manager/auxiliary/pathMapping.py
|
20
21
|
src/general_manager/cache/cacheDecorator.py
|
21
22
|
src/general_manager/cache/cacheTracker.py
|
22
23
|
src/general_manager/cache/dependencyIndex.py
|
23
24
|
src/general_manager/cache/modelDependencyCollector.py
|
24
|
-
src/general_manager/cache/pathMapping.py
|
25
25
|
src/general_manager/cache/signals.py
|
26
26
|
src/general_manager/factory/__init__.py
|
27
27
|
src/general_manager/factory/factories.py
|
@@ -49,14 +49,20 @@ src/general_manager/rule/handler.py
|
|
49
49
|
src/general_manager/rule/rule.py
|
50
50
|
tests/test_argsToKwargs.py
|
51
51
|
tests/test_basePermission.py
|
52
|
+
tests/test_cacheDecorator.py
|
53
|
+
tests/test_cacheTracker.py
|
54
|
+
tests/test_dependencyIndex.py
|
52
55
|
tests/test_filterParser.py
|
53
56
|
tests/test_graph_ql.py
|
54
57
|
tests/test_input.py
|
55
58
|
tests/test_jsonEncoder.py
|
59
|
+
tests/test_makeCacheKey.py
|
56
60
|
tests/test_managerBasedPermission.py
|
57
61
|
tests/test_measurement.py
|
58
62
|
tests/test_measurement_field.py
|
63
|
+
tests/test_modelDependencyCollector.py
|
59
64
|
tests/test_noneToZero.py
|
60
65
|
tests/test_rule_handler.py
|
61
66
|
tests/test_rules.py
|
62
|
-
tests/test_settings.py
|
67
|
+
tests/test_settings.py
|
68
|
+
tests/test_signals.py
|
@@ -6,22 +6,25 @@ from hashlib import sha256
|
|
6
6
|
|
7
7
|
def make_cache_key(func, args, kwargs):
|
8
8
|
"""
|
9
|
-
|
10
|
-
|
9
|
+
Generates a unique, deterministic cache key for a specific function call.
|
10
|
+
|
11
|
+
The key is derived from the function's module, qualified name, and bound arguments,
|
12
|
+
serialized to JSON and hashed with SHA-256 to ensure uniqueness for each call signature.
|
13
|
+
|
11
14
|
Args:
|
12
|
-
func: The function
|
13
|
-
args: Positional arguments
|
14
|
-
kwargs: Keyword arguments
|
15
|
-
|
15
|
+
func: The target function to be identified.
|
16
|
+
args: Positional arguments for the function call.
|
17
|
+
kwargs: Keyword arguments for the function call.
|
18
|
+
|
16
19
|
Returns:
|
17
|
-
|
20
|
+
A hexadecimal SHA-256 hash string uniquely representing the function call.
|
18
21
|
"""
|
19
22
|
sig = inspect.signature(func)
|
20
23
|
bound = sig.bind_partial(*args, **kwargs)
|
21
24
|
bound.apply_defaults()
|
22
25
|
payload = {
|
23
26
|
"module": func.__module__,
|
24
|
-
"
|
27
|
+
"qualname": func.__qualname__,
|
25
28
|
"args": bound.arguments,
|
26
29
|
}
|
27
30
|
raw = json.dumps(
|
@@ -3,9 +3,8 @@ from typing import TYPE_CHECKING, cast, get_args
|
|
3
3
|
from general_manager.manager.meta import GeneralManagerMeta
|
4
4
|
from general_manager.api.property import GraphQLProperty
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
from general_manager.manager.generalManager import GeneralManager
|
6
|
+
from general_manager.interface.baseInterface import Bucket
|
7
|
+
from general_manager.manager.generalManager import GeneralManager
|
9
8
|
|
10
9
|
|
11
10
|
type PathStart = str
|
@@ -25,6 +24,11 @@ class PathMap:
|
|
25
24
|
|
26
25
|
@classmethod
|
27
26
|
def createPathMapping(cls):
|
27
|
+
"""
|
28
|
+
Builds the mapping of paths between all pairs of distinct managed classes.
|
29
|
+
|
30
|
+
Iterates over all registered managed classes and creates a PathTracer for each unique start and destination class pair, storing them in the mapping dictionary.
|
31
|
+
"""
|
28
32
|
all_managed_classes = GeneralManagerMeta.all_classes
|
29
33
|
for start_class in all_managed_classes:
|
30
34
|
for destination_class in all_managed_classes:
|
@@ -34,8 +38,12 @@ class PathMap:
|
|
34
38
|
] = PathTracer(start_class, destination_class)
|
35
39
|
|
36
40
|
def __init__(self, path_start: PathStart | GeneralManager | type[GeneralManager]):
|
37
|
-
from general_manager.manager.generalManager import GeneralManager
|
38
41
|
|
42
|
+
"""
|
43
|
+
Initializes a PathMap with a specified starting point.
|
44
|
+
|
45
|
+
The starting point can be a class name (string), a GeneralManager instance, or a GeneralManager subclass. Sets internal attributes for the start instance, class, and class name based on the input.
|
46
|
+
"""
|
39
47
|
if isinstance(path_start, GeneralManager):
|
40
48
|
self.start_instance = path_start
|
41
49
|
self.start_class = path_start.__class__
|
@@ -99,8 +107,17 @@ class PathTracer:
|
|
99
107
|
def createPath(
|
100
108
|
self, current_manager: type[GeneralManager], path: list[str]
|
101
109
|
) -> list[str] | None:
|
102
|
-
from general_manager.manager.generalManager import GeneralManager
|
103
110
|
|
111
|
+
"""
|
112
|
+
Recursively constructs a path of attribute names from the current manager class to the destination class.
|
113
|
+
|
114
|
+
Args:
|
115
|
+
current_manager: The current GeneralManager subclass being inspected.
|
116
|
+
path: The list of attribute names traversed so far.
|
117
|
+
|
118
|
+
Returns:
|
119
|
+
A list of attribute names representing the path to the destination class, or None if no path exists.
|
120
|
+
"""
|
104
121
|
current_connections = {
|
105
122
|
attr_name: attr_value["type"]
|
106
123
|
for attr_name, attr_value in current_manager.Interface.getAttributeTypes().items()
|
@@ -131,8 +148,16 @@ class PathTracer:
|
|
131
148
|
def traversePath(
|
132
149
|
self, start_instance: GeneralManager | Bucket
|
133
150
|
) -> GeneralManager | Bucket | None:
|
134
|
-
from general_manager.interface.baseInterface import Bucket
|
135
151
|
|
152
|
+
"""
|
153
|
+
Traverses the stored path from a starting instance to reach the destination instance or bucket.
|
154
|
+
|
155
|
+
Args:
|
156
|
+
start_instance: The initial GeneralManager or Bucket instance from which to begin traversal.
|
157
|
+
|
158
|
+
Returns:
|
159
|
+
The resulting GeneralManager or Bucket instance at the end of the path, or None if the path is empty.
|
160
|
+
"""
|
136
161
|
current_instance = start_instance
|
137
162
|
if not self.path:
|
138
163
|
return None
|
@@ -0,0 +1,81 @@
|
|
1
|
+
from typing import Any, Callable, Optional, Protocol, Set
|
2
|
+
from functools import wraps
|
3
|
+
from django.core.cache import cache as django_cache
|
4
|
+
from general_manager.cache.cacheTracker import DependencyTracker
|
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
|
8
|
+
|
9
|
+
|
10
|
+
class CacheBackend(Protocol):
|
11
|
+
def get(self, key: str, default: Optional[Any] = None) -> Any:
|
12
|
+
"""
|
13
|
+
Retrieves a value from the cache by key, returning a default if the key is not found.
|
14
|
+
|
15
|
+
Args:
|
16
|
+
key: The cache key to look up.
|
17
|
+
default: Value to return if the key is not present in the cache.
|
18
|
+
|
19
|
+
Returns:
|
20
|
+
The cached value if found; otherwise, the provided default.
|
21
|
+
"""
|
22
|
+
...
|
23
|
+
|
24
|
+
def set(self, key: str, value: Any, timeout: Optional[int] = None) -> None:
|
25
|
+
"""
|
26
|
+
Stores a value in the cache under the specified key with an optional expiration timeout.
|
27
|
+
|
28
|
+
Args:
|
29
|
+
key: The cache key to associate with the value.
|
30
|
+
value: The value to store in the cache.
|
31
|
+
timeout: Optional expiration time in seconds. If None, the value is cached indefinitely.
|
32
|
+
"""
|
33
|
+
...
|
34
|
+
|
35
|
+
|
36
|
+
RecordFn = Callable[[str, Set[Dependency]], None]
|
37
|
+
|
38
|
+
_SENTINEL = object()
|
39
|
+
|
40
|
+
|
41
|
+
def cached(
|
42
|
+
timeout: Optional[int] = None,
|
43
|
+
cache_backend: CacheBackend = django_cache,
|
44
|
+
record_fn: RecordFn = record_dependencies,
|
45
|
+
) -> Callable:
|
46
|
+
"""
|
47
|
+
Decorator that caches function results and tracks their dependencies.
|
48
|
+
|
49
|
+
When applied to a function, this decorator caches the function's output using a generated cache key based on its arguments. It also tracks dependencies accessed during the function's execution and stores them alongside the cached result. On cache hits, previously stored dependencies are re-tracked to maintain dependency tracking continuity. If dependencies exist and no timeout is set, an external recording function is invoked to persist the dependency information.
|
50
|
+
"""
|
51
|
+
|
52
|
+
def decorator(func: Callable) -> Callable:
|
53
|
+
@wraps(func)
|
54
|
+
def wrapper(*args, **kwargs):
|
55
|
+
key = make_cache_key(func, args, kwargs)
|
56
|
+
deps_key = f"{key}:deps"
|
57
|
+
|
58
|
+
cached_result = cache_backend.get(key, _SENTINEL)
|
59
|
+
if cached_result is not _SENTINEL:
|
60
|
+
# saved dependencies are added to the current tracker
|
61
|
+
cached_deps = cache_backend.get(deps_key)
|
62
|
+
if cached_deps:
|
63
|
+
for class_name, operation, identifier in cached_deps:
|
64
|
+
DependencyTracker.track(class_name, operation, identifier)
|
65
|
+
return cached_result
|
66
|
+
|
67
|
+
with DependencyTracker() as dependencies:
|
68
|
+
result = func(*args, **kwargs)
|
69
|
+
ModelDependencyCollector.addArgs(dependencies, args, kwargs)
|
70
|
+
|
71
|
+
cache_backend.set(key, result, timeout)
|
72
|
+
cache_backend.set(deps_key, dependencies, timeout)
|
73
|
+
|
74
|
+
if dependencies and timeout is None:
|
75
|
+
record_fn(key, dependencies)
|
76
|
+
|
77
|
+
return result
|
78
|
+
|
79
|
+
return wrapper
|
80
|
+
|
81
|
+
return decorator
|
@@ -0,0 +1,74 @@
|
|
1
|
+
import threading
|
2
|
+
from general_manager.cache.dependencyIndex import (
|
3
|
+
general_manager_name,
|
4
|
+
Dependency,
|
5
|
+
filter_type,
|
6
|
+
)
|
7
|
+
|
8
|
+
# Thread-lokale Variable zur Speicherung der Abhängigkeiten
|
9
|
+
_dependency_storage = threading.local()
|
10
|
+
|
11
|
+
|
12
|
+
class DependencyTracker:
|
13
|
+
def __enter__(
|
14
|
+
self,
|
15
|
+
) -> set[Dependency]:
|
16
|
+
"""
|
17
|
+
Enters a new dependency tracking context and returns the set for collecting dependencies.
|
18
|
+
|
19
|
+
Initializes thread-local storage for dependency tracking if not already present, supports nested contexts, and provides a set to accumulate dependencies at the current nesting level.
|
20
|
+
|
21
|
+
Returns:
|
22
|
+
The set used to collect dependencies for the current context level.
|
23
|
+
"""
|
24
|
+
if not hasattr(_dependency_storage, "dependencies"):
|
25
|
+
_dependency_storage._depth = 0
|
26
|
+
_dependency_storage.dependencies = list()
|
27
|
+
else:
|
28
|
+
_dependency_storage._depth += 1
|
29
|
+
_dependency_storage.dependencies.append(set())
|
30
|
+
return _dependency_storage.dependencies[_dependency_storage._depth]
|
31
|
+
|
32
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
33
|
+
"""
|
34
|
+
Exits the dependency tracking context, managing cleanup for nested scopes.
|
35
|
+
|
36
|
+
If exiting the outermost context, removes all dependency tracking data from thread-local storage. Otherwise, decrements the nesting depth and removes the most recent dependency set.
|
37
|
+
"""
|
38
|
+
if hasattr(_dependency_storage, "dependencies"):
|
39
|
+
if _dependency_storage._depth == 0:
|
40
|
+
self.reset_thread_local_storage()
|
41
|
+
|
42
|
+
else:
|
43
|
+
# Ansonsten reduzieren wir nur die Tiefe
|
44
|
+
_dependency_storage._depth -= 1
|
45
|
+
_dependency_storage.dependencies.pop()
|
46
|
+
|
47
|
+
@staticmethod
|
48
|
+
def track(
|
49
|
+
class_name: general_manager_name,
|
50
|
+
operation: filter_type,
|
51
|
+
identifier: str,
|
52
|
+
) -> None:
|
53
|
+
"""
|
54
|
+
Records a dependency in all active dependency tracking contexts.
|
55
|
+
|
56
|
+
Adds the specified dependency tuple to each set in the current stack of dependency tracking scopes, ensuring it is tracked at all nested levels.
|
57
|
+
"""
|
58
|
+
if hasattr(_dependency_storage, "dependencies"):
|
59
|
+
for dep_set in _dependency_storage.dependencies[
|
60
|
+
: _dependency_storage._depth + 1
|
61
|
+
]:
|
62
|
+
dep_set: set[Dependency]
|
63
|
+
dep_set.add((class_name, operation, identifier))
|
64
|
+
|
65
|
+
@staticmethod
|
66
|
+
def reset_thread_local_storage() -> None:
|
67
|
+
"""
|
68
|
+
Resets the thread-local storage for dependency tracking.
|
69
|
+
|
70
|
+
This method clears the thread-local storage, ensuring that all dependency tracking data is removed. It is useful for cleaning up after operations that may have modified the state of the tracker.
|
71
|
+
"""
|
72
|
+
if hasattr(_dependency_storage, "dependencies"):
|
73
|
+
del _dependency_storage.dependencies
|
74
|
+
del _dependency_storage._depth
|
@@ -33,18 +33,19 @@ type Dependency = Tuple[general_manager_name, filter_type, str]
|
|
33
33
|
INDEX_KEY = "dependency_index" # Key unter dem der gesamte Index liegt
|
34
34
|
LOCK_KEY = "dependency_index_lock" # Key für das Sperr‑Mutex
|
35
35
|
LOCK_TIMEOUT = 5 # Sekunden TTL für den Lock
|
36
|
+
UNDEFINED = object() # Dummy für nicht definierte Werte
|
36
37
|
|
37
38
|
|
38
39
|
# -----------------------------------------------------------------------------
|
39
40
|
# LOCKING HELPERS
|
40
41
|
# -----------------------------------------------------------------------------
|
41
42
|
def acquire_lock(timeout: int = LOCK_TIMEOUT) -> bool:
|
42
|
-
"""Atomar:
|
43
|
+
"""Atomar: create Lock key if it doesn't exist."""
|
43
44
|
return cache.add(LOCK_KEY, "1", timeout)
|
44
45
|
|
45
46
|
|
46
47
|
def release_lock() -> None:
|
47
|
-
"""
|
48
|
+
"""Release Lock key."""
|
48
49
|
cache.delete(LOCK_KEY)
|
49
50
|
|
50
51
|
|
@@ -52,7 +53,7 @@ def release_lock() -> None:
|
|
52
53
|
# INDEX ACCESS
|
53
54
|
# -----------------------------------------------------------------------------
|
54
55
|
def get_full_index() -> dependency_index:
|
55
|
-
"""
|
56
|
+
"""Load or initialize the full index."""
|
56
57
|
idx = cache.get(INDEX_KEY, None)
|
57
58
|
if idx is None:
|
58
59
|
idx: dependency_index = {"filter": {}, "exclude": {}}
|
@@ -61,7 +62,7 @@ def get_full_index() -> dependency_index:
|
|
61
62
|
|
62
63
|
|
63
64
|
def set_full_index(idx: dependency_index) -> None:
|
64
|
-
"""
|
65
|
+
"""Write the complete index back to the cache."""
|
65
66
|
cache.set(INDEX_KEY, idx, None)
|
66
67
|
|
67
68
|
|
@@ -78,15 +79,6 @@ def record_dependencies(
|
|
78
79
|
]
|
79
80
|
],
|
80
81
|
) -> None:
|
81
|
-
"""
|
82
|
-
Speichert die Abhängigkeiten eines Cache Eintrags.
|
83
|
-
:param cache_key: der Key unter dem das Ergebnis im cache steht
|
84
|
-
:param dependencies: Iterable von Tuplen (model_name, action, identifier)
|
85
|
-
action ∈ {'filter','exclude'} oder sonstige → 'id'
|
86
|
-
identifier = für filter/exclude: Dict String,
|
87
|
-
sonst: Primärschlüssel als String
|
88
|
-
"""
|
89
|
-
# 1) Lock holen (Spin‑Lock mit Timeout)
|
90
82
|
start = time.time()
|
91
83
|
while not acquire_lock():
|
92
84
|
if time.time() - start > LOCK_TIMEOUT:
|
@@ -105,7 +97,7 @@ def record_dependencies(
|
|
105
97
|
lookup_map.setdefault(val_key, set()).add(cache_key)
|
106
98
|
|
107
99
|
else:
|
108
|
-
#
|
100
|
+
# director ID Lookup as simple filter on 'id'
|
109
101
|
section = idx["filter"].setdefault(model_name, {})
|
110
102
|
lookup_map = section.setdefault("identification", {})
|
111
103
|
val_key = identifier
|
@@ -120,15 +112,12 @@ def record_dependencies(
|
|
120
112
|
# -----------------------------------------------------------------------------
|
121
113
|
# INDEX CLEANUP
|
122
114
|
# -----------------------------------------------------------------------------
|
123
|
-
def remove_cache_key_from_index(cache_key: str) ->
|
124
|
-
"""
|
125
|
-
Entfernt einen cache_key aus allen Einträgen in filter/exclude.
|
126
|
-
Nützlich, sobald Du den Cache gelöscht hast.
|
127
|
-
"""
|
115
|
+
def remove_cache_key_from_index(cache_key: str) -> None:
|
116
|
+
"""Remove a cache key from the index."""
|
128
117
|
start = time.time()
|
129
118
|
while not acquire_lock():
|
130
119
|
if time.time() - start > LOCK_TIMEOUT:
|
131
|
-
|
120
|
+
raise TimeoutError("Could not aquire lock for remove_cache_key_from_index")
|
132
121
|
time.sleep(0.05)
|
133
122
|
|
134
123
|
try:
|
@@ -149,14 +138,12 @@ def remove_cache_key_from_index(cache_key: str) -> bool:
|
|
149
138
|
set_full_index(idx)
|
150
139
|
finally:
|
151
140
|
release_lock()
|
152
|
-
return True
|
153
141
|
|
154
142
|
|
155
143
|
# -----------------------------------------------------------------------------
|
156
144
|
# CACHE INVALIDATION
|
157
145
|
# -----------------------------------------------------------------------------
|
158
146
|
def invalidate_cache_key(cache_key: str) -> None:
|
159
|
-
"""Löscht den CacheEintrag – hier nutzt du deinen CacheBackend."""
|
160
147
|
cache.delete(cache_key)
|
161
148
|
|
162
149
|
|
@@ -165,33 +152,28 @@ def capture_old_values(
|
|
165
152
|
sender: Type[GeneralManager], instance: GeneralManager | None, **kwargs
|
166
153
|
) -> None:
|
167
154
|
if instance is None:
|
168
|
-
# Wenn es kein Modell ist, gibt es nichts zu tun
|
169
155
|
return
|
170
156
|
manager_name = sender.__name__
|
171
157
|
idx = get_full_index()
|
172
|
-
#
|
158
|
+
# get all lookups for this model
|
173
159
|
lookups = set()
|
174
160
|
for action in ("filter", "exclude"):
|
175
161
|
lookups |= set(idx.get(action, {}).get(manager_name, {}))
|
176
162
|
if lookups and instance.identification:
|
177
|
-
#
|
163
|
+
# save old values for later comparison
|
178
164
|
vals = {}
|
179
165
|
for lookup in lookups:
|
180
166
|
attr_path = lookup.split("__")
|
181
167
|
obj = instance
|
182
|
-
for attr in attr_path:
|
183
|
-
|
184
|
-
|
168
|
+
for i, attr in enumerate(attr_path):
|
169
|
+
if getattr(obj, attr, UNDEFINED) is UNDEFINED:
|
170
|
+
lookup = "__".join(attr_path[:i])
|
185
171
|
break
|
172
|
+
obj = getattr(obj, attr, None)
|
186
173
|
vals[lookup] = obj
|
187
174
|
setattr(instance, "_old_values", vals)
|
188
175
|
|
189
176
|
|
190
|
-
# -----------------------------------------------------------------------------
|
191
|
-
# GENERIC CACHE INVALIDATION: vergleicht alt vs. neu und invalidiert nur bei Übergang
|
192
|
-
# -----------------------------------------------------------------------------
|
193
|
-
|
194
|
-
|
195
177
|
@receiver(post_data_change)
|
196
178
|
def generic_cache_invalidation(
|
197
179
|
sender: type[GeneralManager],
|
@@ -236,18 +218,32 @@ def generic_cache_invalidation(
|
|
236
218
|
# wildcard / regex
|
237
219
|
if op in ("contains", "startswith", "endswith", "regex"):
|
238
220
|
try:
|
239
|
-
|
240
|
-
except:
|
241
|
-
|
242
|
-
|
243
|
-
|
221
|
+
literal = ast.literal_eval(val_key)
|
222
|
+
except Exception:
|
223
|
+
literal = val_key
|
224
|
+
|
225
|
+
# ensure we always work with strings to avoid TypeErrors
|
226
|
+
text = "" if value is None else str(value)
|
227
|
+
if op == "contains":
|
228
|
+
return literal in text
|
229
|
+
if op == "startswith":
|
230
|
+
return text.startswith(literal)
|
231
|
+
if op == "endswith":
|
232
|
+
return text.endswith(literal)
|
233
|
+
# regex: val_key selbst als Pattern benutzen
|
234
|
+
if op == "regex":
|
235
|
+
try:
|
236
|
+
pattern = re.compile(val_key)
|
237
|
+
except re.error:
|
238
|
+
return False
|
239
|
+
return bool(pattern.search(text))
|
244
240
|
|
245
241
|
return False
|
246
242
|
|
247
243
|
for action in ("filter", "exclude"):
|
248
244
|
model_section = idx.get(action, {}).get(manager_name, {})
|
249
245
|
for lookup, lookup_map in model_section.items():
|
250
|
-
# 1)
|
246
|
+
# 1) get operator and attribute path
|
251
247
|
parts = lookup.split("__")
|
252
248
|
if parts[-1] in (
|
253
249
|
"gt",
|
@@ -266,8 +262,8 @@ def generic_cache_invalidation(
|
|
266
262
|
op = "eq"
|
267
263
|
attr_path = parts
|
268
264
|
|
269
|
-
# 2)
|
270
|
-
old_val = old_relevant_values.get(
|
265
|
+
# 2) get old & new value
|
266
|
+
old_val = old_relevant_values.get("__".join(attr_path))
|
271
267
|
|
272
268
|
obj = instance
|
273
269
|
for attr in attr_path:
|
@@ -276,14 +272,14 @@ def generic_cache_invalidation(
|
|
276
272
|
break
|
277
273
|
new_val = obj
|
278
274
|
|
279
|
-
# 3)
|
275
|
+
# 3) check against all cache_keys
|
280
276
|
for val_key, cache_keys in list(lookup_map.items()):
|
281
277
|
old_match = matches(op, old_val, val_key)
|
282
278
|
new_match = matches(op, new_val, val_key)
|
283
279
|
|
284
280
|
if action == "filter":
|
285
|
-
#
|
286
|
-
if new_match:
|
281
|
+
# Filter: invalidate if new match or old match
|
282
|
+
if new_match or old_match:
|
287
283
|
print(
|
288
284
|
f"Invalidate cache key {cache_keys} for filter {lookup} with value {val_key}"
|
289
285
|
)
|
@@ -292,7 +288,7 @@ def generic_cache_invalidation(
|
|
292
288
|
remove_cache_key_from_index(ck)
|
293
289
|
|
294
290
|
else: # action == 'exclude'
|
295
|
-
# Excludes:
|
291
|
+
# Excludes: invalidate only if matches changed
|
296
292
|
if old_match != new_match:
|
297
293
|
print(
|
298
294
|
f"Invalidate cache key {cache_keys} for exclude {lookup} with value {val_key}"
|
{generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/cache/modelDependencyCollector.py
RENAMED
@@ -9,15 +9,23 @@ from general_manager.cache.dependencyIndex import (
|
|
9
9
|
|
10
10
|
class ModelDependencyCollector:
|
11
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
12
|
@staticmethod
|
19
13
|
def collect(obj) -> Generator[tuple[general_manager_name, filter_type, str]]:
|
20
|
-
"""
|
14
|
+
"""
|
15
|
+
Recursively yields dependency information from objects related to Django models.
|
16
|
+
|
17
|
+
Traverses the input object and its nested structures to identify instances of
|
18
|
+
GeneralManager and Bucket, yielding tuples that describe their dependencies.
|
19
|
+
Yields a tuple of (manager class name, dependency type, dependency value) for
|
20
|
+
each dependency found.
|
21
|
+
|
22
|
+
Args:
|
23
|
+
obj: The object or collection to inspect for model dependencies.
|
24
|
+
|
25
|
+
Yields:
|
26
|
+
Tuples containing the manager class name, dependency type ("identification",
|
27
|
+
"filter", or "exclude"), and the string representation of the dependency value.
|
28
|
+
"""
|
21
29
|
if isinstance(obj, GeneralManager):
|
22
30
|
yield (
|
23
31
|
obj.__class__.__name__,
|