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.
- {generalmanager-0.6.2 → generalmanager-0.7.0}/GeneralManager.egg-info/PKG-INFO +1 -1
- {generalmanager-0.6.2 → generalmanager-0.7.0}/GeneralManager.egg-info/SOURCES.txt +1 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/PKG-INFO +1 -1
- {generalmanager-0.6.2 → generalmanager-0.7.0}/pyproject.toml +1 -1
- generalmanager-0.7.0/src/general_manager/apps.py +170 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/cache/dependencyIndex.py +10 -2
- {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/interface/baseInterface.py +8 -9
- {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/interface/databaseBasedInterface.py +28 -13
- generalmanager-0.7.0/src/general_manager/interface/readOnlyInterface.py +265 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/manager/meta.py +1 -1
- generalmanager-0.7.0/tests/test_readOnlyInterface.py +370 -0
- generalmanager-0.6.2/src/general_manager/apps.py +0 -83
- generalmanager-0.6.2/src/general_manager/interface/readOnlyInterface.py +0 -107
- {generalmanager-0.6.2 → generalmanager-0.7.0}/GeneralManager.egg-info/dependency_links.txt +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/GeneralManager.egg-info/requires.txt +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/GeneralManager.egg-info/top_level.txt +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/LICENSE +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/README.md +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/setup.cfg +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/__init__.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/api/graphql.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/api/mutation.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/api/property.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/auxiliary/__init__.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/auxiliary/argsToKwargs.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/auxiliary/filterParser.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/auxiliary/jsonEncoder.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/auxiliary/makeCacheKey.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/auxiliary/noneToZero.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/auxiliary/pathMapping.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/bucket/baseBucket.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/bucket/calculationBucket.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/bucket/databaseBucket.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/bucket/groupBucket.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/cache/cacheDecorator.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/cache/cacheTracker.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/cache/modelDependencyCollector.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/cache/signals.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/factory/__init__.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/factory/autoFactory.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/factory/factories.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/factory/factoryMethods.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/interface/__init__.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/interface/calculationInterface.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/interface/databaseInterface.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/manager/__init__.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/manager/generalManager.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/manager/groupManager.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/manager/input.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/measurement/__init__.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/measurement/measurement.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/measurement/measurementField.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/permission/__init__.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/permission/basePermission.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/permission/fileBasedPermission.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/permission/managerBasedPermission.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/permission/permissionChecks.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/permission/permissionDataManager.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/rule/__init__.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/rule/handler.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/rule/rule.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_argsToKwargs.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_autoFactory.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_baseBucket.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_baseInterface.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_basePermission.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_cacheDecorator.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_cacheTracker.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_calculationBucket.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_calculationInterface.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_databaseBasedInterface.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_databaseBucket.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_databaseInterface.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_dependencyIndex.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_factories.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_factoryMethods.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_filterParser.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_generalManager.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_generalManagerMeta.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_graph_ql.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_groupManager.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_input.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_jsonEncoder.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_makeCacheKey.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_managerBasedPermission.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_measurement.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_measurement_field.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_modelDependencyCollector.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_noneToZero.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_rule_handler.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_rules.py +0 -0
- {generalmanager-0.6.2 → generalmanager-0.7.0}/tests/test_settings.py +0 -0
- {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.
|
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
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: GeneralManager
|
3
|
-
Version: 0.
|
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.
|
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
|
-
|
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
|
-
|
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):
|
{generalmanager-0.6.2 → generalmanager-0.7.0}/src/general_manager/interface/baseInterface.py
RENAMED
@@ -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 =
|
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:
|
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
|
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:
|
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) ->
|
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:
|
181
|
-
) ->
|
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,
|
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
|
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, (
|
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
|
+
)
|