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.
- general_manager/__init__.py +0 -0
- general_manager/api/graphql.py +732 -0
- general_manager/api/mutation.py +143 -0
- general_manager/api/property.py +20 -0
- general_manager/apps.py +83 -0
- general_manager/auxiliary/__init__.py +2 -0
- general_manager/auxiliary/argsToKwargs.py +25 -0
- general_manager/auxiliary/filterParser.py +97 -0
- general_manager/auxiliary/noneToZero.py +12 -0
- general_manager/cache/cacheDecorator.py +72 -0
- general_manager/cache/cacheTracker.py +33 -0
- general_manager/cache/dependencyIndex.py +300 -0
- general_manager/cache/pathMapping.py +151 -0
- general_manager/cache/signals.py +48 -0
- general_manager/factory/__init__.py +5 -0
- general_manager/factory/factories.py +287 -0
- general_manager/factory/lazy_methods.py +38 -0
- general_manager/interface/__init__.py +3 -0
- general_manager/interface/baseInterface.py +308 -0
- general_manager/interface/calculationInterface.py +406 -0
- general_manager/interface/databaseInterface.py +726 -0
- general_manager/manager/__init__.py +3 -0
- general_manager/manager/generalManager.py +136 -0
- general_manager/manager/groupManager.py +288 -0
- general_manager/manager/input.py +48 -0
- general_manager/manager/meta.py +75 -0
- general_manager/measurement/__init__.py +2 -0
- general_manager/measurement/measurement.py +233 -0
- general_manager/measurement/measurementField.py +152 -0
- general_manager/permission/__init__.py +1 -0
- general_manager/permission/basePermission.py +178 -0
- general_manager/permission/fileBasedPermission.py +0 -0
- general_manager/permission/managerBasedPermission.py +171 -0
- general_manager/permission/permissionChecks.py +53 -0
- general_manager/permission/permissionDataManager.py +55 -0
- general_manager/rule/__init__.py +1 -0
- general_manager/rule/handler.py +122 -0
- general_manager/rule/rule.py +313 -0
- generalmanager-0.0.0.dist-info/METADATA +207 -0
- generalmanager-0.0.0.dist-info/RECORD +43 -0
- generalmanager-0.0.0.dist-info/WHEEL +5 -0
- generalmanager-0.0.0.dist-info/licenses/LICENSE +29 -0
- generalmanager-0.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,143 @@
|
|
1
|
+
import inspect
|
2
|
+
from typing import get_type_hints, Optional, Union, List, Tuple, get_origin, get_args
|
3
|
+
import graphene
|
4
|
+
|
5
|
+
from general_manager.api.graphql import GraphQL
|
6
|
+
from general_manager.manager.generalManager import GeneralManager
|
7
|
+
|
8
|
+
|
9
|
+
def snake_to_pascal(s: str) -> str:
|
10
|
+
return "".join(p.title() for p in s.split("_"))
|
11
|
+
|
12
|
+
|
13
|
+
def snake_to_camel(s: str) -> str:
|
14
|
+
parts = s.split("_")
|
15
|
+
return parts[0] + "".join(p.title() for p in parts[1:])
|
16
|
+
|
17
|
+
|
18
|
+
def graphQlMutation(needs_role: Optional[str] = None, auth_required: bool = False):
|
19
|
+
"""
|
20
|
+
Decorator to generate a graphene.Mutation from a function and register it.
|
21
|
+
:param auth_required: if True, enforces that info.context.user is authenticated.
|
22
|
+
"""
|
23
|
+
|
24
|
+
def decorator(fn):
|
25
|
+
sig = inspect.signature(fn)
|
26
|
+
hints = get_type_hints(fn)
|
27
|
+
|
28
|
+
# Mutation name in PascalCase
|
29
|
+
mutation_name = snake_to_camel(fn.__name__)
|
30
|
+
|
31
|
+
# Build Arguments inner class dynamically
|
32
|
+
arg_fields = {}
|
33
|
+
for name, param in sig.parameters.items():
|
34
|
+
if name == "info":
|
35
|
+
continue
|
36
|
+
ann = hints.get(name)
|
37
|
+
if ann is None:
|
38
|
+
raise TypeError(
|
39
|
+
f"Missing type hint for parameter {name} in {fn.__name__}"
|
40
|
+
)
|
41
|
+
required = True
|
42
|
+
default = param.default
|
43
|
+
has_default = default is not inspect._empty
|
44
|
+
|
45
|
+
# Prepare kwargs
|
46
|
+
kwargs = {}
|
47
|
+
if required:
|
48
|
+
kwargs["required"] = True
|
49
|
+
if has_default:
|
50
|
+
kwargs["default_value"] = default
|
51
|
+
|
52
|
+
# Handle Optional[...] → not required
|
53
|
+
origin = get_origin(ann)
|
54
|
+
if origin is Union and type(None) in get_args(ann):
|
55
|
+
required = False
|
56
|
+
# extract inner type
|
57
|
+
ann = [a for a in get_args(ann) if a is not type(None)][0]
|
58
|
+
|
59
|
+
# Resolve list types to List scalar
|
60
|
+
if get_origin(ann) is list or get_origin(ann) is List:
|
61
|
+
inner = get_args(ann)[0]
|
62
|
+
field = graphene.List(
|
63
|
+
GraphQL._mapFieldToGrapheneBaseType(inner)(**kwargs)
|
64
|
+
)
|
65
|
+
else:
|
66
|
+
if isinstance(ann, GeneralManager):
|
67
|
+
field = graphene.ID(**kwargs)
|
68
|
+
else:
|
69
|
+
field = GraphQL._mapFieldToGrapheneBaseType(ann)(**kwargs)
|
70
|
+
|
71
|
+
arg_fields[name] = field
|
72
|
+
|
73
|
+
Arguments = type("Arguments", (), arg_fields)
|
74
|
+
|
75
|
+
# Build output fields: success, errors, + fn return types
|
76
|
+
outputs = {
|
77
|
+
"success": graphene.Boolean(required=True),
|
78
|
+
"errors": graphene.List(graphene.String),
|
79
|
+
}
|
80
|
+
return_ann: type | tuple[type] | None = hints.get("return")
|
81
|
+
if return_ann is None:
|
82
|
+
raise TypeError(f"Mutation {fn.__name__} missing return annotation")
|
83
|
+
|
84
|
+
# Unpack tuple return or single
|
85
|
+
out_types = (
|
86
|
+
list(get_args(return_ann))
|
87
|
+
if get_origin(return_ann) in (tuple, Tuple)
|
88
|
+
else [return_ann]
|
89
|
+
)
|
90
|
+
for out in out_types:
|
91
|
+
if not isinstance(out, type):
|
92
|
+
raise TypeError(
|
93
|
+
f"Mutation {fn.__name__} return type {out} is not a type"
|
94
|
+
)
|
95
|
+
name = out.__name__
|
96
|
+
field_name = name[0].lower() + name[1:]
|
97
|
+
outputs[field_name] = GraphQL._mapFieldToGrapheneRead(out, field_name)
|
98
|
+
|
99
|
+
# Define mutate method
|
100
|
+
def _mutate(root, info, **kwargs):
|
101
|
+
# Auth check
|
102
|
+
if auth_required and not getattr(info.context, "user", None):
|
103
|
+
return mutation_class(
|
104
|
+
**{"success": False, "errors": ["Authentication required"]}
|
105
|
+
)
|
106
|
+
try:
|
107
|
+
result = fn(info, **kwargs)
|
108
|
+
data = {}
|
109
|
+
if isinstance(result, tuple):
|
110
|
+
# unpack according to outputs ordering after success/errors
|
111
|
+
for (field, _), val in zip(
|
112
|
+
outputs.items(), [None, None] + list(result)
|
113
|
+
):
|
114
|
+
# skip success/errors
|
115
|
+
if field in ("success", "errors"):
|
116
|
+
continue
|
117
|
+
data[field] = val
|
118
|
+
else:
|
119
|
+
only = next(k for k in outputs if k not in ("success", "errors"))
|
120
|
+
data[only] = result
|
121
|
+
data["success"] = True
|
122
|
+
data["errors"] = []
|
123
|
+
return mutation_class(**data)
|
124
|
+
except Exception as e:
|
125
|
+
return mutation_class(**{"success": False, "errors": [str(e)]})
|
126
|
+
|
127
|
+
# Assemble class dict
|
128
|
+
class_dict = {
|
129
|
+
"Arguments": Arguments,
|
130
|
+
"__doc__": fn.__doc__,
|
131
|
+
"mutate": staticmethod(_mutate),
|
132
|
+
}
|
133
|
+
class_dict.update(outputs)
|
134
|
+
|
135
|
+
# Create Mutation class
|
136
|
+
mutation_class = type(mutation_name, (graphene.Mutation,), class_dict)
|
137
|
+
|
138
|
+
if mutation_class.__name__ not in GraphQL._mutations:
|
139
|
+
GraphQL._mutations[mutation_class.__name__] = mutation_class
|
140
|
+
|
141
|
+
return fn
|
142
|
+
|
143
|
+
return decorator
|
@@ -0,0 +1,20 @@
|
|
1
|
+
from typing import Any, Callable, get_type_hints
|
2
|
+
|
3
|
+
|
4
|
+
class GraphQLProperty(property):
|
5
|
+
def __init__(self, fget: Callable[..., Any], doc: str | None = None):
|
6
|
+
super().__init__(fget, doc=doc)
|
7
|
+
self.is_graphql_resolver = True
|
8
|
+
self.graphql_type_hint = get_type_hints(fget).get("return", None)
|
9
|
+
|
10
|
+
|
11
|
+
def graphQlProperty(func: Callable[..., Any]):
|
12
|
+
from general_manager.cache.cacheDecorator import cached
|
13
|
+
|
14
|
+
"""
|
15
|
+
Dekorator für GraphQL-Feld-Resolver, der automatisch:
|
16
|
+
- die Methode als benutzerdefiniertes Property registriert,
|
17
|
+
- die Resolver-Informationen speichert,
|
18
|
+
- den Field-Typ aus dem Type-Hint ableitet.
|
19
|
+
"""
|
20
|
+
return GraphQLProperty(cached()(func))
|
general_manager/apps.py
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
from django.apps import AppConfig
|
2
|
+
import graphene
|
3
|
+
from django.conf import settings
|
4
|
+
from django.urls import path
|
5
|
+
from graphene_django.views import GraphQLView
|
6
|
+
from importlib import import_module
|
7
|
+
from general_manager.manager.generalManager import GeneralManager
|
8
|
+
from general_manager.manager.meta import GeneralManagerMeta
|
9
|
+
from general_manager.manager.input import Input
|
10
|
+
from general_manager.api.property import graphQlProperty
|
11
|
+
from general_manager.api.graphql import GraphQL
|
12
|
+
|
13
|
+
|
14
|
+
class GeneralmanagerConfig(AppConfig):
|
15
|
+
default_auto_field = "django.db.models.BigAutoField"
|
16
|
+
name = "general_manager"
|
17
|
+
|
18
|
+
def ready(self):
|
19
|
+
|
20
|
+
for general_manager_class in GeneralManagerMeta.all_classes:
|
21
|
+
attributes = getattr(general_manager_class.Interface, "input_fields", {})
|
22
|
+
for attribute_name, attribute in attributes.items():
|
23
|
+
if isinstance(attribute, Input) and issubclass(
|
24
|
+
attribute.type, GeneralManager
|
25
|
+
):
|
26
|
+
connected_manager = attribute.type
|
27
|
+
func = lambda x, attribute_name=attribute_name: general_manager_class.filter(
|
28
|
+
**{attribute_name: x}
|
29
|
+
)
|
30
|
+
|
31
|
+
func.__annotations__ = {"return": general_manager_class}
|
32
|
+
setattr(
|
33
|
+
connected_manager,
|
34
|
+
f"{general_manager_class.__name__.lower()}_list",
|
35
|
+
graphQlProperty(func),
|
36
|
+
)
|
37
|
+
|
38
|
+
for (
|
39
|
+
general_manager_class
|
40
|
+
) in GeneralManagerMeta.pending_attribute_initialization:
|
41
|
+
attributes = general_manager_class.Interface.getAttributes()
|
42
|
+
setattr(general_manager_class, "_attributes", attributes)
|
43
|
+
GeneralManagerMeta.createAtPropertiesForAttributes(
|
44
|
+
attributes, general_manager_class
|
45
|
+
)
|
46
|
+
|
47
|
+
if getattr(settings, "AUTOCREATE_GRAPHQL", False):
|
48
|
+
|
49
|
+
for general_manager_class in GeneralManagerMeta.pending_graphql_interfaces:
|
50
|
+
GraphQL.createGraphqlInterface(general_manager_class)
|
51
|
+
GraphQL.createGraphqlMutation(general_manager_class)
|
52
|
+
|
53
|
+
query_class = type("Query", (graphene.ObjectType,), GraphQL._query_fields)
|
54
|
+
GraphQL._query_class = query_class
|
55
|
+
|
56
|
+
mutation_class = type(
|
57
|
+
"Mutation",
|
58
|
+
(graphene.ObjectType,),
|
59
|
+
{
|
60
|
+
name: mutation.Field()
|
61
|
+
for name, mutation in GraphQL._mutations.items()
|
62
|
+
},
|
63
|
+
)
|
64
|
+
GraphQL._mutation_class = mutation_class
|
65
|
+
|
66
|
+
schema = graphene.Schema(
|
67
|
+
query=GraphQL._query_class,
|
68
|
+
mutation=GraphQL._mutation_class,
|
69
|
+
)
|
70
|
+
self.add_graphql_url(schema)
|
71
|
+
|
72
|
+
def add_graphql_url(self, schema):
|
73
|
+
root_url_conf_path = getattr(settings, "ROOT_URLCONF", None)
|
74
|
+
graph_ql_url = getattr(settings, "GRAPHQL_URL", "graphql/")
|
75
|
+
if not root_url_conf_path:
|
76
|
+
raise Exception("ROOT_URLCONF not found in settings")
|
77
|
+
urlconf = import_module(root_url_conf_path)
|
78
|
+
urlconf.urlpatterns.append(
|
79
|
+
path(
|
80
|
+
graph_ql_url,
|
81
|
+
GraphQLView.as_view(graphiql=True, schema=schema),
|
82
|
+
)
|
83
|
+
)
|
@@ -0,0 +1,25 @@
|
|
1
|
+
from typing import Any, Iterable
|
2
|
+
|
3
|
+
|
4
|
+
def args_to_kwargs(
|
5
|
+
args: tuple[Any, ...], keys: Iterable[Any], existing_kwargs: dict | None = None
|
6
|
+
):
|
7
|
+
"""
|
8
|
+
Wandelt *args in **kwargs um und kombiniert sie mit bestehenden **kwargs.
|
9
|
+
|
10
|
+
:param args: Tuple der positional arguments (z. B. *args).
|
11
|
+
:param keys: Liste der Schlüssel, die den Argumenten zugeordnet werden.
|
12
|
+
:param existing_kwargs: Optionales Dictionary mit bereits existierenden Schlüssel-Wert-Zuordnungen.
|
13
|
+
:return: Dictionary mit kombinierten **kwargs.
|
14
|
+
"""
|
15
|
+
keys = list(keys)
|
16
|
+
if len(args) > len(keys):
|
17
|
+
raise ValueError("Mehr args als keys vorhanden.")
|
18
|
+
|
19
|
+
kwargs = {key: value for key, value in zip(keys, args)}
|
20
|
+
if existing_kwargs and any(key in kwargs for key in existing_kwargs):
|
21
|
+
raise ValueError("Konflikte in bestehenden kwargs.")
|
22
|
+
if existing_kwargs:
|
23
|
+
kwargs.update(existing_kwargs)
|
24
|
+
|
25
|
+
return kwargs
|
@@ -0,0 +1,97 @@
|
|
1
|
+
from typing import Any, Callable, List
|
2
|
+
|
3
|
+
|
4
|
+
def parse_filters(
|
5
|
+
filter_kwargs: dict[str, Any], possible_values: dict[str, Any]
|
6
|
+
) -> dict[str, dict]:
|
7
|
+
from general_manager.manager.generalManager import GeneralManager
|
8
|
+
|
9
|
+
filters = {}
|
10
|
+
for kwarg, value in filter_kwargs.items():
|
11
|
+
parts = kwarg.split("__")
|
12
|
+
field_name = parts[0]
|
13
|
+
if field_name not in possible_values:
|
14
|
+
raise ValueError(f"Unknown input field '{field_name}' in filter")
|
15
|
+
input_field = possible_values[field_name]
|
16
|
+
|
17
|
+
lookup = "__".join(parts[1:]) if len(parts) > 1 else ""
|
18
|
+
|
19
|
+
if issubclass(input_field.type, GeneralManager):
|
20
|
+
# Sammle die Filter-Keyword-Argumente für das InputField
|
21
|
+
if lookup == "":
|
22
|
+
lookup = "id"
|
23
|
+
if not isinstance(value, GeneralManager):
|
24
|
+
value = input_field.cast(value)
|
25
|
+
value = getattr(value, "id", value)
|
26
|
+
filters.setdefault(field_name, {}).setdefault("filter_kwargs", {})[
|
27
|
+
lookup
|
28
|
+
] = value
|
29
|
+
else:
|
30
|
+
# Erstelle Filterfunktionen für Nicht-Bucket-Typen
|
31
|
+
if isinstance(value, (list, tuple)) and not isinstance(
|
32
|
+
value, input_field.type
|
33
|
+
):
|
34
|
+
casted_value = [input_field.cast(v) for v in value]
|
35
|
+
else:
|
36
|
+
casted_value = input_field.cast(value)
|
37
|
+
filter_func = create_filter_function(lookup, casted_value)
|
38
|
+
filters.setdefault(field_name, {}).setdefault("filter_funcs", []).append(
|
39
|
+
filter_func
|
40
|
+
)
|
41
|
+
return filters
|
42
|
+
|
43
|
+
|
44
|
+
def create_filter_function(lookup_str: str, value: Any) -> Callable[[Any], bool]:
|
45
|
+
parts = lookup_str.split("__") if lookup_str else []
|
46
|
+
if parts and parts[-1] in [
|
47
|
+
"exact",
|
48
|
+
"lt",
|
49
|
+
"lte",
|
50
|
+
"gt",
|
51
|
+
"gte",
|
52
|
+
"contains",
|
53
|
+
"startswith",
|
54
|
+
"endswith",
|
55
|
+
"in",
|
56
|
+
]:
|
57
|
+
lookup = parts[-1]
|
58
|
+
attr_path = parts[:-1]
|
59
|
+
else:
|
60
|
+
lookup = "exact"
|
61
|
+
attr_path = parts
|
62
|
+
|
63
|
+
def filter_func(x):
|
64
|
+
for attr in attr_path:
|
65
|
+
if hasattr(x, attr):
|
66
|
+
x = getattr(x, attr)
|
67
|
+
else:
|
68
|
+
return False
|
69
|
+
return apply_lookup(x, lookup, value)
|
70
|
+
|
71
|
+
return filter_func
|
72
|
+
|
73
|
+
|
74
|
+
def apply_lookup(x: Any, lookup: str, value: Any) -> bool:
|
75
|
+
try:
|
76
|
+
if lookup == "exact":
|
77
|
+
return x == value
|
78
|
+
elif lookup == "lt":
|
79
|
+
return x < value
|
80
|
+
elif lookup == "lte":
|
81
|
+
return x <= value
|
82
|
+
elif lookup == "gt":
|
83
|
+
return x > value
|
84
|
+
elif lookup == "gte":
|
85
|
+
return x >= value
|
86
|
+
elif lookup == "contains" and isinstance(x, str):
|
87
|
+
return value in x
|
88
|
+
elif lookup == "startswith" and isinstance(x, str):
|
89
|
+
return x.startswith(value)
|
90
|
+
elif lookup == "endswith" and isinstance(x, str):
|
91
|
+
return x.endswith(value)
|
92
|
+
elif lookup == "in":
|
93
|
+
return x in value
|
94
|
+
else:
|
95
|
+
return False
|
96
|
+
except TypeError as e:
|
97
|
+
return False
|
@@ -0,0 +1,12 @@
|
|
1
|
+
from typing import Optional, TypeVar, Literal
|
2
|
+
from general_manager.measurement import Measurement
|
3
|
+
|
4
|
+
VALUE = TypeVar("VALUE", int, float, Measurement)
|
5
|
+
|
6
|
+
|
7
|
+
def noneToZero(
|
8
|
+
value: Optional[VALUE],
|
9
|
+
) -> VALUE | Literal[0]:
|
10
|
+
if value is None:
|
11
|
+
return 0
|
12
|
+
return value
|
@@ -0,0 +1,72 @@
|
|
1
|
+
from typing import Callable, Optional
|
2
|
+
from functools import wraps
|
3
|
+
from django.core.cache import cache as django_cache
|
4
|
+
from hashlib import sha256
|
5
|
+
from general_manager.cache.cacheTracker import DependencyTracker
|
6
|
+
from general_manager.cache.dependencyIndex import record_dependencies, get_full_index
|
7
|
+
|
8
|
+
|
9
|
+
def cached(timeout: Optional[int] = None) -> Callable:
|
10
|
+
"""
|
11
|
+
Decorator to cache the result of a function for a specified timeout.
|
12
|
+
If no timeout is provided, the cache will expire when a dependency is invalidated.
|
13
|
+
"""
|
14
|
+
|
15
|
+
def decorator(func: Callable) -> Callable:
|
16
|
+
|
17
|
+
@wraps(func)
|
18
|
+
def wrapper(*args, **kwargs):
|
19
|
+
from general_manager.manager.generalManager import GeneralManager, Bucket
|
20
|
+
|
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
|
+
with DependencyTracker() as dependencies:
|
30
|
+
result = func(*args, **kwargs)
|
31
|
+
|
32
|
+
def collect_model_dependencies(obj):
|
33
|
+
"""Rekursiv Django-Model-Instanzen im Objekt finden."""
|
34
|
+
if isinstance(obj, GeneralManager):
|
35
|
+
yield (
|
36
|
+
obj.__class__.__name__,
|
37
|
+
"identification",
|
38
|
+
f"{obj.identification}",
|
39
|
+
)
|
40
|
+
elif isinstance(obj, Bucket):
|
41
|
+
yield (obj._manager_class.__name__, "filter", f"{obj.filters}")
|
42
|
+
yield (obj._manager_class.__name__, "exclude", f"{obj.excludes}")
|
43
|
+
elif isinstance(obj, dict):
|
44
|
+
for v in obj.values():
|
45
|
+
yield from collect_model_dependencies(v)
|
46
|
+
elif isinstance(obj, (list, tuple, set)):
|
47
|
+
for item in obj:
|
48
|
+
yield from collect_model_dependencies(item)
|
49
|
+
|
50
|
+
if args and isinstance(args[0], GeneralManager):
|
51
|
+
self = args[0]
|
52
|
+
for attr_val in self.__dict__.values():
|
53
|
+
for dependency_tuple in collect_model_dependencies(attr_val):
|
54
|
+
dependencies.add(dependency_tuple)
|
55
|
+
|
56
|
+
for dependency_tuple in collect_model_dependencies(args):
|
57
|
+
dependencies.add(dependency_tuple)
|
58
|
+
for dependency_tuple in collect_model_dependencies(kwargs):
|
59
|
+
dependencies.add(dependency_tuple)
|
60
|
+
|
61
|
+
django_cache.set(django_cache_key, result, timeout)
|
62
|
+
|
63
|
+
if dependencies and not timeout:
|
64
|
+
record_dependencies(
|
65
|
+
django_cache_key,
|
66
|
+
dependencies,
|
67
|
+
)
|
68
|
+
return result
|
69
|
+
|
70
|
+
return wrapper
|
71
|
+
|
72
|
+
return decorator
|
@@ -0,0 +1,33 @@
|
|
1
|
+
import threading
|
2
|
+
from general_manager.cache.dependencyIndex import general_manager_name
|
3
|
+
from typing import Any, Literal
|
4
|
+
|
5
|
+
|
6
|
+
# Thread-lokale Variable zur Speicherung der Abhängigkeiten
|
7
|
+
_dependency_storage = threading.local()
|
8
|
+
|
9
|
+
|
10
|
+
class DependencyTracker:
|
11
|
+
def __enter__(
|
12
|
+
self,
|
13
|
+
) -> set[
|
14
|
+
tuple[general_manager_name, Literal["filter", "exclude", "identification"], str]
|
15
|
+
]:
|
16
|
+
_dependency_storage.dependencies = set()
|
17
|
+
return _dependency_storage.dependencies
|
18
|
+
|
19
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
20
|
+
# Optional: Aufräumen oder weitere Verarbeitung
|
21
|
+
pass
|
22
|
+
|
23
|
+
|
24
|
+
def addDependency(class_name: str, operation: str, identifier: str) -> None:
|
25
|
+
"""
|
26
|
+
Adds a dependency to the dependency storage.
|
27
|
+
"""
|
28
|
+
if hasattr(_dependency_storage, "dependencies"):
|
29
|
+
dependencies: set[
|
30
|
+
tuple[general_manager_name, Literal["filter", "exclude", "id"], str]
|
31
|
+
] = _dependency_storage.dependencies
|
32
|
+
|
33
|
+
_dependency_storage.dependencies.add((class_name, operation, identifier))
|