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.
@@ -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],
@@ -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: ClassVar[Type[Any]]
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 GeneralManagerModel(models.Model):
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
- Returns the user who last modified this model instance.
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
- class DBBasedInterface(InterfaceBase):
98
- _model: ClassVar[Type[GeneralManagerModel]]
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
- Initializes the interface instance and loads the corresponding model record.
109
-
110
- If a `search_date` is provided, retrieves the historical record as of that date; otherwise, loads the current record.
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) -> GeneralManagerModel:
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: GeneralManagerModel, search_date: datetime | None = None
181
- ) -> GeneralManagerModel:
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
- Returns a dictionary mapping attribute names to their type information and metadata.
198
-
199
- The returned dictionary includes all model fields, custom fields, foreign keys, many-to-many, and reverse relation fields. Each entry provides the Python type (translated from Django field types when possible), whether the field is required, editable, and its default value. For related models that have a general manager class, the type is set to that class.
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, attrs: attributes, interface: interfaceBaseClass
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 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
-
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, (GeneralManagerModel,), model_fields)
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(self, creator_id: int, history_comment: str | None = None) -> int:
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 exsist in {model.__name__}")
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, instance: GeneralManagerModel, creator_id: int, history_comment: str | None
141
+ cls,
142
+ instance: GeneralManagerModel,
143
+ creator_id: int | None,
144
+ history_comment: str | None,
110
145
  ) -> int:
111
146
  """
112
- Saves a model instance with validation and optional history tracking.
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 an atomic transaction.
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
- GeneralManagerModel,
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
- class ReadOnlyInterface(DBBasedInterface):
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
- @classmethod
22
- def sync_data(cls) -> None:
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
- Synchronizes the database model with JSON data, ensuring exact correspondence.
38
+ opts = model._meta
39
+ unique_fields: set[str] = set()
25
40
 
26
- This method parses JSON data from the parent class and updates the associated Django model so that its records exactly match the JSON content. It creates or updates instances based on unique fields and deletes any database entries not present in the JSON data. Raises a ValueError if required attributes are missing or if the JSON data is invalid.
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: Type[models.Model] | None = getattr(cls, "_model", None)
29
- parent_class = getattr(cls, "_parent_class", None)
30
- if model is None or parent_class is None:
31
- raise ValueError("Attribute '_model' and '_parent_class' must be set.")
32
- json_data = getattr(parent_class, "_json_data", None)
33
- if not json_data:
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 be set '_json_data'"
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 = getattr(parent_class, "_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 be defined '_unique_fields'"
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
- # Daten synchronisieren
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, _ = model.objects.get_or_create(**lookup)
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
- # Existierende Einträge abrufen und löschen, wenn nicht im JSON vorhanden
73
- existing_instances = model.objects.all()
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.delete()
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 readOnlyPostCreate(func: Callable[..., Any]) -> Callable[..., Any]:
136
+ def ensureSchemaIsUpToDate(
137
+ new_manager_class: Type[GeneralManager], model: Type[models.Model]
138
+ ) -> list[Warning]:
82
139
  """
83
- Decorator for post-creation hooks that registers the interface class as read-only.
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
- Wraps a function to be called after a class creation event, then appends the interface
86
- class to the meta-class's `read_only_classes` list.
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[GeneralManagerModel],
213
+ model: Type[GeneralManagerBasisModel],
94
214
  ):
95
- func(mcs, new_class, interface_cls, model)
96
- mcs.read_only_classes.append(interface_cls)
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
- Returns pre- and post-creation methods for integrating the interface with a GeneralManager.
104
-
105
- The pre-creation method modifies keyword arguments before a GeneralManager instance is created. The post-creation method, wrapped with a decorator, modifies the instance after creation to add additional data. These methods are intended for use by the GeneralManagerMeta class during the manager's lifecycle.
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(cls._postCreate)
270
+ return cls.readOnlyPreCreate(cls._preCreate), cls.readOnlyPostCreate(
271
+ cls._postCreate
272
+ )