GeneralManager 0.0.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.
Files changed (43) hide show
  1. general_manager/__init__.py +0 -0
  2. general_manager/api/graphql.py +732 -0
  3. general_manager/api/mutation.py +143 -0
  4. general_manager/api/property.py +20 -0
  5. general_manager/apps.py +83 -0
  6. general_manager/auxiliary/__init__.py +2 -0
  7. general_manager/auxiliary/argsToKwargs.py +25 -0
  8. general_manager/auxiliary/filterParser.py +97 -0
  9. general_manager/auxiliary/noneToZero.py +12 -0
  10. general_manager/cache/cacheDecorator.py +72 -0
  11. general_manager/cache/cacheTracker.py +33 -0
  12. general_manager/cache/dependencyIndex.py +300 -0
  13. general_manager/cache/pathMapping.py +151 -0
  14. general_manager/cache/signals.py +48 -0
  15. general_manager/factory/__init__.py +5 -0
  16. general_manager/factory/factories.py +287 -0
  17. general_manager/factory/lazy_methods.py +38 -0
  18. general_manager/interface/__init__.py +3 -0
  19. general_manager/interface/baseInterface.py +308 -0
  20. general_manager/interface/calculationInterface.py +406 -0
  21. general_manager/interface/databaseInterface.py +726 -0
  22. general_manager/manager/__init__.py +3 -0
  23. general_manager/manager/generalManager.py +136 -0
  24. general_manager/manager/groupManager.py +288 -0
  25. general_manager/manager/input.py +48 -0
  26. general_manager/manager/meta.py +75 -0
  27. general_manager/measurement/__init__.py +2 -0
  28. general_manager/measurement/measurement.py +233 -0
  29. general_manager/measurement/measurementField.py +152 -0
  30. general_manager/permission/__init__.py +1 -0
  31. general_manager/permission/basePermission.py +178 -0
  32. general_manager/permission/fileBasedPermission.py +0 -0
  33. general_manager/permission/managerBasedPermission.py +171 -0
  34. general_manager/permission/permissionChecks.py +53 -0
  35. general_manager/permission/permissionDataManager.py +55 -0
  36. general_manager/rule/__init__.py +1 -0
  37. general_manager/rule/handler.py +122 -0
  38. general_manager/rule/rule.py +313 -0
  39. generalmanager-0.0.0.dist-info/METADATA +207 -0
  40. generalmanager-0.0.0.dist-info/RECORD +43 -0
  41. generalmanager-0.0.0.dist-info/WHEEL +5 -0
  42. generalmanager-0.0.0.dist-info/licenses/LICENSE +29 -0
  43. generalmanager-0.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,300 @@
1
+ from __future__ import annotations
2
+ import time
3
+ import ast
4
+ import re
5
+
6
+ from django.core.cache import cache
7
+ from general_manager.cache.signals import post_data_change, pre_data_change
8
+ from django.dispatch import receiver
9
+ from typing import Literal, Any, Iterable, TYPE_CHECKING, Type
10
+
11
+ if TYPE_CHECKING:
12
+ from general_manager.manager.generalManager import GeneralManager
13
+
14
+ type general_manager_name = str # e.g. "Project", "Derivative", "User"
15
+ type attribute = str # e.g. "field", "name", "id"
16
+ type lookup = str # e.g. "field__gt", "field__in", "field__contains", "field"
17
+ type cache_keys = set[str] # e.g. "cache_key_1", "cache_key_2"
18
+ type identifier = str # e.g. "{'id': 1}"", "{'project': Project(**{'id': 1})}", ...
19
+ type dependency_index = dict[
20
+ Literal["filter", "exclude"],
21
+ dict[
22
+ general_manager_name,
23
+ dict[attribute, dict[lookup, cache_keys]],
24
+ ],
25
+ ]
26
+
27
+
28
+ # -----------------------------------------------------------------------------
29
+ # CONFIG
30
+ # -----------------------------------------------------------------------------
31
+ INDEX_KEY = "dependency_index" # Key unter dem der gesamte Index liegt
32
+ LOCK_KEY = "dependency_index_lock" # Key für das Sperr‑Mutex
33
+ LOCK_TIMEOUT = 5 # Sekunden TTL für den Lock
34
+
35
+
36
+ # -----------------------------------------------------------------------------
37
+ # LOCKING HELPERS
38
+ # -----------------------------------------------------------------------------
39
+ def acquire_lock(timeout: int = LOCK_TIMEOUT) -> bool:
40
+ """Atomar: legt den LOCK_KEY an, wenn noch frei."""
41
+ return cache.add(LOCK_KEY, "1", timeout)
42
+
43
+
44
+ def release_lock() -> None:
45
+ """Gibt den Lock frei."""
46
+ cache.delete(LOCK_KEY)
47
+
48
+
49
+ # -----------------------------------------------------------------------------
50
+ # INDEX ACCESS
51
+ # -----------------------------------------------------------------------------
52
+ def get_full_index() -> dependency_index:
53
+ """Lädt oder initialisiert den kompletten Index."""
54
+ idx = cache.get(INDEX_KEY, None)
55
+ if idx is None:
56
+ idx: dependency_index = {"filter": {}, "exclude": {}}
57
+ cache.set(INDEX_KEY, idx, None)
58
+ return idx
59
+
60
+
61
+ def set_full_index(idx: dependency_index) -> None:
62
+ """Schreibt den kompletten Index zurück in den Cache."""
63
+ cache.set(INDEX_KEY, idx, None)
64
+
65
+
66
+ # -----------------------------------------------------------------------------
67
+ # DEPENDENCY RECORDING
68
+ # -----------------------------------------------------------------------------
69
+ def record_dependencies(
70
+ cache_key: str,
71
+ dependencies: Iterable[
72
+ tuple[
73
+ general_manager_name,
74
+ Literal["filter", "exclude", "identification"],
75
+ identifier,
76
+ ]
77
+ ],
78
+ ) -> None:
79
+ """
80
+ Speichert die Abhängigkeiten eines Cache Eintrags.
81
+ :param cache_key: der Key unter dem das Ergebnis im cache steht
82
+ :param dependencies: Iterable von Tuplen (model_name, action, identifier)
83
+ action ∈ {'filter','exclude'} oder sonstige → 'id'
84
+ identifier = für filter/exclude: Dict String,
85
+ sonst: Primärschlüssel als String
86
+ """
87
+ # 1) Lock holen (Spin‑Lock mit Timeout)
88
+ start = time.time()
89
+ while not acquire_lock():
90
+ if time.time() - start > LOCK_TIMEOUT:
91
+ raise TimeoutError("Could not aquire lock for record_dependencies")
92
+ time.sleep(0.05)
93
+
94
+ try:
95
+ idx = get_full_index()
96
+ for model_name, action, identifier in dependencies:
97
+ if action in ("filter", "exclude"):
98
+ params = ast.literal_eval(identifier)
99
+ section = idx[action].setdefault(model_name, {})
100
+ for lookup, val in params.items():
101
+ lookup_map = section.setdefault(lookup, {})
102
+ val_key = repr(val)
103
+ lookup_map.setdefault(val_key, set()).add(cache_key)
104
+
105
+ else:
106
+ # Direkter ID‑Lookup als simpler filter auf 'id'
107
+ section = idx["filter"].setdefault(model_name, {})
108
+ lookup_map = section.setdefault("identification", {})
109
+ val_key = identifier
110
+ lookup_map.setdefault(val_key, set()).add(cache_key)
111
+
112
+ set_full_index(idx)
113
+
114
+ finally:
115
+ release_lock()
116
+
117
+
118
+ # -----------------------------------------------------------------------------
119
+ # INDEX CLEANUP
120
+ # -----------------------------------------------------------------------------
121
+ def remove_cache_key_from_index(cache_key: str) -> bool:
122
+ """
123
+ Entfernt einen cache_key aus allen Einträgen in filter/​exclude.
124
+ Nützlich, sobald Du den Cache gelöscht hast.
125
+ """
126
+ start = time.time()
127
+ while not acquire_lock():
128
+ if time.time() - start > LOCK_TIMEOUT:
129
+ return False
130
+ time.sleep(0.05)
131
+
132
+ try:
133
+ idx = get_full_index()
134
+ for action in ("filter", "exclude"):
135
+ action_section = idx.get(action, {})
136
+ for mname, model_section in list(action_section.items()):
137
+ for lookup, lookup_map in list(model_section.items()):
138
+ for val_key, key_set in list(lookup_map.items()):
139
+ if cache_key in key_set:
140
+ key_set.remove(cache_key)
141
+ if not key_set:
142
+ del lookup_map[val_key]
143
+ if not lookup_map:
144
+ del model_section[lookup]
145
+ if not model_section:
146
+ del action_section[mname]
147
+ set_full_index(idx)
148
+ finally:
149
+ release_lock()
150
+ return True
151
+
152
+
153
+ # -----------------------------------------------------------------------------
154
+ # CACHE INVALIDATION
155
+ # -----------------------------------------------------------------------------
156
+ def invalidate_cache_key(cache_key: str) -> None:
157
+ """Löscht den CacheEintrag – hier nutzt du deinen CacheBackend."""
158
+ cache.delete(cache_key)
159
+
160
+
161
+ @receiver(pre_data_change)
162
+ def capture_old_values(
163
+ sender: Type[GeneralManager], instance: GeneralManager | None, **kwargs
164
+ ) -> None:
165
+ if instance is None:
166
+ # Wenn es kein Modell ist, gibt es nichts zu tun
167
+ return
168
+ manager_name = sender.__name__
169
+ idx = get_full_index()
170
+ # Welche Lookups interessieren uns für diesen Model?
171
+ lookups = set()
172
+ for action in ("filter", "exclude"):
173
+ lookups |= set(idx.get(action, {}).get(manager_name, {}))
174
+ if lookups and instance.identification:
175
+ # Speichere alle relevanten Attribute für später
176
+ vals = {}
177
+ for lookup in lookups:
178
+ attr_path = lookup.split("__")
179
+ obj = instance
180
+ for attr in attr_path:
181
+ obj = getattr(obj, attr, None)
182
+ if obj is None:
183
+ break
184
+ vals[lookup] = obj
185
+ setattr(instance, "_old_values", vals)
186
+
187
+
188
+ # -----------------------------------------------------------------------------
189
+ # GENERIC CACHE INVALIDATION: vergleicht alt vs. neu und invalidiert nur bei Übergang
190
+ # -----------------------------------------------------------------------------
191
+
192
+
193
+ @receiver(post_data_change)
194
+ def generic_cache_invalidation(
195
+ sender: type[GeneralManager],
196
+ instance: GeneralManager,
197
+ old_relevant_values: dict[str, Any],
198
+ **kwargs,
199
+ ):
200
+ manager_name = sender.__name__
201
+ idx = get_full_index()
202
+
203
+ def matches(op: str, value: Any, val_key: Any) -> bool:
204
+ if value is None:
205
+ return False
206
+
207
+ # eq
208
+ if op == "eq":
209
+ return repr(value) == val_key
210
+
211
+ # in
212
+ if op == "in":
213
+ try:
214
+ seq = ast.literal_eval(val_key)
215
+ return value in seq
216
+ except:
217
+ return False
218
+
219
+ # range
220
+ if op in ("gt", "gte", "lt", "lte"):
221
+ try:
222
+ thr = type(value)(ast.literal_eval(val_key))
223
+ except:
224
+ return False
225
+ if op == "gt":
226
+ return value > thr
227
+ if op == "gte":
228
+ return value >= thr
229
+ if op == "lt":
230
+ return value < thr
231
+ if op == "lte":
232
+ return value <= thr
233
+
234
+ # wildcard / regex
235
+ if op in ("contains", "startswith", "endswith", "regex"):
236
+ try:
237
+ pattern = re.compile(val_key)
238
+ except:
239
+ return False
240
+ text = value or ""
241
+ return bool(pattern.search(text))
242
+
243
+ return False
244
+
245
+ for action in ("filter", "exclude"):
246
+ model_section = idx.get(action, {}).get(manager_name, {})
247
+ for lookup, lookup_map in model_section.items():
248
+ # 1) Operator und Attributpfad ermitteln
249
+ parts = lookup.split("__")
250
+ if parts[-1] in (
251
+ "gt",
252
+ "gte",
253
+ "lt",
254
+ "lte",
255
+ "in",
256
+ "contains",
257
+ "startswith",
258
+ "endswith",
259
+ "regex",
260
+ ):
261
+ op = parts[-1]
262
+ attr_path = parts[:-1]
263
+ else:
264
+ op = "eq"
265
+ attr_path = parts
266
+
267
+ # 2) Alten und neuen Wert holen
268
+ old_val = old_relevant_values.get(lookup)
269
+
270
+ obj = instance
271
+ for attr in attr_path:
272
+ obj = getattr(obj, attr, None)
273
+ if obj is None:
274
+ break
275
+ new_val = obj
276
+
277
+ # 3) Für jedes val_key prüfen
278
+ for val_key, cache_keys in list(lookup_map.items()):
279
+ old_match = matches(op, old_val, val_key)
280
+ new_match = matches(op, new_val, val_key)
281
+
282
+ if action == "filter":
283
+ # Direkte & alle Filter-Abhängigkeiten: immer invalidieren, wenn neu matcht
284
+ if new_match:
285
+ print(
286
+ f"Invalidate cache key {cache_keys} for filter {lookup} with value {val_key}"
287
+ )
288
+ for ck in list(cache_keys):
289
+ invalidate_cache_key(ck)
290
+ remove_cache_key_from_index(ck)
291
+
292
+ else: # action == 'exclude'
293
+ # Excludes: nur invalidieren, wenn sich der Match-Status ändert
294
+ if old_match != new_match:
295
+ print(
296
+ f"Invalidate cache key {cache_keys} for exclude {lookup} with value {val_key}"
297
+ )
298
+ for ck in list(cache_keys):
299
+ invalidate_cache_key(ck)
300
+ remove_cache_key_from_index(ck)
@@ -0,0 +1,151 @@
1
+ from __future__ import annotations
2
+ from typing import TYPE_CHECKING, cast, get_args
3
+ from general_manager.manager.meta import GeneralManagerMeta
4
+ from general_manager.api.property import GraphQLProperty
5
+
6
+ if TYPE_CHECKING:
7
+ from general_manager.interface.baseInterface import Bucket
8
+ from general_manager.manager.generalManager import GeneralManager
9
+
10
+
11
+ type PathStart = str
12
+ type PathDestination = str
13
+
14
+
15
+ class PathMap:
16
+
17
+ instance: PathMap
18
+ mapping: dict[tuple[PathStart, PathDestination], PathTracer] = {}
19
+
20
+ def __new__(cls, *args, **kwargs):
21
+ if not hasattr(cls, "instance"):
22
+ cls.instance = super().__new__(cls)
23
+ cls.createPathMapping()
24
+ return cls.instance
25
+
26
+ @classmethod
27
+ def createPathMapping(cls):
28
+ all_managed_classes = GeneralManagerMeta.all_classes
29
+ for start_class in all_managed_classes:
30
+ for destination_class in all_managed_classes:
31
+ if start_class != destination_class:
32
+ cls.instance.mapping[
33
+ (start_class.__name__, destination_class.__name__)
34
+ ] = PathTracer(start_class, destination_class)
35
+
36
+ def __init__(self, path_start: PathStart | GeneralManager | type[GeneralManager]):
37
+ from general_manager.manager.generalManager import GeneralManager
38
+
39
+ if isinstance(path_start, GeneralManager):
40
+ self.start_instance = path_start
41
+ self.start_class = path_start.__class__
42
+ self.start_class_name = path_start.__class__.__name__
43
+ elif isinstance(path_start, type):
44
+ self.start_instance = None
45
+ self.start_class = path_start
46
+ self.start_class_name = path_start.__name__
47
+ else:
48
+ self.start_instance = None
49
+ self.start_class = None
50
+ self.start_class_name = path_start
51
+
52
+ def to(
53
+ self, path_destination: PathDestination | type[GeneralManager] | str
54
+ ) -> PathTracer | None:
55
+ if isinstance(path_destination, type):
56
+ path_destination = path_destination.__name__
57
+
58
+ tracer = self.mapping.get((self.start_class_name, path_destination), None)
59
+ if not tracer:
60
+ return None
61
+ return tracer
62
+
63
+ def goTo(
64
+ self, path_destination: PathDestination | type[GeneralManager] | str
65
+ ) -> GeneralManager | Bucket | None:
66
+ if isinstance(path_destination, type):
67
+ path_destination = path_destination.__name__
68
+
69
+ tracer = self.mapping.get((self.start_class_name, path_destination), None)
70
+ if not tracer:
71
+ return None
72
+ if not self.start_instance:
73
+ raise ValueError("Cannot call goTo on a PathMap without a start instance.")
74
+ return tracer.traversePath(self.start_instance)
75
+
76
+ def getAllConnected(self) -> set[str]:
77
+ """
78
+ Returns a list of all classes that are connected to the start class.
79
+ """
80
+ connected_classes: set[str] = set()
81
+ for path_tuple in self.mapping.keys():
82
+ if path_tuple[0] == self.start_class_name:
83
+ destination_class_name = path_tuple[1]
84
+ connected_classes.add(destination_class_name)
85
+ return connected_classes
86
+
87
+
88
+ class PathTracer:
89
+ def __init__(
90
+ self, start_class: type[GeneralManager], destination_class: type[GeneralManager]
91
+ ):
92
+ self.start_class = start_class
93
+ self.destination_class = destination_class
94
+ if self.start_class == self.destination_class:
95
+ self.path = []
96
+ else:
97
+ self.path = self.createPath(start_class, [])
98
+
99
+ def createPath(
100
+ self, current_manager: type[GeneralManager], path: list[str]
101
+ ) -> list[str] | None:
102
+ from general_manager.manager.generalManager import GeneralManager
103
+
104
+ current_connections = {
105
+ attr_name: attr_value["type"]
106
+ for attr_name, attr_value in current_manager.Interface.getAttributeTypes().items()
107
+ }
108
+ for attr_name, attr_value in current_manager.__dict__.items():
109
+ if not isinstance(attr_value, GraphQLProperty):
110
+ continue
111
+ type_hints = get_args(attr_value.graphql_type_hint)
112
+ field_type = (
113
+ type_hints[0]
114
+ if type_hints
115
+ else cast(type, attr_value.graphql_type_hint)
116
+ )
117
+ current_connections[attr_name] = field_type
118
+ for attr, attr_type in current_connections.items():
119
+ if attr in path or attr_type == self.start_class:
120
+ continue
121
+ if not issubclass(attr_type, GeneralManager):
122
+ continue
123
+ if attr_type == self.destination_class:
124
+ return [*path, attr]
125
+ result = self.createPath(attr_type, [*path, attr])
126
+ if result:
127
+ return result
128
+
129
+ return None
130
+
131
+ def traversePath(
132
+ self, start_instance: GeneralManager | Bucket
133
+ ) -> GeneralManager | Bucket | None:
134
+ from general_manager.interface.baseInterface import Bucket
135
+
136
+ current_instance = start_instance
137
+ if not self.path:
138
+ return None
139
+ for attr in self.path:
140
+ if not isinstance(current_instance, Bucket):
141
+ current_instance = getattr(current_instance, attr)
142
+ continue
143
+ new_instance = None
144
+ for entry in current_instance:
145
+ if not new_instance:
146
+ new_instance = getattr(entry, attr)
147
+ else:
148
+ new_instance = new_instance | getattr(entry, attr)
149
+ current_instance = new_instance
150
+
151
+ return current_instance
@@ -0,0 +1,48 @@
1
+ from django.dispatch import Signal
2
+ from typing import Callable, Any
3
+ from functools import wraps
4
+
5
+ post_data_change = Signal()
6
+
7
+ pre_data_change = Signal()
8
+
9
+
10
+ def dataChange(func: Callable[..., Any]) -> Callable:
11
+ """
12
+ Signal to indicate that data has changed.
13
+ """
14
+
15
+ @wraps(func)
16
+ def wrapper(*args, **kwargs):
17
+ action = func.__name__
18
+ if func.__name__ == "create":
19
+ sender = args[0]
20
+ instance_before = None
21
+ else:
22
+ instance = args[0]
23
+ sender = instance.__class__
24
+ instance_before = instance
25
+ pre_data_change.send(
26
+ sender=sender,
27
+ instance=instance_before,
28
+ action=action,
29
+ **kwargs,
30
+ )
31
+ old_relevant_values = getattr(instance_before, "_old_values", {})
32
+ result = (
33
+ func.__func__(*args, **kwargs)
34
+ if isinstance(func, classmethod)
35
+ else func(*args, **kwargs)
36
+ )
37
+ instance = result
38
+
39
+ post_data_change.send(
40
+ sender=sender,
41
+ instance=instance,
42
+ action=action,
43
+ old_relevant_values=old_relevant_values,
44
+ **kwargs,
45
+ )
46
+ return result
47
+
48
+ return wrapper
@@ -0,0 +1,5 @@
1
+ from .lazy_methods import (
2
+ LazyMeasurement,
3
+ LazyDeltaDate,
4
+ LazyProjectName,
5
+ )