GeneralManager 0.6.2__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.2 → generalmanager-0.7.0}/GeneralManager.egg-info/PKG-INFO +1 -1
  2. {generalmanager-0.6.2 → generalmanager-0.7.0}/GeneralManager.egg-info/SOURCES.txt +1 -0
  3. {generalmanager-0.6.2 → generalmanager-0.7.0}/PKG-INFO +1 -1
  4. {generalmanager-0.6.2 → generalmanager-0.7.0}/pyproject.toml +1 -1
  5. generalmanager-0.7.0/src/general_manager/apps.py +170 -0
  6. {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/cache/dependencyIndex.py +10 -2
  7. {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/interface/baseInterface.py +8 -9
  8. {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/interface/databaseBasedInterface.py +28 -13
  9. generalmanager-0.7.0/src/general_manager/interface/readOnlyInterface.py +265 -0
  10. {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/manager/meta.py +1 -1
  11. generalmanager-0.7.0/tests/test_readOnlyInterface.py +370 -0
  12. generalmanager-0.6.2/src/general_manager/apps.py +0 -83
  13. generalmanager-0.6.2/src/general_manager/interface/readOnlyInterface.py +0 -107
  14. {generalmanager-0.6.2 → generalmanager-0.7.0}/GeneralManager.egg-info/dependency_links.txt +0 -0
  15. {generalmanager-0.6.2 → generalmanager-0.7.0}/GeneralManager.egg-info/requires.txt +0 -0
  16. {generalmanager-0.6.2 → generalmanager-0.7.0}/GeneralManager.egg-info/top_level.txt +0 -0
  17. {generalmanager-0.6.2 → generalmanager-0.7.0}/LICENSE +0 -0
  18. {generalmanager-0.6.2 → generalmanager-0.7.0}/README.md +0 -0
  19. {generalmanager-0.6.2 → generalmanager-0.7.0}/setup.cfg +0 -0
  20. {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/__init__.py +0 -0
  21. {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/api/graphql.py +0 -0
  22. {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/api/mutation.py +0 -0
  23. {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/api/property.py +0 -0
  24. {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/auxiliary/__init__.py +0 -0
  25. {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/auxiliary/argsToKwargs.py +0 -0
  26. {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/auxiliary/filterParser.py +0 -0
  27. {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/auxiliary/jsonEncoder.py +0 -0
  28. {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/auxiliary/makeCacheKey.py +0 -0
  29. {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/auxiliary/noneToZero.py +0 -0
  30. {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/auxiliary/pathMapping.py +0 -0
  31. {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/bucket/baseBucket.py +0 -0
  32. {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/bucket/calculationBucket.py +0 -0
  33. {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/bucket/databaseBucket.py +0 -0
  34. {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/bucket/groupBucket.py +0 -0
  35. {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/cache/cacheDecorator.py +0 -0
  36. {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/cache/cacheTracker.py +0 -0
  37. {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/cache/modelDependencyCollector.py +0 -0
  38. {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/cache/signals.py +0 -0
  39. {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/factory/__init__.py +0 -0
  40. {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/factory/autoFactory.py +0 -0
  41. {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/factory/factories.py +0 -0
  42. {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/factory/factoryMethods.py +0 -0
  43. {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/interface/__init__.py +0 -0
  44. {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/interface/calculationInterface.py +0 -0
  45. {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/interface/databaseInterface.py +0 -0
  46. {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/manager/__init__.py +0 -0
  47. {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/manager/generalManager.py +0 -0
  48. {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/manager/groupManager.py +0 -0
  49. {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/manager/input.py +0 -0
  50. {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/measurement/__init__.py +0 -0
  51. {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/measurement/measurement.py +0 -0
  52. {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/measurement/measurementField.py +0 -0
  53. {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/permission/__init__.py +0 -0
  54. {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/permission/basePermission.py +0 -0
  55. {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/permission/fileBasedPermission.py +0 -0
  56. {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/permission/managerBasedPermission.py +0 -0
  57. {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/permission/permissionChecks.py +0 -0
  58. {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/permission/permissionDataManager.py +0 -0
  59. {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/rule/__init__.py +0 -0
  60. {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/rule/handler.py +0 -0
  61. {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/rule/rule.py +0 -0
  62. {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_argsToKwargs.py +0 -0
  63. {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_autoFactory.py +0 -0
  64. {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_baseBucket.py +0 -0
  65. {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_baseInterface.py +0 -0
  66. {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_basePermission.py +0 -0
  67. {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_cacheDecorator.py +0 -0
  68. {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_cacheTracker.py +0 -0
  69. {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_calculationBucket.py +0 -0
  70. {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_calculationInterface.py +0 -0
  71. {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_databaseBasedInterface.py +0 -0
  72. {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_databaseBucket.py +0 -0
  73. {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_databaseInterface.py +0 -0
  74. {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_dependencyIndex.py +0 -0
  75. {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_factories.py +0 -0
  76. {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_factoryMethods.py +0 -0
  77. {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_filterParser.py +0 -0
  78. {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_generalManager.py +0 -0
  79. {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_generalManagerMeta.py +0 -0
  80. {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_graph_ql.py +0 -0
  81. {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_groupManager.py +0 -0
  82. {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_input.py +0 -0
  83. {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_jsonEncoder.py +0 -0
  84. {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_makeCacheKey.py +0 -0
  85. {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_managerBasedPermission.py +0 -0
  86. {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_measurement.py +0 -0
  87. {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_measurement_field.py +0 -0
  88. {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_modelDependencyCollector.py +0 -0
  89. {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_noneToZero.py +0 -0
  90. {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_rule_handler.py +0 -0
  91. {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_rules.py +0 -0
  92. {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_settings.py +0 -0
  93. {generalmanager-0.6.2 → 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.2
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
@@ -82,6 +82,7 @@ tests/test_measurement.py
82
82
  tests/test_measurement_field.py
83
83
  tests/test_modelDependencyCollector.py
84
84
  tests/test_noneToZero.py
85
+ tests/test_readOnlyInterface.py
85
86
  tests/test_rule_handler.py
86
87
  tests/test_rules.py
87
88
  tests/test_settings.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GeneralManager
3
- Version: 0.6.2
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.2"
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
@@ -0,0 +1,265 @@
1
+ from __future__ import annotations
2
+ import json
3
+
4
+ from typing import Type, Any, Callable, TYPE_CHECKING
5
+ from django.db import models, transaction
6
+ from general_manager.interface.databaseBasedInterface import (
7
+ DBBasedInterface,
8
+ GeneralManagerBasisModel,
9
+ classPreCreationMethod,
10
+ classPostCreationMethod,
11
+ generalManagerClassName,
12
+ attributes,
13
+ interfaceBaseClass,
14
+ )
15
+ from django.db import connection
16
+ from typing import ClassVar
17
+ from django.core.checks import Warning
18
+ import logging
19
+
20
+ if TYPE_CHECKING:
21
+ from general_manager.manager.generalManager import GeneralManager
22
+
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ class ReadOnlyInterface(DBBasedInterface):
28
+ _interface_type = "readonly"
29
+ _model: Type[GeneralManagerBasisModel]
30
+ _parent_class: Type[GeneralManager]
31
+
32
+ @staticmethod
33
+ def getUniqueFields(model: Type[models.Model]) -> set[str]:
34
+ """
35
+ Return the set of field names that uniquely identify instances of the given Django model.
36
+
37
+ Considers fields marked as unique (excluding "id"), unique_together constraints, and UniqueConstraint definitions.
38
+ """
39
+ opts = model._meta
40
+ unique_fields: set[str] = set()
41
+
42
+ for field in opts.local_fields:
43
+ if getattr(field, "unique", False):
44
+ if field.name == "id":
45
+ continue
46
+ unique_fields.add(field.name)
47
+
48
+ for ut in opts.unique_together:
49
+ unique_fields.update(ut)
50
+
51
+ for constraint in opts.constraints:
52
+ if isinstance(constraint, models.UniqueConstraint):
53
+ unique_fields.update(constraint.fields)
54
+
55
+ return unique_fields
56
+
57
+ @classmethod
58
+ def syncData(cls) -> None:
59
+ """
60
+ Synchronizes the associated Django model with the JSON data from the parent class, ensuring records match exactly.
61
+
62
+ Parses the JSON data, creates or updates model instances based on unique fields, and marks as inactive any database records not present in the JSON data. Raises a ValueError if required attributes are missing, if the JSON data is invalid, or if no unique fields are defined.
63
+ """
64
+ if cls.ensureSchemaIsUpToDate(cls._parent_class, cls._model):
65
+ logger.warning(
66
+ f"Schema for ReadOnlyInterface '{cls._parent_class.__name__}' is not up to date."
67
+ )
68
+ return
69
+
70
+ model = cls._model
71
+ parent_class = cls._parent_class
72
+ json_data = getattr(parent_class, "_data", None)
73
+ if json_data is None:
74
+ raise ValueError(
75
+ f"For ReadOnlyInterface '{parent_class.__name__}' must set '_data'"
76
+ )
77
+
78
+ # JSON-Daten parsen
79
+ if isinstance(json_data, str):
80
+ data_list = json.loads(json_data)
81
+ elif isinstance(json_data, list):
82
+ data_list: list[Any] = json_data
83
+ else:
84
+ raise ValueError("_data must be a JSON string or a list of dictionaries")
85
+
86
+ unique_fields = cls.getUniqueFields(model)
87
+ if not unique_fields:
88
+ raise ValueError(
89
+ f"For ReadOnlyInterface '{parent_class.__name__}' must have at least one unique field."
90
+ )
91
+
92
+ changes = {
93
+ "created": [],
94
+ "updated": [],
95
+ "deactivated": [],
96
+ }
97
+
98
+ with transaction.atomic():
99
+ json_unique_values: set[Any] = set()
100
+
101
+ # data synchronization
102
+ for data in data_list:
103
+ lookup = {field: data[field] for field in unique_fields}
104
+ unique_identifier = tuple(lookup[field] for field in unique_fields)
105
+ json_unique_values.add(unique_identifier)
106
+
107
+ instance, is_created = model.objects.get_or_create(**lookup)
108
+ updated = False
109
+ for field_name, value in data.items():
110
+ if getattr(instance, field_name, None) != value:
111
+ setattr(instance, field_name, value)
112
+ updated = True
113
+ if updated or not instance.is_active:
114
+ instance.is_active = True
115
+ instance.save()
116
+ changes["created" if is_created else "updated"].append(instance)
117
+
118
+ # deactivate instances not in JSON data
119
+ existing_instances = model.objects.filter(is_active=True)
120
+ for instance in existing_instances:
121
+ lookup = {field: getattr(instance, field) for field in unique_fields}
122
+ unique_identifier = tuple(lookup[field] for field in unique_fields)
123
+ if unique_identifier not in json_unique_values:
124
+ instance.is_active = False
125
+ instance.save()
126
+ changes["deactivated"].append(instance)
127
+
128
+ if changes["created"] or changes["updated"] or changes["deactivated"]:
129
+ logger.info(
130
+ f"Data changes for ReadOnlyInterface '{parent_class.__name__}': "
131
+ f"Created: {len(changes['created'])}, "
132
+ f"Updated: {len(changes['updated'])}, "
133
+ f"Deactivated: {len(changes['deactivated'])}"
134
+ )
135
+
136
+ @staticmethod
137
+ def ensureSchemaIsUpToDate(
138
+ new_manager_class: Type[GeneralManager], model: Type[models.Model]
139
+ ) -> list[Warning]:
140
+ """
141
+ Checks whether the database schema for the given model matches the model definition.
142
+
143
+ Parameters:
144
+ new_manager_class (Type[GeneralManager]): The manager class associated with the model.
145
+ model (Type[models.Model]): The Django model to check.
146
+
147
+ Returns:
148
+ list[Warning]: A list of Django Warning objects describing schema issues, or an empty list if the schema is up to date.
149
+ """
150
+
151
+ def table_exists(table_name: str) -> bool:
152
+ """
153
+ Check if a database table with the given name exists.
154
+
155
+ Parameters:
156
+ table_name (str): The name of the database table to check.
157
+
158
+ Returns:
159
+ bool: True if the table exists, False otherwise.
160
+ """
161
+ with connection.cursor() as cursor:
162
+ tables = connection.introspection.table_names(cursor)
163
+ return table_name in tables
164
+
165
+ def compare_model_to_table(
166
+ model: Type[models.Model], table: str
167
+ ) -> tuple[list[str], list[str]]:
168
+ """
169
+ Compare a Django model's fields to the columns of a database table.
170
+
171
+ Returns:
172
+ missing (list[str]): Columns defined in the model but missing from the database table.
173
+ extra (list[str]): Columns present in the database table but not defined in the model.
174
+ """
175
+ with connection.cursor() as cursor:
176
+ desc = connection.introspection.get_table_description(cursor, table)
177
+ existing_cols = {col.name for col in desc}
178
+ model_cols = {field.column for field in model._meta.local_fields}
179
+ missing = model_cols - existing_cols
180
+ extra = existing_cols - model_cols
181
+ return list(missing), list(extra)
182
+
183
+ table = model._meta.db_table
184
+ if not table_exists(table):
185
+ return [
186
+ Warning(
187
+ f"Database table does not exist!",
188
+ hint=f"ReadOnlyInterface '{new_manager_class.__name__}' (Table '{table}') does not exist in the database.",
189
+ obj=model,
190
+ )
191
+ ]
192
+ missing, extra = compare_model_to_table(model, table)
193
+ if missing or extra:
194
+ return [
195
+ Warning(
196
+ "Database schema mismatch!",
197
+ hint=(
198
+ f"ReadOnlyInterface '{new_manager_class.__name__}' has missing columns: {missing} or extra columns: {extra}. \n"
199
+ " Please update the model or the database schema, to enable data synchronization."
200
+ ),
201
+ obj=model,
202
+ )
203
+ ]
204
+ return []
205
+
206
+ @staticmethod
207
+ def readOnlyPostCreate(func: Callable[..., Any]) -> Callable[..., Any]:
208
+ """
209
+ Decorator for post-creation hooks that registers a new manager class as read-only.
210
+
211
+ After executing the wrapped post-creation function, this decorator appends the newly created manager class to the `read_only_classes` list in the meta-class, marking it as a read-only interface.
212
+ """
213
+
214
+ def wrapper(
215
+ new_class: Type[GeneralManager],
216
+ interface_cls: Type[ReadOnlyInterface],
217
+ model: Type[GeneralManagerBasisModel],
218
+ ):
219
+ """
220
+ Registers the newly created class as a read-only class after invoking the wrapped post-creation function.
221
+
222
+ Parameters:
223
+ new_class (Type[GeneralManager]): The newly created manager class to register.
224
+ interface_cls (Type[ReadOnlyInterface]): The associated read-only interface class.
225
+ model (Type[GeneralManagerModel]): The model class associated with the manager.
226
+ """
227
+ from general_manager.manager.meta import GeneralManagerMeta
228
+
229
+ func(new_class, interface_cls, model)
230
+ GeneralManagerMeta.read_only_classes.append(new_class)
231
+
232
+ return wrapper
233
+
234
+ @staticmethod
235
+ def readOnlyPreCreate(func: Callable[..., Any]) -> Callable[..., Any]:
236
+ """
237
+ Decorator for pre-creation hook functions to ensure the base model class is set to ReadOnlyModel.
238
+
239
+ Wraps a pre-creation function, injecting ReadOnlyModel as the base model class argument before the GeneralManager instance is created.
240
+ """
241
+ def wrapper(
242
+ name: generalManagerClassName,
243
+ attrs: attributes,
244
+ interface: interfaceBaseClass,
245
+ base_model_class=GeneralManagerBasisModel,
246
+ ):
247
+ return func(
248
+ name, attrs, interface, base_model_class=GeneralManagerBasisModel
249
+ )
250
+
251
+ return wrapper
252
+
253
+ @classmethod
254
+ def handleInterface(cls) -> tuple[classPreCreationMethod, classPostCreationMethod]:
255
+ """
256
+ Return the pre- and post-creation hook methods for integrating this interface with a GeneralManager.
257
+
258
+ The returned tuple contains the pre-creation method, which injects the base model class, and the post-creation method, which registers the class as read-only. These hooks are intended for use by GeneralManagerMeta during the manager class lifecycle.
259
+
260
+ Returns:
261
+ tuple: A pair of methods for pre- and post-creation processing.
262
+ """
263
+ return cls.readOnlyPreCreate(cls._preCreate), cls.readOnlyPostCreate(
264
+ cls._postCreate
265
+ )