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,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))
@@ -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,2 @@
1
+ from .noneToZero import noneToZero
2
+ from .argsToKwargs import args_to_kwargs
@@ -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))