GeneralManager 0.6.2__py3-none-any.whl → 0.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- general_manager/api/graphql.py +117 -65
- general_manager/api/mutation.py +11 -10
- general_manager/apps.py +131 -31
- general_manager/auxiliary/formatString.py +60 -0
- general_manager/cache/dependencyIndex.py +10 -2
- general_manager/interface/baseInterface.py +9 -9
- general_manager/interface/databaseBasedInterface.py +55 -32
- general_manager/interface/databaseInterface.py +43 -13
- general_manager/interface/readOnlyInterface.py +202 -37
- general_manager/manager/generalManager.py +73 -14
- general_manager/manager/input.py +9 -12
- general_manager/manager/meta.py +17 -11
- {generalmanager-0.6.2.dist-info → generalmanager-0.8.0.dist-info}/METADATA +1 -1
- {generalmanager-0.6.2.dist-info → generalmanager-0.8.0.dist-info}/RECORD +17 -16
- {generalmanager-0.6.2.dist-info → generalmanager-0.8.0.dist-info}/WHEEL +0 -0
- {generalmanager-0.6.2.dist-info → generalmanager-0.8.0.dist-info}/licenses/LICENSE +0 -0
- {generalmanager-0.6.2.dist-info → generalmanager-0.8.0.dist-info}/top_level.txt +0 -0
@@ -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],
|
@@ -52,10 +51,11 @@ class AttributeTypedDict(TypedDict):
|
|
52
51
|
default: Any
|
53
52
|
is_required: bool
|
54
53
|
is_editable: bool
|
54
|
+
is_derived: bool
|
55
55
|
|
56
56
|
|
57
57
|
class InterfaceBase(ABC):
|
58
|
-
_parent_class:
|
58
|
+
_parent_class: Type[GeneralManager]
|
59
59
|
_interface_type: ClassVar[str]
|
60
60
|
input_fields: dict[str, Input]
|
61
61
|
|
@@ -70,9 +70,9 @@ class InterfaceBase(ABC):
|
|
70
70
|
) -> dict[str, Any]:
|
71
71
|
"""
|
72
72
|
Parses and validates input arguments into a structured identification dictionary.
|
73
|
-
|
73
|
+
|
74
74
|
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
|
-
|
75
|
+
|
76
76
|
Returns:
|
77
77
|
A dictionary mapping input field names to their validated and cast values.
|
78
78
|
"""
|
@@ -131,7 +131,7 @@ class InterfaceBase(ABC):
|
|
131
131
|
) -> None:
|
132
132
|
"""
|
133
133
|
Validates the type and allowed values of an input field.
|
134
|
-
|
134
|
+
|
135
135
|
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
136
|
"""
|
137
137
|
input_field = self.input_fields[name]
|
@@ -218,13 +218,13 @@ class InterfaceBase(ABC):
|
|
218
218
|
def getFieldType(cls, field_name: str) -> type:
|
219
219
|
"""
|
220
220
|
Returns the type of the specified input field.
|
221
|
-
|
221
|
+
|
222
222
|
Args:
|
223
223
|
field_name: The name of the input field.
|
224
|
-
|
224
|
+
|
225
225
|
Returns:
|
226
226
|
The Python type associated with the given field name.
|
227
|
-
|
227
|
+
|
228
228
|
Raises:
|
229
229
|
NotImplementedError: This method must be implemented by subclasses.
|
230
230
|
"""
|
@@ -1,12 +1,5 @@
|
|
1
1
|
from __future__ import annotations
|
2
|
-
from typing import
|
3
|
-
Type,
|
4
|
-
ClassVar,
|
5
|
-
Any,
|
6
|
-
Callable,
|
7
|
-
TYPE_CHECKING,
|
8
|
-
TypeVar,
|
9
|
-
)
|
2
|
+
from typing import Type, ClassVar, Any, Callable, TYPE_CHECKING, TypeVar, Generic
|
10
3
|
from django.db import models
|
11
4
|
from django.conf import settings
|
12
5
|
from datetime import datetime, timedelta
|
@@ -66,17 +59,28 @@ def getFullCleanMethode(model: Type[models.Model]) -> Callable[..., None]:
|
|
66
59
|
return full_clean
|
67
60
|
|
68
61
|
|
69
|
-
class
|
62
|
+
class GeneralManagerBasisModel(models.Model):
|
70
63
|
_general_manager_class: ClassVar[Type[GeneralManager]]
|
71
64
|
is_active = models.BooleanField(default=True)
|
72
|
-
changed_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
|
73
|
-
changed_by_id: int
|
74
65
|
history = HistoricalRecords(inherit=True)
|
75
66
|
|
67
|
+
class Meta:
|
68
|
+
abstract = True
|
69
|
+
|
70
|
+
|
71
|
+
class GeneralManagerModel(GeneralManagerBasisModel):
|
72
|
+
changed_by = models.ForeignKey(
|
73
|
+
settings.AUTH_USER_MODEL, on_delete=models.PROTECT, null=True, blank=True
|
74
|
+
)
|
75
|
+
changed_by_id: int | None
|
76
|
+
|
76
77
|
@property
|
77
|
-
def _history_user(self) -> AbstractUser:
|
78
|
+
def _history_user(self) -> AbstractUser | None:
|
78
79
|
"""
|
79
|
-
|
80
|
+
Gets the user who last modified this model instance, or None if not set.
|
81
|
+
|
82
|
+
Returns:
|
83
|
+
AbstractUser | None: The user who last changed the instance, or None if unavailable.
|
80
84
|
"""
|
81
85
|
return self.changed_by
|
82
86
|
|
@@ -90,12 +94,15 @@ class GeneralManagerModel(models.Model):
|
|
90
94
|
"""
|
91
95
|
self.changed_by = value
|
92
96
|
|
93
|
-
class Meta:
|
97
|
+
class Meta: # type: ignore
|
94
98
|
abstract = True
|
95
99
|
|
96
100
|
|
97
|
-
|
98
|
-
|
101
|
+
MODEL_TYPE = TypeVar("MODEL_TYPE", bound=GeneralManagerBasisModel)
|
102
|
+
|
103
|
+
|
104
|
+
class DBBasedInterface(InterfaceBase, Generic[MODEL_TYPE]):
|
105
|
+
_model: Type[MODEL_TYPE]
|
99
106
|
input_fields: dict[str, Input] = {"id": Input(int)}
|
100
107
|
|
101
108
|
def __init__(
|
@@ -105,15 +112,15 @@ class DBBasedInterface(InterfaceBase):
|
|
105
112
|
**kwargs: dict[str, Any],
|
106
113
|
):
|
107
114
|
"""
|
108
|
-
|
109
|
-
|
110
|
-
If
|
115
|
+
Initialize the interface instance and load the associated model record.
|
116
|
+
|
117
|
+
If `search_date` is provided, retrieves the historical record as of that date; otherwise, loads the current record.
|
111
118
|
"""
|
112
119
|
super().__init__(*args, **kwargs)
|
113
120
|
self.pk = self.identification["id"]
|
114
121
|
self._instance = self.getData(search_date)
|
115
122
|
|
116
|
-
def getData(self, search_date: datetime | None = None) ->
|
123
|
+
def getData(self, search_date: datetime | None = None) -> GeneralManagerBasisModel:
|
117
124
|
"""
|
118
125
|
Retrieves the model instance by primary key, optionally as of a specified historical date.
|
119
126
|
|
@@ -177,8 +184,8 @@ class DBBasedInterface(InterfaceBase):
|
|
177
184
|
|
178
185
|
@classmethod
|
179
186
|
def getHistoricalRecord(
|
180
|
-
cls, instance:
|
181
|
-
) ->
|
187
|
+
cls, instance: GeneralManagerBasisModel, search_date: datetime | None = None
|
188
|
+
) -> GeneralManagerBasisModel:
|
182
189
|
"""
|
183
190
|
Retrieves the most recent historical record of a model instance at or before a specified date.
|
184
191
|
|
@@ -194,9 +201,12 @@ class DBBasedInterface(InterfaceBase):
|
|
194
201
|
@classmethod
|
195
202
|
def getAttributeTypes(cls) -> dict[str, AttributeTypedDict]:
|
196
203
|
"""
|
197
|
-
|
198
|
-
|
199
|
-
The returned dictionary includes all model fields, custom fields, foreign keys, many-to-many, and reverse relation fields.
|
204
|
+
Return a dictionary mapping attribute names to metadata describing their type and properties.
|
205
|
+
|
206
|
+
The returned dictionary includes all model fields, custom fields, foreign keys, many-to-many, and reverse relation fields. For each attribute, the metadata includes its Python type (translated from Django field types when possible), whether it is required, editable, derived, and its default value. For related models with a general manager class, the type is set to that class.
|
207
|
+
|
208
|
+
Returns:
|
209
|
+
dict[str, AttributeTypedDict]: Mapping of attribute names to their type information and metadata.
|
200
210
|
"""
|
201
211
|
TRANSLATION: dict[Type[models.Field[Any, Any]], type] = {
|
202
212
|
models.fields.BigAutoField: int,
|
@@ -222,6 +232,7 @@ class DBBasedInterface(InterfaceBase):
|
|
222
232
|
field: models.Field = getattr(cls._model, field_name)
|
223
233
|
fields[field_name] = {
|
224
234
|
"type": type(field),
|
235
|
+
"is_derived": False,
|
225
236
|
"is_required": not field.null,
|
226
237
|
"is_editable": field.editable,
|
227
238
|
"default": field.default,
|
@@ -232,6 +243,7 @@ class DBBasedInterface(InterfaceBase):
|
|
232
243
|
field: models.Field = getattr(cls._model, field_name).field
|
233
244
|
fields[field_name] = {
|
234
245
|
"type": type(field),
|
246
|
+
"is_derived": False,
|
235
247
|
"is_required": not field.null,
|
236
248
|
"is_editable": field.editable,
|
237
249
|
"default": field.default,
|
@@ -249,6 +261,7 @@ class DBBasedInterface(InterfaceBase):
|
|
249
261
|
elif related_model is not None:
|
250
262
|
fields[field_name] = {
|
251
263
|
"type": related_model,
|
264
|
+
"is_derived": False,
|
252
265
|
"is_required": not field.null,
|
253
266
|
"is_editable": field.editable,
|
254
267
|
"default": field.default,
|
@@ -274,6 +287,7 @@ class DBBasedInterface(InterfaceBase):
|
|
274
287
|
fields[f"{field_name}_list"] = {
|
275
288
|
"type": related_model,
|
276
289
|
"is_required": False,
|
290
|
+
"is_derived": not bool(field.many_to_many),
|
277
291
|
"is_editable": bool(field.many_to_many and field.editable),
|
278
292
|
"default": None,
|
279
293
|
}
|
@@ -440,16 +454,25 @@ class DBBasedInterface(InterfaceBase):
|
|
440
454
|
|
441
455
|
@staticmethod
|
442
456
|
def _preCreate(
|
443
|
-
name: generalManagerClassName,
|
457
|
+
name: generalManagerClassName,
|
458
|
+
attrs: attributes,
|
459
|
+
interface: interfaceBaseClass,
|
460
|
+
base_model_class=GeneralManagerModel,
|
444
461
|
) -> tuple[attributes, interfaceBaseClass, relatedClass]:
|
445
462
|
# Felder aus der Interface-Klasse sammeln
|
446
463
|
"""
|
447
|
-
Dynamically creates a Django model, its associated interface class, and a factory class
|
448
|
-
|
449
|
-
This method extracts fields and meta information from the interface class, constructs a new Django model inheriting from
|
450
|
-
|
464
|
+
Dynamically creates a Django model class, its associated interface class, and a factory class from an interface definition.
|
465
|
+
|
466
|
+
This method extracts fields and meta information from the provided 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.
|
467
|
+
|
468
|
+
Parameters:
|
469
|
+
name: The name for the dynamically created model class.
|
470
|
+
attrs: The attributes dictionary to be updated with the new interface and factory classes.
|
471
|
+
interface: The interface base class defining the model structure and metadata.
|
472
|
+
base_model_class: The base class to use for the new model (defaults to GeneralManagerModel).
|
473
|
+
|
451
474
|
Returns:
|
452
|
-
A tuple containing the updated attributes dictionary, the new interface class, and the newly created model class.
|
475
|
+
tuple: A tuple containing the updated attributes dictionary, the new interface class, and the newly created model class.
|
453
476
|
"""
|
454
477
|
model_fields: dict[str, Any] = {}
|
455
478
|
meta_class = None
|
@@ -474,7 +497,7 @@ class DBBasedInterface(InterfaceBase):
|
|
474
497
|
delattr(meta_class, "rules")
|
475
498
|
|
476
499
|
# Modell erstellen
|
477
|
-
model = type(name, (
|
500
|
+
model = type(name, (base_model_class,), model_fields)
|
478
501
|
if meta_class and rules:
|
479
502
|
setattr(model._meta, "rules", rules)
|
480
503
|
# full_clean Methode hinzufügen
|
@@ -11,14 +11,25 @@ from general_manager.interface.databaseBasedInterface import (
|
|
11
11
|
)
|
12
12
|
|
13
13
|
|
14
|
-
class DatabaseInterface(DBBasedInterface):
|
14
|
+
class DatabaseInterface(DBBasedInterface[GeneralManagerModel]):
|
15
15
|
_interface_type = "database"
|
16
16
|
|
17
17
|
@classmethod
|
18
18
|
def create(
|
19
|
-
cls, creator_id: int, history_comment: str | None = None, **kwargs: Any
|
19
|
+
cls, creator_id: int | None, history_comment: str | None = None, **kwargs: Any
|
20
20
|
) -> int:
|
21
|
+
"""
|
22
|
+
Create a new model instance with the provided attributes and optional history tracking.
|
23
|
+
|
24
|
+
Validates input attributes, separates and sets many-to-many relationships, saves the instance with optional creator and history comment, and returns the primary key of the created instance.
|
25
|
+
|
26
|
+
Parameters:
|
27
|
+
creator_id (int | None): The ID of the user creating the instance, or None if not applicable.
|
28
|
+
history_comment (str | None): Optional comment to record in the instance's history.
|
21
29
|
|
30
|
+
Returns:
|
31
|
+
int: The primary key of the newly created instance.
|
32
|
+
"""
|
22
33
|
cls._checkForInvalidKwargs(cls._model, kwargs=kwargs)
|
23
34
|
kwargs, many_to_many_kwargs = cls._sortKwargs(cls._model, kwargs)
|
24
35
|
instance = cls.__setAttrForWrite(cls._model(), kwargs)
|
@@ -27,9 +38,18 @@ class DatabaseInterface(DBBasedInterface):
|
|
27
38
|
return pk
|
28
39
|
|
29
40
|
def update(
|
30
|
-
self, creator_id: int, history_comment: str | None = None, **kwargs: Any
|
41
|
+
self, creator_id: int | None, history_comment: str | None = None, **kwargs: Any
|
31
42
|
) -> int:
|
43
|
+
"""
|
44
|
+
Update the current model instance with new attribute values and many-to-many relationships, saving changes with optional history tracking.
|
45
|
+
|
46
|
+
Parameters:
|
47
|
+
creator_id (int | None): The ID of the user making the update, or None if not specified.
|
48
|
+
history_comment (str | None): An optional comment describing the reason for the update.
|
32
49
|
|
50
|
+
Returns:
|
51
|
+
int: The primary key of the updated instance.
|
52
|
+
"""
|
33
53
|
self._checkForInvalidKwargs(self._model, kwargs=kwargs)
|
34
54
|
kwargs, many_to_many_kwargs = self._sortKwargs(self._model, kwargs)
|
35
55
|
instance = self.__setAttrForWrite(self._model.objects.get(pk=self.pk), kwargs)
|
@@ -37,7 +57,19 @@ class DatabaseInterface(DBBasedInterface):
|
|
37
57
|
self.__setManyToManyAttributes(instance, many_to_many_kwargs)
|
38
58
|
return pk
|
39
59
|
|
40
|
-
def deactivate(
|
60
|
+
def deactivate(
|
61
|
+
self, creator_id: int | None, history_comment: str | None = None
|
62
|
+
) -> int:
|
63
|
+
"""
|
64
|
+
Deactivate the current model instance by setting its `is_active` flag to `False` and recording the change with an optional history comment.
|
65
|
+
|
66
|
+
Parameters:
|
67
|
+
creator_id (int | None): The ID of the user performing the deactivation, or None if not specified.
|
68
|
+
history_comment (str | None): An optional comment to include in the instance's history log.
|
69
|
+
|
70
|
+
Returns:
|
71
|
+
int: The primary key of the deactivated instance.
|
72
|
+
"""
|
41
73
|
instance = self._model.objects.get(pk=self.pk)
|
42
74
|
instance.is_active = False
|
43
75
|
if history_comment:
|
@@ -89,7 +121,7 @@ class DatabaseInterface(DBBasedInterface):
|
|
89
121
|
for key in kwargs:
|
90
122
|
temp_key = key.split("_id_list")[0] # Remove '_id_list' suffix
|
91
123
|
if temp_key not in attributes and temp_key not in field_names:
|
92
|
-
raise ValueError(f"{key} does not
|
124
|
+
raise ValueError(f"{key} does not exist in {model.__name__}")
|
93
125
|
|
94
126
|
@staticmethod
|
95
127
|
def _sortKwargs(
|
@@ -106,17 +138,15 @@ class DatabaseInterface(DBBasedInterface):
|
|
106
138
|
@classmethod
|
107
139
|
@transaction.atomic
|
108
140
|
def _save_with_history(
|
109
|
-
cls,
|
141
|
+
cls,
|
142
|
+
instance: GeneralManagerModel,
|
143
|
+
creator_id: int | None,
|
144
|
+
history_comment: str | None,
|
110
145
|
) -> int:
|
111
146
|
"""
|
112
|
-
|
147
|
+
Atomically saves a model instance with validation and optional history comment.
|
113
148
|
|
114
|
-
Sets the `changed_by_id` field, validates the instance, applies a history comment if provided, and saves the instance within
|
115
|
-
|
116
|
-
Args:
|
117
|
-
instance: The model instance to save.
|
118
|
-
creator_id: The ID of the user making the change.
|
119
|
-
history_comment: Optional comment describing the reason for the change.
|
149
|
+
Sets the `changed_by_id` field, validates the instance, applies a history comment if provided, and saves the instance within a database transaction.
|
120
150
|
|
121
151
|
Returns:
|
122
152
|
The primary key of the saved instance.
|
@@ -5,34 +5,73 @@ from typing import Type, Any, Callable, TYPE_CHECKING
|
|
5
5
|
from django.db import models, transaction
|
6
6
|
from general_manager.interface.databaseBasedInterface import (
|
7
7
|
DBBasedInterface,
|
8
|
-
|
8
|
+
GeneralManagerBasisModel,
|
9
9
|
classPreCreationMethod,
|
10
10
|
classPostCreationMethod,
|
11
|
+
generalManagerClassName,
|
12
|
+
attributes,
|
13
|
+
interfaceBaseClass,
|
11
14
|
)
|
15
|
+
from django.db import connection
|
16
|
+
from typing import ClassVar
|
17
|
+
from django.core.checks import Warning
|
18
|
+
import logging
|
12
19
|
|
13
20
|
if TYPE_CHECKING:
|
14
21
|
from general_manager.manager.generalManager import GeneralManager
|
15
|
-
from general_manager.manager.meta import GeneralManagerMeta
|
16
22
|
|
17
23
|
|
18
|
-
|
24
|
+
logger = logging.getLogger(__name__)
|
25
|
+
|
26
|
+
|
27
|
+
class ReadOnlyInterface(DBBasedInterface[GeneralManagerBasisModel]):
|
19
28
|
_interface_type = "readonly"
|
29
|
+
_parent_class: Type[GeneralManager]
|
20
30
|
|
21
|
-
@
|
22
|
-
def
|
31
|
+
@staticmethod
|
32
|
+
def getUniqueFields(model: Type[models.Model]) -> set[str]:
|
33
|
+
"""
|
34
|
+
Return a set of field names that uniquely identify instances of the specified Django model.
|
35
|
+
|
36
|
+
Considers fields marked as unique (excluding "id"), as well as fields defined in `unique_together` and `UniqueConstraint` constraints.
|
23
37
|
"""
|
24
|
-
|
38
|
+
opts = model._meta
|
39
|
+
unique_fields: set[str] = set()
|
25
40
|
|
26
|
-
|
41
|
+
for field in opts.local_fields:
|
42
|
+
if getattr(field, "unique", False):
|
43
|
+
if field.name == "id":
|
44
|
+
continue
|
45
|
+
unique_fields.add(field.name)
|
46
|
+
|
47
|
+
for ut in opts.unique_together:
|
48
|
+
unique_fields.update(ut)
|
49
|
+
|
50
|
+
for constraint in opts.constraints:
|
51
|
+
if isinstance(constraint, models.UniqueConstraint):
|
52
|
+
unique_fields.update(constraint.fields)
|
53
|
+
|
54
|
+
return unique_fields
|
55
|
+
|
56
|
+
@classmethod
|
57
|
+
def syncData(cls) -> None:
|
27
58
|
"""
|
28
|
-
model
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
59
|
+
Synchronizes the associated Django model with JSON data from the parent class, ensuring the database records match the provided data exactly.
|
60
|
+
|
61
|
+
Parses the JSON data, creates or updates model instances based on unique fields, and deactivates 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.
|
62
|
+
"""
|
63
|
+
if cls.ensureSchemaIsUpToDate(cls._parent_class, cls._model):
|
64
|
+
logger.warning(
|
65
|
+
f"Schema for ReadOnlyInterface '{cls._parent_class.__name__}' is not up to date."
|
66
|
+
)
|
67
|
+
return
|
68
|
+
|
69
|
+
model = cls._model
|
70
|
+
parent_class = cls._parent_class
|
71
|
+
json_data = getattr(parent_class, "_data", None)
|
72
|
+
if json_data is None:
|
34
73
|
raise ValueError(
|
35
|
-
f"For ReadOnlyInterface '{parent_class.__name__}' must
|
74
|
+
f"For ReadOnlyInterface '{parent_class.__name__}' must set '_data'"
|
36
75
|
)
|
37
76
|
|
38
77
|
# JSON-Daten parsen
|
@@ -41,67 +80,193 @@ class ReadOnlyInterface(DBBasedInterface):
|
|
41
80
|
elif isinstance(json_data, list):
|
42
81
|
data_list: list[Any] = json_data
|
43
82
|
else:
|
44
|
-
raise ValueError(
|
45
|
-
"_json_data must be a JSON string or a list of dictionaries"
|
46
|
-
)
|
83
|
+
raise ValueError("_data must be a JSON string or a list of dictionaries")
|
47
84
|
|
48
|
-
unique_fields =
|
85
|
+
unique_fields = cls.getUniqueFields(model)
|
49
86
|
if not unique_fields:
|
50
87
|
raise ValueError(
|
51
|
-
f"For ReadOnlyInterface '{parent_class.__name__}' must
|
88
|
+
f"For ReadOnlyInterface '{parent_class.__name__}' must have at least one unique field."
|
52
89
|
)
|
53
90
|
|
91
|
+
changes = {
|
92
|
+
"created": [],
|
93
|
+
"updated": [],
|
94
|
+
"deactivated": [],
|
95
|
+
}
|
96
|
+
|
54
97
|
with transaction.atomic():
|
55
98
|
json_unique_values: set[Any] = set()
|
56
99
|
|
57
|
-
#
|
100
|
+
# data synchronization
|
58
101
|
for data in data_list:
|
59
102
|
lookup = {field: data[field] for field in unique_fields}
|
60
103
|
unique_identifier = tuple(lookup[field] for field in unique_fields)
|
61
104
|
json_unique_values.add(unique_identifier)
|
62
105
|
|
63
|
-
instance,
|
106
|
+
instance, is_created = model.objects.get_or_create(**lookup)
|
64
107
|
updated = False
|
65
108
|
for field_name, value in data.items():
|
66
109
|
if getattr(instance, field_name, None) != value:
|
67
110
|
setattr(instance, field_name, value)
|
68
111
|
updated = True
|
69
|
-
if updated:
|
112
|
+
if updated or not instance.is_active:
|
113
|
+
instance.is_active = True
|
70
114
|
instance.save()
|
115
|
+
changes["created" if is_created else "updated"].append(instance)
|
71
116
|
|
72
|
-
#
|
73
|
-
existing_instances = model.objects.
|
117
|
+
# deactivate instances not in JSON data
|
118
|
+
existing_instances = model.objects.filter(is_active=True)
|
74
119
|
for instance in existing_instances:
|
75
120
|
lookup = {field: getattr(instance, field) for field in unique_fields}
|
76
121
|
unique_identifier = tuple(lookup[field] for field in unique_fields)
|
77
122
|
if unique_identifier not in json_unique_values:
|
78
|
-
instance.
|
123
|
+
instance.is_active = False
|
124
|
+
instance.save()
|
125
|
+
changes["deactivated"].append(instance)
|
126
|
+
|
127
|
+
if changes["created"] or changes["updated"] or changes["deactivated"]:
|
128
|
+
logger.info(
|
129
|
+
f"Data changes for ReadOnlyInterface '{parent_class.__name__}': "
|
130
|
+
f"Created: {len(changes['created'])}, "
|
131
|
+
f"Updated: {len(changes['updated'])}, "
|
132
|
+
f"Deactivated: {len(changes['deactivated'])}"
|
133
|
+
)
|
79
134
|
|
80
135
|
@staticmethod
|
81
|
-
def
|
136
|
+
def ensureSchemaIsUpToDate(
|
137
|
+
new_manager_class: Type[GeneralManager], model: Type[models.Model]
|
138
|
+
) -> list[Warning]:
|
82
139
|
"""
|
83
|
-
|
140
|
+
Check if the database schema for a Django model matches its model definition.
|
141
|
+
|
142
|
+
Returns:
|
143
|
+
A list of Django Warning objects describing schema issues, such as missing tables or column mismatches. Returns an empty list if the schema is up to date.
|
144
|
+
"""
|
145
|
+
|
146
|
+
def table_exists(table_name: str) -> bool:
|
147
|
+
"""
|
148
|
+
Determine whether a database table with the specified name exists.
|
149
|
+
|
150
|
+
Parameters:
|
151
|
+
table_name (str): Name of the database table to check.
|
152
|
+
|
153
|
+
Returns:
|
154
|
+
bool: True if the table exists, False otherwise.
|
155
|
+
"""
|
156
|
+
with connection.cursor() as cursor:
|
157
|
+
tables = connection.introspection.table_names(cursor)
|
158
|
+
return table_name in tables
|
159
|
+
|
160
|
+
def compare_model_to_table(
|
161
|
+
model: Type[models.Model], table: str
|
162
|
+
) -> tuple[list[str], list[str]]:
|
163
|
+
"""
|
164
|
+
Compares the fields of a Django model to the columns of a specified database table.
|
165
|
+
|
166
|
+
Returns:
|
167
|
+
A tuple containing two lists:
|
168
|
+
- The first list contains column names defined in the model but missing from the database table.
|
169
|
+
- The second list contains column names present in the database table but not defined in the model.
|
170
|
+
"""
|
171
|
+
with connection.cursor() as cursor:
|
172
|
+
desc = connection.introspection.get_table_description(cursor, table)
|
173
|
+
existing_cols = {col.name for col in desc}
|
174
|
+
model_cols = {field.column for field in model._meta.local_fields}
|
175
|
+
missing = model_cols - existing_cols
|
176
|
+
extra = existing_cols - model_cols
|
177
|
+
return list(missing), list(extra)
|
84
178
|
|
85
|
-
|
86
|
-
|
179
|
+
table = model._meta.db_table
|
180
|
+
if not table_exists(table):
|
181
|
+
return [
|
182
|
+
Warning(
|
183
|
+
f"Database table does not exist!",
|
184
|
+
hint=f"ReadOnlyInterface '{new_manager_class.__name__}' (Table '{table}') does not exist in the database.",
|
185
|
+
obj=model,
|
186
|
+
)
|
187
|
+
]
|
188
|
+
missing, extra = compare_model_to_table(model, table)
|
189
|
+
if missing or extra:
|
190
|
+
return [
|
191
|
+
Warning(
|
192
|
+
"Database schema mismatch!",
|
193
|
+
hint=(
|
194
|
+
f"ReadOnlyInterface '{new_manager_class.__name__}' has missing columns: {missing} or extra columns: {extra}. \n"
|
195
|
+
" Please update the model or the database schema, to enable data synchronization."
|
196
|
+
),
|
197
|
+
obj=model,
|
198
|
+
)
|
199
|
+
]
|
200
|
+
return []
|
201
|
+
|
202
|
+
@staticmethod
|
203
|
+
def readOnlyPostCreate(func: Callable[..., Any]) -> Callable[..., Any]:
|
204
|
+
"""
|
205
|
+
Decorator for post-creation hooks that registers a new manager class as read-only.
|
206
|
+
|
207
|
+
After the wrapped post-creation function is executed, the newly created manager class is added to the meta-class's list of read-only classes, marking it as a read-only interface.
|
87
208
|
"""
|
88
209
|
|
89
210
|
def wrapper(
|
90
|
-
mcs: Type[GeneralManagerMeta],
|
91
211
|
new_class: Type[GeneralManager],
|
92
212
|
interface_cls: Type[ReadOnlyInterface],
|
93
|
-
model: Type[
|
213
|
+
model: Type[GeneralManagerBasisModel],
|
94
214
|
):
|
95
|
-
|
96
|
-
|
215
|
+
"""
|
216
|
+
Registers a newly created manager class as read-only after executing the wrapped post-creation function.
|
217
|
+
|
218
|
+
This function appends the new manager class to the list of read-only classes in the meta system, ensuring it is recognized as a read-only interface.
|
219
|
+
"""
|
220
|
+
from general_manager.manager.meta import GeneralManagerMeta
|
221
|
+
|
222
|
+
func(new_class, interface_cls, model)
|
223
|
+
GeneralManagerMeta.read_only_classes.append(new_class)
|
224
|
+
|
225
|
+
return wrapper
|
226
|
+
|
227
|
+
@staticmethod
|
228
|
+
def readOnlyPreCreate(func: Callable[..., Any]) -> Callable[..., Any]:
|
229
|
+
"""
|
230
|
+
Decorator for pre-creation hook functions that ensures the base model class is set to `GeneralManagerBasisModel`.
|
231
|
+
|
232
|
+
Wraps a pre-creation function, injecting `GeneralManagerBasisModel` as the `base_model_class` argument before the manager class is created.
|
233
|
+
"""
|
234
|
+
|
235
|
+
def wrapper(
|
236
|
+
name: generalManagerClassName,
|
237
|
+
attrs: attributes,
|
238
|
+
interface: interfaceBaseClass,
|
239
|
+
base_model_class=GeneralManagerBasisModel,
|
240
|
+
):
|
241
|
+
"""
|
242
|
+
Wraps a function to ensure the `base_model_class` argument is set to `GeneralManagerBasisModel` before invocation.
|
243
|
+
|
244
|
+
Parameters:
|
245
|
+
name: The name of the manager class being created.
|
246
|
+
attrs: Attributes for the manager class.
|
247
|
+
interface: The interface base class to use.
|
248
|
+
|
249
|
+
Returns:
|
250
|
+
The result of calling the wrapped function with `base_model_class` set to `GeneralManagerBasisModel`.
|
251
|
+
"""
|
252
|
+
return func(
|
253
|
+
name, attrs, interface, base_model_class=GeneralManagerBasisModel
|
254
|
+
)
|
97
255
|
|
98
256
|
return wrapper
|
99
257
|
|
100
258
|
@classmethod
|
101
259
|
def handleInterface(cls) -> tuple[classPreCreationMethod, classPostCreationMethod]:
|
102
260
|
"""
|
103
|
-
|
104
|
-
|
105
|
-
The
|
261
|
+
Return the pre- and post-creation hook methods for integrating the interface with a manager meta-class system.
|
262
|
+
|
263
|
+
The returned tuple includes:
|
264
|
+
- A pre-creation method that ensures the base model class is set for read-only operation.
|
265
|
+
- A post-creation method that registers the manager class as read-only.
|
266
|
+
|
267
|
+
Returns:
|
268
|
+
tuple: The pre-creation and post-creation hook methods for manager class lifecycle integration.
|
106
269
|
"""
|
107
|
-
return cls._preCreate, cls.readOnlyPostCreate(
|
270
|
+
return cls.readOnlyPreCreate(cls._preCreate), cls.readOnlyPostCreate(
|
271
|
+
cls._postCreate
|
272
|
+
)
|