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.
Files changed (72) hide show
  1. {generalmanager-0.3.0 → generalmanager-0.3.2}/GeneralManager.egg-info/PKG-INFO +1 -1
  2. {generalmanager-0.3.0 → generalmanager-0.3.2}/GeneralManager.egg-info/SOURCES.txt +8 -2
  3. {generalmanager-0.3.0 → generalmanager-0.3.2}/PKG-INFO +1 -1
  4. {generalmanager-0.3.0 → generalmanager-0.3.2}/pyproject.toml +1 -1
  5. {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/auxiliary/makeCacheKey.py +11 -8
  6. {generalmanager-0.3.0/src/general_manager/cache → generalmanager-0.3.2/src/general_manager/auxiliary}/pathMapping.py +31 -6
  7. generalmanager-0.3.2/src/general_manager/cache/cacheDecorator.py +81 -0
  8. generalmanager-0.3.2/src/general_manager/cache/cacheTracker.py +74 -0
  9. {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/cache/dependencyIndex.py +41 -45
  10. {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/cache/modelDependencyCollector.py +15 -7
  11. generalmanager-0.3.2/tests/test_cacheDecorator.py +396 -0
  12. generalmanager-0.3.2/tests/test_cacheTracker.py +76 -0
  13. generalmanager-0.3.2/tests/test_dependencyIndex.py +967 -0
  14. generalmanager-0.3.2/tests/test_makeCacheKey.py +372 -0
  15. generalmanager-0.3.2/tests/test_modelDependencyCollector.py +119 -0
  16. generalmanager-0.3.2/tests/test_signals.py +109 -0
  17. generalmanager-0.3.0/src/general_manager/cache/cacheDecorator.py +0 -45
  18. generalmanager-0.3.0/src/general_manager/cache/cacheTracker.py +0 -34
  19. {generalmanager-0.3.0 → generalmanager-0.3.2}/GeneralManager.egg-info/dependency_links.txt +0 -0
  20. {generalmanager-0.3.0 → generalmanager-0.3.2}/GeneralManager.egg-info/requires.txt +0 -0
  21. {generalmanager-0.3.0 → generalmanager-0.3.2}/GeneralManager.egg-info/top_level.txt +0 -0
  22. {generalmanager-0.3.0 → generalmanager-0.3.2}/LICENSE +0 -0
  23. {generalmanager-0.3.0 → generalmanager-0.3.2}/README.md +0 -0
  24. {generalmanager-0.3.0 → generalmanager-0.3.2}/setup.cfg +0 -0
  25. {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/__init__.py +0 -0
  26. {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/api/graphql.py +0 -0
  27. {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/api/mutation.py +0 -0
  28. {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/api/property.py +0 -0
  29. {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/apps.py +0 -0
  30. {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/auxiliary/__init__.py +0 -0
  31. {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/auxiliary/argsToKwargs.py +0 -0
  32. {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/auxiliary/filterParser.py +0 -0
  33. {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/auxiliary/jsonEncoder.py +0 -0
  34. {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/auxiliary/noneToZero.py +0 -0
  35. {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/cache/signals.py +0 -0
  36. {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/factory/__init__.py +0 -0
  37. {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/factory/factories.py +0 -0
  38. {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/factory/lazy_methods.py +0 -0
  39. {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/interface/__init__.py +0 -0
  40. {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/interface/baseInterface.py +0 -0
  41. {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/interface/calculationInterface.py +0 -0
  42. {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/interface/databaseInterface.py +0 -0
  43. {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/manager/__init__.py +0 -0
  44. {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/manager/generalManager.py +0 -0
  45. {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/manager/groupManager.py +0 -0
  46. {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/manager/input.py +0 -0
  47. {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/manager/meta.py +0 -0
  48. {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/measurement/__init__.py +0 -0
  49. {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/measurement/measurement.py +0 -0
  50. {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/measurement/measurementField.py +0 -0
  51. {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/permission/__init__.py +0 -0
  52. {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/permission/basePermission.py +0 -0
  53. {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/permission/fileBasedPermission.py +0 -0
  54. {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/permission/managerBasedPermission.py +0 -0
  55. {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/permission/permissionChecks.py +0 -0
  56. {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/permission/permissionDataManager.py +0 -0
  57. {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/rule/__init__.py +0 -0
  58. {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/rule/handler.py +0 -0
  59. {generalmanager-0.3.0 → generalmanager-0.3.2}/src/general_manager/rule/rule.py +0 -0
  60. {generalmanager-0.3.0 → generalmanager-0.3.2}/tests/test_argsToKwargs.py +0 -0
  61. {generalmanager-0.3.0 → generalmanager-0.3.2}/tests/test_basePermission.py +0 -0
  62. {generalmanager-0.3.0 → generalmanager-0.3.2}/tests/test_filterParser.py +0 -0
  63. {generalmanager-0.3.0 → generalmanager-0.3.2}/tests/test_graph_ql.py +0 -0
  64. {generalmanager-0.3.0 → generalmanager-0.3.2}/tests/test_input.py +0 -0
  65. {generalmanager-0.3.0 → generalmanager-0.3.2}/tests/test_jsonEncoder.py +0 -0
  66. {generalmanager-0.3.0 → generalmanager-0.3.2}/tests/test_managerBasedPermission.py +0 -0
  67. {generalmanager-0.3.0 → generalmanager-0.3.2}/tests/test_measurement.py +0 -0
  68. {generalmanager-0.3.0 → generalmanager-0.3.2}/tests/test_measurement_field.py +0 -0
  69. {generalmanager-0.3.0 → generalmanager-0.3.2}/tests/test_noneToZero.py +0 -0
  70. {generalmanager-0.3.0 → generalmanager-0.3.2}/tests/test_rule_handler.py +0 -0
  71. {generalmanager-0.3.0 → generalmanager-0.3.2}/tests/test_rules.py +0 -0
  72. {generalmanager-0.3.0 → generalmanager-0.3.2}/tests/test_settings.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GeneralManager
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: Kurzbeschreibung deines Pakets
5
5
  Author-email: Tim Kleindick <tkleindick@yahoo.de>
6
6
  License: Non-Commercial MIT License
@@ -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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GeneralManager
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: Kurzbeschreibung deines Pakets
5
5
  Author-email: Tim Kleindick <tkleindick@yahoo.de>
6
6
  License: Non-Commercial MIT License
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
7
7
 
8
8
  [project]
9
9
  name = "GeneralManager"
10
- version = "0.3.0"
10
+ version = "0.3.2"
11
11
  description = "Kurzbeschreibung deines Pakets"
12
12
  readme = "README.md"
13
13
  authors = [
@@ -6,22 +6,25 @@ from hashlib import sha256
6
6
 
7
7
  def make_cache_key(func, args, kwargs):
8
8
  """
9
- Generate a deterministic cache key for a function call.
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 being called
13
- args: Positional arguments to the function
14
- kwargs: Keyword arguments to the function
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
- str: A hexadecimal SHA-256 hash that uniquely identifies this function call
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
- "name": func.__name__,
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
- if TYPE_CHECKING:
7
- from general_manager.interface.baseInterface import Bucket
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: legt den LOCK_KEY an, wenn noch frei."""
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
- """Gibt den Lock frei."""
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
- """Lädt oder initialisiert den kompletten Index."""
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
- """Schreibt den kompletten Index zurück in den Cache."""
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
- # Direkter IDLookup als simpler filter auf 'id'
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) -> bool:
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
- return False
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
- # Welche Lookups interessieren uns für diesen Model?
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
- # Speichere alle relevanten Attribute für später
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
- obj = getattr(obj, attr, None)
184
- if obj is None:
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
- pattern = re.compile(val_key)
240
- except:
241
- return False
242
- text = value or ""
243
- return bool(pattern.search(text))
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) Operator und Attributpfad ermitteln
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) Alten und neuen Wert holen
270
- old_val = old_relevant_values.get(lookup)
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) Für jedes val_key prüfen
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
- # Direkte & alle Filter-Abhängigkeiten: immer invalidieren, wenn neu matcht
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: nur invalidieren, wenn sich der Match-Status ändert
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}"
@@ -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
- """Recursively find Django model instances in the object."""
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__,