GeneralManager 0.6.1__tar.gz → 0.7.0__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 (93) hide show
  1. {generalmanager-0.6.1 → generalmanager-0.7.0}/GeneralManager.egg-info/PKG-INFO +1 -1
  2. {generalmanager-0.6.1 → generalmanager-0.7.0}/GeneralManager.egg-info/SOURCES.txt +2 -0
  3. {generalmanager-0.6.1 → generalmanager-0.7.0}/PKG-INFO +1 -1
  4. {generalmanager-0.6.1 → generalmanager-0.7.0}/pyproject.toml +1 -1
  5. generalmanager-0.7.0/src/general_manager/apps.py +170 -0
  6. {generalmanager-0.6.1 → generalmanager-0.7.0}/src/general_manager/cache/dependencyIndex.py +10 -2
  7. {generalmanager-0.6.1 → generalmanager-0.7.0}/src/general_manager/interface/baseInterface.py +8 -9
  8. {generalmanager-0.6.1 → generalmanager-0.7.0}/src/general_manager/interface/databaseBasedInterface.py +28 -13
  9. {generalmanager-0.6.1 → generalmanager-0.7.0}/src/general_manager/interface/databaseInterface.py +58 -34
  10. generalmanager-0.7.0/src/general_manager/interface/readOnlyInterface.py +265 -0
  11. {generalmanager-0.6.1 → generalmanager-0.7.0}/src/general_manager/manager/meta.py +1 -1
  12. generalmanager-0.7.0/tests/test_databaseInterface.py +181 -0
  13. generalmanager-0.7.0/tests/test_readOnlyInterface.py +370 -0
  14. generalmanager-0.6.1/src/general_manager/apps.py +0 -83
  15. generalmanager-0.6.1/src/general_manager/interface/readOnlyInterface.py +0 -107
  16. {generalmanager-0.6.1 → generalmanager-0.7.0}/GeneralManager.egg-info/dependency_links.txt +0 -0
  17. {generalmanager-0.6.1 → generalmanager-0.7.0}/GeneralManager.egg-info/requires.txt +0 -0
  18. {generalmanager-0.6.1 → generalmanager-0.7.0}/GeneralManager.egg-info/top_level.txt +0 -0
  19. {generalmanager-0.6.1 → generalmanager-0.7.0}/LICENSE +0 -0
  20. {generalmanager-0.6.1 → generalmanager-0.7.0}/README.md +0 -0
  21. {generalmanager-0.6.1 → generalmanager-0.7.0}/setup.cfg +0 -0
  22. {generalmanager-0.6.1 → generalmanager-0.7.0}/src/general_manager/__init__.py +0 -0
  23. {generalmanager-0.6.1 → generalmanager-0.7.0}/src/general_manager/api/graphql.py +0 -0
  24. {generalmanager-0.6.1 → generalmanager-0.7.0}/src/general_manager/api/mutation.py +0 -0
  25. {generalmanager-0.6.1 → generalmanager-0.7.0}/src/general_manager/api/property.py +0 -0
  26. {generalmanager-0.6.1 → generalmanager-0.7.0}/src/general_manager/auxiliary/__init__.py +0 -0
  27. {generalmanager-0.6.1 → generalmanager-0.7.0}/src/general_manager/auxiliary/argsToKwargs.py +0 -0
  28. {generalmanager-0.6.1 → generalmanager-0.7.0}/src/general_manager/auxiliary/filterParser.py +0 -0
  29. {generalmanager-0.6.1 → generalmanager-0.7.0}/src/general_manager/auxiliary/jsonEncoder.py +0 -0
  30. {generalmanager-0.6.1 → generalmanager-0.7.0}/src/general_manager/auxiliary/makeCacheKey.py +0 -0
  31. {generalmanager-0.6.1 → generalmanager-0.7.0}/src/general_manager/auxiliary/noneToZero.py +0 -0
  32. {generalmanager-0.6.1 → generalmanager-0.7.0}/src/general_manager/auxiliary/pathMapping.py +0 -0
  33. {generalmanager-0.6.1 → generalmanager-0.7.0}/src/general_manager/bucket/baseBucket.py +0 -0
  34. {generalmanager-0.6.1 → generalmanager-0.7.0}/src/general_manager/bucket/calculationBucket.py +0 -0
  35. {generalmanager-0.6.1 → generalmanager-0.7.0}/src/general_manager/bucket/databaseBucket.py +0 -0
  36. {generalmanager-0.6.1 → generalmanager-0.7.0}/src/general_manager/bucket/groupBucket.py +0 -0
  37. {generalmanager-0.6.1 → generalmanager-0.7.0}/src/general_manager/cache/cacheDecorator.py +0 -0
  38. {generalmanager-0.6.1 → generalmanager-0.7.0}/src/general_manager/cache/cacheTracker.py +0 -0
  39. {generalmanager-0.6.1 → generalmanager-0.7.0}/src/general_manager/cache/modelDependencyCollector.py +0 -0
  40. {generalmanager-0.6.1 → generalmanager-0.7.0}/src/general_manager/cache/signals.py +0 -0
  41. {generalmanager-0.6.1 → generalmanager-0.7.0}/src/general_manager/factory/__init__.py +0 -0
  42. {generalmanager-0.6.1 → generalmanager-0.7.0}/src/general_manager/factory/autoFactory.py +0 -0
  43. {generalmanager-0.6.1 → generalmanager-0.7.0}/src/general_manager/factory/factories.py +0 -0
  44. {generalmanager-0.6.1 → generalmanager-0.7.0}/src/general_manager/factory/factoryMethods.py +0 -0
  45. {generalmanager-0.6.1 → generalmanager-0.7.0}/src/general_manager/interface/__init__.py +0 -0
  46. {generalmanager-0.6.1 → generalmanager-0.7.0}/src/general_manager/interface/calculationInterface.py +0 -0
  47. {generalmanager-0.6.1 → generalmanager-0.7.0}/src/general_manager/manager/__init__.py +0 -0
  48. {generalmanager-0.6.1 → generalmanager-0.7.0}/src/general_manager/manager/generalManager.py +0 -0
  49. {generalmanager-0.6.1 → generalmanager-0.7.0}/src/general_manager/manager/groupManager.py +0 -0
  50. {generalmanager-0.6.1 → generalmanager-0.7.0}/src/general_manager/manager/input.py +0 -0
  51. {generalmanager-0.6.1 → generalmanager-0.7.0}/src/general_manager/measurement/__init__.py +0 -0
  52. {generalmanager-0.6.1 → generalmanager-0.7.0}/src/general_manager/measurement/measurement.py +0 -0
  53. {generalmanager-0.6.1 → generalmanager-0.7.0}/src/general_manager/measurement/measurementField.py +0 -0
  54. {generalmanager-0.6.1 → generalmanager-0.7.0}/src/general_manager/permission/__init__.py +0 -0
  55. {generalmanager-0.6.1 → generalmanager-0.7.0}/src/general_manager/permission/basePermission.py +0 -0
  56. {generalmanager-0.6.1 → generalmanager-0.7.0}/src/general_manager/permission/fileBasedPermission.py +0 -0
  57. {generalmanager-0.6.1 → generalmanager-0.7.0}/src/general_manager/permission/managerBasedPermission.py +0 -0
  58. {generalmanager-0.6.1 → generalmanager-0.7.0}/src/general_manager/permission/permissionChecks.py +0 -0
  59. {generalmanager-0.6.1 → generalmanager-0.7.0}/src/general_manager/permission/permissionDataManager.py +0 -0
  60. {generalmanager-0.6.1 → generalmanager-0.7.0}/src/general_manager/rule/__init__.py +0 -0
  61. {generalmanager-0.6.1 → generalmanager-0.7.0}/src/general_manager/rule/handler.py +0 -0
  62. {generalmanager-0.6.1 → generalmanager-0.7.0}/src/general_manager/rule/rule.py +0 -0
  63. {generalmanager-0.6.1 → generalmanager-0.7.0}/tests/test_argsToKwargs.py +0 -0
  64. {generalmanager-0.6.1 → generalmanager-0.7.0}/tests/test_autoFactory.py +0 -0
  65. {generalmanager-0.6.1 → generalmanager-0.7.0}/tests/test_baseBucket.py +0 -0
  66. {generalmanager-0.6.1 → generalmanager-0.7.0}/tests/test_baseInterface.py +0 -0
  67. {generalmanager-0.6.1 → generalmanager-0.7.0}/tests/test_basePermission.py +0 -0
  68. {generalmanager-0.6.1 → generalmanager-0.7.0}/tests/test_cacheDecorator.py +0 -0
  69. {generalmanager-0.6.1 → generalmanager-0.7.0}/tests/test_cacheTracker.py +0 -0
  70. {generalmanager-0.6.1 → generalmanager-0.7.0}/tests/test_calculationBucket.py +0 -0
  71. {generalmanager-0.6.1 → generalmanager-0.7.0}/tests/test_calculationInterface.py +0 -0
  72. {generalmanager-0.6.1 → generalmanager-0.7.0}/tests/test_databaseBasedInterface.py +0 -0
  73. {generalmanager-0.6.1 → generalmanager-0.7.0}/tests/test_databaseBucket.py +0 -0
  74. {generalmanager-0.6.1 → generalmanager-0.7.0}/tests/test_dependencyIndex.py +0 -0
  75. {generalmanager-0.6.1 → generalmanager-0.7.0}/tests/test_factories.py +0 -0
  76. {generalmanager-0.6.1 → generalmanager-0.7.0}/tests/test_factoryMethods.py +0 -0
  77. {generalmanager-0.6.1 → generalmanager-0.7.0}/tests/test_filterParser.py +0 -0
  78. {generalmanager-0.6.1 → generalmanager-0.7.0}/tests/test_generalManager.py +0 -0
  79. {generalmanager-0.6.1 → generalmanager-0.7.0}/tests/test_generalManagerMeta.py +0 -0
  80. {generalmanager-0.6.1 → generalmanager-0.7.0}/tests/test_graph_ql.py +0 -0
  81. {generalmanager-0.6.1 → generalmanager-0.7.0}/tests/test_groupManager.py +0 -0
  82. {generalmanager-0.6.1 → generalmanager-0.7.0}/tests/test_input.py +0 -0
  83. {generalmanager-0.6.1 → generalmanager-0.7.0}/tests/test_jsonEncoder.py +0 -0
  84. {generalmanager-0.6.1 → generalmanager-0.7.0}/tests/test_makeCacheKey.py +0 -0
  85. {generalmanager-0.6.1 → generalmanager-0.7.0}/tests/test_managerBasedPermission.py +0 -0
  86. {generalmanager-0.6.1 → generalmanager-0.7.0}/tests/test_measurement.py +0 -0
  87. {generalmanager-0.6.1 → generalmanager-0.7.0}/tests/test_measurement_field.py +0 -0
  88. {generalmanager-0.6.1 → generalmanager-0.7.0}/tests/test_modelDependencyCollector.py +0 -0
  89. {generalmanager-0.6.1 → generalmanager-0.7.0}/tests/test_noneToZero.py +0 -0
  90. {generalmanager-0.6.1 → generalmanager-0.7.0}/tests/test_rule_handler.py +0 -0
  91. {generalmanager-0.6.1 → generalmanager-0.7.0}/tests/test_rules.py +0 -0
  92. {generalmanager-0.6.1 → generalmanager-0.7.0}/tests/test_settings.py +0 -0
  93. {generalmanager-0.6.1 → generalmanager-0.7.0}/tests/test_signals.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GeneralManager
3
- Version: 0.6.1
3
+ Version: 0.7.0
4
4
  Summary: Modular Django-based data management framework with ORM, GraphQL, fine-grained permissions, rule validation, calculations and caching.
5
5
  Author-email: Tim Kleindick <tkleindick@yahoo.de>
6
6
  License-Expression: MIT
@@ -65,6 +65,7 @@ tests/test_calculationBucket.py
65
65
  tests/test_calculationInterface.py
66
66
  tests/test_databaseBasedInterface.py
67
67
  tests/test_databaseBucket.py
68
+ tests/test_databaseInterface.py
68
69
  tests/test_dependencyIndex.py
69
70
  tests/test_factories.py
70
71
  tests/test_factoryMethods.py
@@ -81,6 +82,7 @@ tests/test_measurement.py
81
82
  tests/test_measurement_field.py
82
83
  tests/test_modelDependencyCollector.py
83
84
  tests/test_noneToZero.py
85
+ tests/test_readOnlyInterface.py
84
86
  tests/test_rule_handler.py
85
87
  tests/test_rules.py
86
88
  tests/test_settings.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GeneralManager
3
- Version: 0.6.1
3
+ Version: 0.7.0
4
4
  Summary: Modular Django-based data management framework with ORM, GraphQL, fine-grained permissions, rule validation, calculations and caching.
5
5
  Author-email: Tim Kleindick <tkleindick@yahoo.de>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "GeneralManager"
7
- version = "0.6.1"
7
+ version = "0.7.0"
8
8
  description = "Modular Django-based data management framework with ORM, GraphQL, fine-grained permissions, rule validation, calculations and caching."
9
9
  readme = "README.md"
10
10
  authors = [{ name = "Tim Kleindick", email = "tkleindick@yahoo.de" }]
@@ -0,0 +1,170 @@
1
+ from django.apps import AppConfig
2
+ import graphene
3
+ import os
4
+ from django.conf import settings
5
+ from django.urls import path
6
+ from graphene_django.views import GraphQLView
7
+ from importlib import import_module
8
+ from general_manager.manager.generalManager import GeneralManager
9
+ from general_manager.manager.meta import GeneralManagerMeta
10
+ from general_manager.manager.input import Input
11
+ from general_manager.api.property import graphQlProperty
12
+ from general_manager.api.graphql import GraphQL
13
+ from typing import TYPE_CHECKING, Type
14
+ from django.core.checks import register
15
+ import logging
16
+
17
+
18
+ if TYPE_CHECKING:
19
+ from general_manager.interface.readOnlyInterface import ReadOnlyInterface
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ class GeneralmanagerConfig(AppConfig):
25
+ default_auto_field = "django.db.models.BigAutoField"
26
+ name = "general_manager"
27
+
28
+ def ready(self):
29
+ """
30
+ Initializes the general_manager app when Django starts.
31
+
32
+ Sets up read-only interface synchronization and schema validation, initializes general manager class attributes and connections, and conditionally configures the GraphQL schema and endpoint based on settings.
33
+ """
34
+ self.handleReadOnlyInterface()
35
+ self.initializeGeneralManagerClasses()
36
+ if getattr(settings, "AUTOCREATE_GRAPHQL", False):
37
+ self.handleGraphQL()
38
+
39
+ def handleReadOnlyInterface(self):
40
+ """
41
+ Sets up synchronization and schema validation for all registered read-only interfaces.
42
+
43
+ This method patches Django's management command execution to ensure read-only interfaces are synchronized during server runs. It also registers system checks for each read-only interface to validate that their schemas are up to date.
44
+ """
45
+ self.patchReadOnlyInterfaceSync(GeneralManagerMeta.read_only_classes)
46
+ from general_manager.interface.readOnlyInterface import ReadOnlyInterface
47
+
48
+ logger.debug("starting to register ReadOnlyInterface schema warnings...")
49
+ for general_manager_class in GeneralManagerMeta.read_only_classes:
50
+ read_only_interface: ReadOnlyInterface = general_manager_class.Interface # type: ignore
51
+
52
+ register(
53
+ lambda app_configs, model=read_only_interface._model, manager_class=general_manager_class, **kwargs: ReadOnlyInterface.ensureSchemaIsUpToDate(
54
+ manager_class, model
55
+ ),
56
+ "general_manager",
57
+ )
58
+
59
+ @staticmethod
60
+ def patchReadOnlyInterfaceSync(general_manager_classes: list[Type[GeneralManager]]):
61
+ """
62
+ Monkey-patches Django's management command runner to synchronize read-only interface data before executing commands.
63
+
64
+ This ensures that for each provided general manager class, its associated read-only interface's `syncData` method is called before running management commands, except during autoreload subprocesses for `runserver`.
65
+ """
66
+ from django.core.management.base import BaseCommand
67
+
68
+ original_run_from_argv = BaseCommand.run_from_argv
69
+
70
+ def run_from_argv_with_sync(self, argv):
71
+ # Ensure syncData is only called at real run of runserver
72
+ """
73
+ Runs the management command and synchronizes read-only interface data before execution when appropriate.
74
+
75
+ Synchronization occurs for all registered read-only interfaces unless the command is 'runserver' in an autoreload subprocess.
76
+ """
77
+ run_main = os.environ.get("RUN_MAIN") == "true"
78
+ command = argv[1] if len(argv) > 1 else None
79
+ if command != "runserver" or run_main:
80
+ logger.debug("start syncing ReadOnlyInterface data...")
81
+ for general_manager_class in general_manager_classes:
82
+ read_only_interface: ReadOnlyInterface = general_manager_class.Interface # type: ignore
83
+ read_only_interface.syncData()
84
+
85
+ logger.debug("finished syncing ReadOnlyInterface data.")
86
+
87
+ return original_run_from_argv(self, argv)
88
+
89
+ BaseCommand.run_from_argv = run_from_argv_with_sync
90
+
91
+ def initializeGeneralManagerClasses(self):
92
+ """
93
+ Initializes attributes and interconnections for all GeneralManager classes.
94
+
95
+ For each pending GeneralManager class, sets up its attributes and creates property accessors. Then, for all GeneralManager classes, connects input fields referencing other GeneralManager subclasses by dynamically adding GraphQL properties to enable filtered access to related objects.
96
+ """
97
+ logger.debug("Initializing GeneralManager classes...")
98
+
99
+ logger.debug("starting to create attributes for GeneralManager classes...")
100
+ for (
101
+ general_manager_class
102
+ ) in GeneralManagerMeta.pending_attribute_initialization:
103
+ attributes = general_manager_class.Interface.getAttributes()
104
+ setattr(general_manager_class, "_attributes", attributes)
105
+ GeneralManagerMeta.createAtPropertiesForAttributes(
106
+ attributes.keys(), general_manager_class
107
+ )
108
+
109
+ logger.debug("starting to connect inputs to other general manager classes...")
110
+ for general_manager_class in GeneralManagerMeta.all_classes:
111
+ attributes = getattr(general_manager_class.Interface, "input_fields", {})
112
+ for attribute_name, attribute in attributes.items():
113
+ if isinstance(attribute, Input) and issubclass(
114
+ attribute.type, GeneralManager
115
+ ):
116
+ connected_manager = attribute.type
117
+ func = lambda x, attribute_name=attribute_name: general_manager_class.filter(
118
+ **{attribute_name: x}
119
+ )
120
+
121
+ func.__annotations__ = {"return": general_manager_class}
122
+ setattr(
123
+ connected_manager,
124
+ f"{general_manager_class.__name__.lower()}_list",
125
+ graphQlProperty(func),
126
+ )
127
+
128
+ def handleGraphQL(self):
129
+ """
130
+ Sets up GraphQL interfaces, mutations, and schema for all pending general manager classes, and adds the GraphQL endpoint to the Django URL configuration.
131
+ """
132
+ logger.debug("Starting to create GraphQL interfaces and mutations...")
133
+ for general_manager_class in GeneralManagerMeta.pending_graphql_interfaces:
134
+ GraphQL.createGraphqlInterface(general_manager_class)
135
+ GraphQL.createGraphqlMutation(general_manager_class)
136
+
137
+ query_class = type("Query", (graphene.ObjectType,), GraphQL._query_fields)
138
+ GraphQL._query_class = query_class
139
+
140
+ mutation_class = type(
141
+ "Mutation",
142
+ (graphene.ObjectType,),
143
+ {name: mutation.Field() for name, mutation in GraphQL._mutations.items()},
144
+ )
145
+ GraphQL._mutation_class = mutation_class
146
+
147
+ schema = graphene.Schema(
148
+ query=GraphQL._query_class,
149
+ mutation=GraphQL._mutation_class,
150
+ )
151
+ self.addGraphqlUrl(schema)
152
+
153
+ def addGraphqlUrl(self, schema):
154
+ """
155
+ Dynamically adds a GraphQL endpoint to the Django URL configuration using the provided schema.
156
+
157
+ Raises an exception if the ROOT_URLCONF setting is not defined.
158
+ """
159
+ logging.debug("Adding GraphQL URL to Django settings...")
160
+ root_url_conf_path = getattr(settings, "ROOT_URLCONF", None)
161
+ graph_ql_url = getattr(settings, "GRAPHQL_URL", "graphql/")
162
+ if not root_url_conf_path:
163
+ raise Exception("ROOT_URLCONF not found in settings")
164
+ urlconf = import_module(root_url_conf_path)
165
+ urlconf.urlpatterns.append(
166
+ path(
167
+ graph_ql_url,
168
+ GraphQLView.as_view(graphiql=True, schema=schema),
169
+ )
170
+ )
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
  import time
3
3
  import ast
4
4
  import re
5
+ import logging
5
6
 
6
7
  from django.core.cache import cache
7
8
  from general_manager.cache.signals import post_data_change, pre_data_change
@@ -27,6 +28,8 @@ type dependency_index = dict[
27
28
  type filter_type = Literal["filter", "exclude", "identification"]
28
29
  type Dependency = Tuple[general_manager_name, filter_type, str]
29
30
 
31
+ logger = logging.getLogger(__name__)
32
+
30
33
  # -----------------------------------------------------------------------------
31
34
  # CONFIG
32
35
  # -----------------------------------------------------------------------------
@@ -181,6 +184,11 @@ def generic_cache_invalidation(
181
184
  old_relevant_values: dict[str, Any],
182
185
  **kwargs,
183
186
  ):
187
+ """
188
+ Invalidates cached query results related to a model instance when its data changes.
189
+
190
+ This function is intended to be used as a Django signal handler. It compares old and new values of relevant fields on a model instance against registered cache dependencies (filters and excludes). If a change affects any cached queryset result, the corresponding cache keys are invalidated and removed from the dependency index.
191
+ """
184
192
  manager_name = sender.__name__
185
193
  idx = get_full_index()
186
194
 
@@ -280,7 +288,7 @@ def generic_cache_invalidation(
280
288
  if action == "filter":
281
289
  # Filter: invalidate if new match or old match
282
290
  if new_match or old_match:
283
- print(
291
+ logger.info(
284
292
  f"Invalidate cache key {cache_keys} for filter {lookup} with value {val_key}"
285
293
  )
286
294
  for ck in list(cache_keys):
@@ -290,7 +298,7 @@ def generic_cache_invalidation(
290
298
  else: # action == 'exclude'
291
299
  # Excludes: invalidate only if matches changed
292
300
  if old_match != new_match:
293
- print(
301
+ logger.info(
294
302
  f"Invalidate cache key {cache_keys} for exclude {lookup} with value {val_key}"
295
303
  )
296
304
  for ck in list(cache_keys):
@@ -18,7 +18,6 @@ from general_manager.auxiliary import args_to_kwargs
18
18
  if TYPE_CHECKING:
19
19
  from general_manager.manager.input import Input
20
20
  from general_manager.manager.generalManager import GeneralManager
21
- from general_manager.manager.meta import GeneralManagerMeta
22
21
  from general_manager.bucket.baseBucket import Bucket
23
22
 
24
23
 
@@ -28,7 +27,7 @@ type attributes = dict[str, Any]
28
27
  type interfaceBaseClass = Type[InterfaceBase]
29
28
  type newlyCreatedInterfaceClass = Type[InterfaceBase]
30
29
  type relatedClass = Type[Model] | None
31
- type newlyCreatedGeneralManagerClass = GeneralManagerMeta
30
+ type newlyCreatedGeneralManagerClass = Type[GeneralManager]
32
31
 
33
32
  type classPreCreationMethod = Callable[
34
33
  [generalManagerClassName, attributes, interfaceBaseClass],
@@ -55,7 +54,7 @@ class AttributeTypedDict(TypedDict):
55
54
 
56
55
 
57
56
  class InterfaceBase(ABC):
58
- _parent_class: ClassVar[Type[Any]]
57
+ _parent_class: Type[GeneralManager]
59
58
  _interface_type: ClassVar[str]
60
59
  input_fields: dict[str, Input]
61
60
 
@@ -70,9 +69,9 @@ class InterfaceBase(ABC):
70
69
  ) -> dict[str, Any]:
71
70
  """
72
71
  Parses and validates input arguments into a structured identification dictionary.
73
-
72
+
74
73
  Converts positional and keyword arguments into a dictionary keyed by input field names, normalizing argument names and ensuring all required fields are present. Processes input fields in dependency order, casting and validating each value. Raises a `TypeError` for unexpected or missing arguments and a `ValueError` if circular dependencies among input fields are detected.
75
-
74
+
76
75
  Returns:
77
76
  A dictionary mapping input field names to their validated and cast values.
78
77
  """
@@ -131,7 +130,7 @@ class InterfaceBase(ABC):
131
130
  ) -> None:
132
131
  """
133
132
  Validates the type and allowed values of an input field.
134
-
133
+
135
134
  Ensures that the provided value matches the expected type for the specified input field. In debug mode, also checks that the value is among the allowed possible values if defined, supporting both callables and iterables. Raises a TypeError for invalid types or possible value definitions, and a ValueError if the value is not permitted.
136
135
  """
137
136
  input_field = self.input_fields[name]
@@ -218,13 +217,13 @@ class InterfaceBase(ABC):
218
217
  def getFieldType(cls, field_name: str) -> type:
219
218
  """
220
219
  Returns the type of the specified input field.
221
-
220
+
222
221
  Args:
223
222
  field_name: The name of the input field.
224
-
223
+
225
224
  Returns:
226
225
  The Python type associated with the given field name.
227
-
226
+
228
227
  Raises:
229
228
  NotImplementedError: This method must be implemented by subclasses.
230
229
  """
@@ -66,12 +66,18 @@ def getFullCleanMethode(model: Type[models.Model]) -> Callable[..., None]:
66
66
  return full_clean
67
67
 
68
68
 
69
- class GeneralManagerModel(models.Model):
69
+ class GeneralManagerBasisModel(models.Model):
70
70
  _general_manager_class: ClassVar[Type[GeneralManager]]
71
71
  is_active = models.BooleanField(default=True)
72
+ history = HistoricalRecords(inherit=True)
73
+
74
+ class Meta:
75
+ abstract = True
76
+
77
+
78
+ class GeneralManagerModel(GeneralManagerBasisModel):
72
79
  changed_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
73
80
  changed_by_id: int
74
- history = HistoricalRecords(inherit=True)
75
81
 
76
82
  @property
77
83
  def _history_user(self) -> AbstractUser:
@@ -90,12 +96,12 @@ class GeneralManagerModel(models.Model):
90
96
  """
91
97
  self.changed_by = value
92
98
 
93
- class Meta:
99
+ class Meta: # type: ignore
94
100
  abstract = True
95
101
 
96
102
 
97
103
  class DBBasedInterface(InterfaceBase):
98
- _model: ClassVar[Type[GeneralManagerModel]]
104
+ _model: Type[GeneralManagerBasisModel]
99
105
  input_fields: dict[str, Input] = {"id": Input(int)}
100
106
 
101
107
  def __init__(
@@ -113,7 +119,7 @@ class DBBasedInterface(InterfaceBase):
113
119
  self.pk = self.identification["id"]
114
120
  self._instance = self.getData(search_date)
115
121
 
116
- def getData(self, search_date: datetime | None = None) -> GeneralManagerModel:
122
+ def getData(self, search_date: datetime | None = None) -> GeneralManagerBasisModel:
117
123
  """
118
124
  Retrieves the model instance by primary key, optionally as of a specified historical date.
119
125
 
@@ -177,8 +183,8 @@ class DBBasedInterface(InterfaceBase):
177
183
 
178
184
  @classmethod
179
185
  def getHistoricalRecord(
180
- cls, instance: GeneralManagerModel, search_date: datetime | None = None
181
- ) -> GeneralManagerModel:
186
+ cls, instance: GeneralManagerBasisModel, search_date: datetime | None = None
187
+ ) -> GeneralManagerBasisModel:
182
188
  """
183
189
  Retrieves the most recent historical record of a model instance at or before a specified date.
184
190
 
@@ -440,14 +446,23 @@ class DBBasedInterface(InterfaceBase):
440
446
 
441
447
  @staticmethod
442
448
  def _preCreate(
443
- name: generalManagerClassName, attrs: attributes, interface: interfaceBaseClass
449
+ name: generalManagerClassName,
450
+ attrs: attributes,
451
+ interface: interfaceBaseClass,
452
+ base_model_class=GeneralManagerModel,
444
453
  ) -> tuple[attributes, interfaceBaseClass, relatedClass]:
445
454
  # Felder aus der Interface-Klasse sammeln
446
455
  """
447
- Dynamically creates a Django model, its associated interface class, and a factory class based on the provided interface definition.
448
-
449
- This method extracts fields and meta information from the interface class, constructs a new Django model inheriting from `GeneralManagerModel`, attaches custom validation rules if present, and generates a corresponding interface and factory class. The resulting classes are returned for further use in the general manager framework.
450
-
456
+ Dynamically creates a Django model class, its associated interface class, and a factory class based on the provided interface definition.
457
+
458
+ This method extracts fields and meta information from the interface class, constructs a new Django model inheriting from the specified base model class, attaches custom validation rules if present, and generates corresponding interface and factory classes. The resulting classes are returned for integration into the general manager framework.
459
+
460
+ Parameters:
461
+ name: The name for the dynamically created model class.
462
+ attrs: The attributes dictionary to be updated with the new interface and factory classes.
463
+ interface: The interface base class defining the model structure and metadata.
464
+ base_model_class: The base class to use for the new model (defaults to GeneralManagerModel).
465
+
451
466
  Returns:
452
467
  A tuple containing the updated attributes dictionary, the new interface class, and the newly created model class.
453
468
  """
@@ -474,7 +489,7 @@ class DBBasedInterface(InterfaceBase):
474
489
  delattr(meta_class, "rules")
475
490
 
476
491
  # Modell erstellen
477
- model = type(name, (GeneralManagerModel,), model_fields)
492
+ model = type(name, (base_model_class,), model_fields)
478
493
  if meta_class and rules:
479
494
  setattr(model._meta, "rules", rules)
480
495
  # full_clean Methode hinzufügen
@@ -18,36 +18,24 @@ class DatabaseInterface(DBBasedInterface):
18
18
  def create(
19
19
  cls, creator_id: int, history_comment: str | None = None, **kwargs: Any
20
20
  ) -> int:
21
- from general_manager.manager.generalManager import GeneralManager
22
21
 
23
- cls.__checkForInvalidKwargs(cls._model, kwargs=kwargs)
24
- kwargs, many_to_many_kwargs = cls.__sortKwargs(cls._model, kwargs)
25
- instance = cls._model()
26
- for key, value in kwargs.items():
27
- if isinstance(value, GeneralManager):
28
- value = value.identification["id"]
29
- key = f"{key}_id"
30
- setattr(instance, key, value)
31
- for key, value in many_to_many_kwargs.items():
32
- getattr(instance, key).set(value)
33
- return cls.__save_with_history(instance, creator_id, history_comment)
22
+ cls._checkForInvalidKwargs(cls._model, kwargs=kwargs)
23
+ kwargs, many_to_many_kwargs = cls._sortKwargs(cls._model, kwargs)
24
+ instance = cls.__setAttrForWrite(cls._model(), kwargs)
25
+ pk = cls._save_with_history(instance, creator_id, history_comment)
26
+ cls.__setManyToManyAttributes(instance, many_to_many_kwargs)
27
+ return pk
34
28
 
35
29
  def update(
36
30
  self, creator_id: int, history_comment: str | None = None, **kwargs: Any
37
31
  ) -> int:
38
- from general_manager.manager.generalManager import GeneralManager
39
32
 
40
- self.__checkForInvalidKwargs(self._model, kwargs=kwargs)
41
- kwargs, many_to_many_kwargs = self.__sortKwargs(self._model, kwargs)
42
- instance = self._model.objects.get(pk=self.pk)
43
- for key, value in kwargs.items():
44
- if isinstance(value, GeneralManager):
45
- value = value.identification["id"]
46
- key = f"{key}_id"
47
- setattr(instance, key, value)
48
- for key, value in many_to_many_kwargs.items():
49
- getattr(instance, key).set(value)
50
- return self.__save_with_history(instance, creator_id, history_comment)
33
+ self._checkForInvalidKwargs(self._model, kwargs=kwargs)
34
+ kwargs, many_to_many_kwargs = self._sortKwargs(self._model, kwargs)
35
+ instance = self.__setAttrForWrite(self._model.objects.get(pk=self.pk), kwargs)
36
+ pk = self._save_with_history(instance, creator_id, history_comment)
37
+ self.__setManyToManyAttributes(instance, many_to_many_kwargs)
38
+ return pk
51
39
 
52
40
  def deactivate(self, creator_id: int, history_comment: str | None = None) -> int:
53
41
  instance = self._model.objects.get(pk=self.pk)
@@ -56,32 +44,68 @@ class DatabaseInterface(DBBasedInterface):
56
44
  history_comment = f"{history_comment} (deactivated)"
57
45
  else:
58
46
  history_comment = "Deactivated"
59
- return self.__save_with_history(instance, creator_id, history_comment)
47
+ return self._save_with_history(instance, creator_id, history_comment)
60
48
 
61
49
  @staticmethod
62
- def __checkForInvalidKwargs(model: Type[models.Model], kwargs: dict[Any, Any]):
50
+ def __setManyToManyAttributes(
51
+ instance: GeneralManagerModel, many_to_many_kwargs: dict[str, list[Any]]
52
+ ) -> GeneralManagerModel:
53
+ """
54
+ Sets many-to-many attributes for the given instance based on the provided kwargs.
55
+
56
+ Args:
57
+ instance: The model instance to update.
58
+ many_to_many_kwargs: A dictionary containing many-to-many field names and their corresponding values.
59
+
60
+ Returns:
61
+ The updated model instance.
62
+ """
63
+ for key, value in many_to_many_kwargs.items():
64
+ if not value:
65
+ continue
66
+ field_name = key.split("_id_list")[0]
67
+ getattr(instance, field_name).set(value)
68
+
69
+ return instance
70
+
71
+ @staticmethod
72
+ def __setAttrForWrite(
73
+ instance: GeneralManagerModel,
74
+ kwargs: dict[str, Any],
75
+ ) -> GeneralManagerModel:
76
+ from general_manager.manager.generalManager import GeneralManager
77
+
78
+ for key, value in kwargs.items():
79
+ if isinstance(value, GeneralManager):
80
+ value = value.identification["id"]
81
+ key = f"{key}_id"
82
+ setattr(instance, key, value)
83
+ return instance
84
+
85
+ @staticmethod
86
+ def _checkForInvalidKwargs(model: Type[models.Model], kwargs: dict[str, Any]):
63
87
  attributes = vars(model)
64
- fields = model._meta.get_fields()
88
+ field_names = {f.name for f in model._meta.get_fields()}
65
89
  for key in kwargs:
66
- if key not in attributes and key not in fields:
90
+ temp_key = key.split("_id_list")[0] # Remove '_id_list' suffix
91
+ if temp_key not in attributes and temp_key not in field_names:
67
92
  raise ValueError(f"{key} does not exsist in {model.__name__}")
68
93
 
69
94
  @staticmethod
70
- def __sortKwargs(
95
+ def _sortKwargs(
71
96
  model: Type[models.Model], kwargs: dict[Any, Any]
72
97
  ) -> tuple[dict[str, Any], dict[str, list[Any]]]:
73
- many_to_many_fields = model._meta.many_to_many
98
+ many_to_many_fields = [field.name for field in model._meta.many_to_many]
74
99
  many_to_many_kwargs: dict[Any, Any] = {}
75
- for key, value in kwargs.items():
100
+ for key, value in list(kwargs.items()):
76
101
  many_to_many_key = key.split("_id_list")[0]
77
102
  if many_to_many_key in many_to_many_fields:
78
- many_to_many_kwargs[key] = value
79
- kwargs.pop(key)
103
+ many_to_many_kwargs[key] = kwargs.pop(key)
80
104
  return kwargs, many_to_many_kwargs
81
105
 
82
106
  @classmethod
83
107
  @transaction.atomic
84
- def __save_with_history(
108
+ def _save_with_history(
85
109
  cls, instance: GeneralManagerModel, creator_id: int, history_comment: str | None
86
110
  ) -> int:
87
111
  """