GeneralManager 0.5.1__py3-none-any.whl → 0.6.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.
@@ -0,0 +1,538 @@
1
+ from __future__ import annotations
2
+ from typing import (
3
+ Type,
4
+ ClassVar,
5
+ Any,
6
+ Callable,
7
+ TYPE_CHECKING,
8
+ TypeVar,
9
+ )
10
+ from django.db import models
11
+ from django.conf import settings
12
+ from datetime import datetime, timedelta
13
+ from simple_history.models import HistoricalRecords # type: ignore
14
+ from general_manager.measurement.measurement import Measurement
15
+ from general_manager.measurement.measurementField import MeasurementField
16
+ from decimal import Decimal
17
+ from general_manager.factory.autoFactory import AutoFactory
18
+ from django.core.exceptions import ValidationError
19
+ from general_manager.interface.baseInterface import (
20
+ InterfaceBase,
21
+ classPostCreationMethod,
22
+ classPreCreationMethod,
23
+ generalManagerClassName,
24
+ attributes,
25
+ interfaceBaseClass,
26
+ newlyCreatedGeneralManagerClass,
27
+ newlyCreatedInterfaceClass,
28
+ relatedClass,
29
+ AttributeTypedDict,
30
+ )
31
+ from general_manager.manager.input import Input
32
+ from general_manager.bucket.databaseBucket import DatabaseBucket
33
+
34
+ if TYPE_CHECKING:
35
+ from general_manager.manager.generalManager import GeneralManager
36
+ from django.contrib.auth.models import AbstractUser
37
+ from general_manager.rule.rule import Rule
38
+
39
+ modelsModel = TypeVar("modelsModel", bound=models.Model)
40
+
41
+
42
+ def getFullCleanMethode(model: Type[models.Model]) -> Callable[..., None]:
43
+ """
44
+ Generates a custom `full_clean` method for a Django model that combines standard validation with additional rule-based checks.
45
+
46
+ The returned method first performs Django's built-in model validation, then evaluates any custom rules defined in the model's `_meta.rules` attribute. If any validation or rule fails, a `ValidationError` is raised containing all collected errors.
47
+ """
48
+
49
+ def full_clean(self: models.Model, *args: Any, **kwargs: Any):
50
+ errors: dict[str, Any] = {}
51
+ try:
52
+ super(model, self).full_clean(*args, **kwargs) # type: ignore
53
+ except ValidationError as e:
54
+ errors.update(e.message_dict)
55
+
56
+ rules: list[Rule] = getattr(self._meta, "rules")
57
+ for rule in rules:
58
+ if not rule.evaluate(self):
59
+ error_message = rule.getErrorMessage()
60
+ if error_message:
61
+ errors.update(error_message)
62
+
63
+ if errors:
64
+ raise ValidationError(errors)
65
+
66
+ return full_clean
67
+
68
+
69
+ class GeneralManagerModel(models.Model):
70
+ _general_manager_class: ClassVar[Type[GeneralManager]]
71
+ 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
+ history = HistoricalRecords(inherit=True)
75
+
76
+ @property
77
+ def _history_user(self) -> AbstractUser:
78
+ """
79
+ Returns the user who last modified this model instance.
80
+ """
81
+ return self.changed_by
82
+
83
+ @_history_user.setter
84
+ def _history_user(self, value: AbstractUser) -> None:
85
+ """
86
+ Sets the user responsible for the latest change to the model instance.
87
+
88
+ Args:
89
+ value: The user to associate with the change.
90
+ """
91
+ self.changed_by = value
92
+
93
+ class Meta:
94
+ abstract = True
95
+
96
+
97
+ class DBBasedInterface(InterfaceBase):
98
+ _model: ClassVar[Type[GeneralManagerModel]]
99
+ input_fields: dict[str, Input] = {"id": Input(int)}
100
+
101
+ def __init__(
102
+ self,
103
+ *args: list[Any],
104
+ search_date: datetime | None = None,
105
+ **kwargs: dict[str, Any],
106
+ ):
107
+ """
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.
111
+ """
112
+ super().__init__(*args, **kwargs)
113
+ self.pk = self.identification["id"]
114
+ self._instance = self.getData(search_date)
115
+
116
+ def getData(self, search_date: datetime | None = None) -> GeneralManagerModel:
117
+ """
118
+ Retrieves the model instance by primary key, optionally as of a specified historical date.
119
+
120
+ If a `search_date` is provided and is not within the last 5 seconds, returns the historical record of the instance as of that date; otherwise, returns the current instance.
121
+ """
122
+ model = self._model
123
+ instance = model.objects.get(pk=self.pk)
124
+ if search_date and not search_date > datetime.now() - timedelta(seconds=5):
125
+ instance = self.getHistoricalRecord(instance, search_date)
126
+ return instance
127
+
128
+ @classmethod
129
+ def filter(cls, **kwargs: Any) -> DatabaseBucket:
130
+ """
131
+ Returns a DatabaseBucket containing model instances filtered by the given criteria.
132
+
133
+ Args:
134
+ **kwargs: Field lookups to filter the queryset.
135
+
136
+ Returns:
137
+ A DatabaseBucket wrapping the filtered queryset and associated metadata.
138
+ """
139
+ return DatabaseBucket(
140
+ cls._model.objects.filter(**kwargs),
141
+ cls._parent_class,
142
+ cls.__createFilterDefinitions(**kwargs),
143
+ )
144
+
145
+ @classmethod
146
+ def exclude(cls, **kwargs: Any) -> DatabaseBucket:
147
+ """
148
+ Returns a DatabaseBucket containing model instances that do not match the given filter criteria.
149
+
150
+ Args:
151
+ **kwargs: Field lookups to exclude from the queryset.
152
+
153
+ Returns:
154
+ A DatabaseBucket wrapping the queryset of excluded model instances.
155
+ """
156
+ return DatabaseBucket(
157
+ cls._model.objects.exclude(**kwargs),
158
+ cls._parent_class,
159
+ cls.__createFilterDefinitions(**kwargs),
160
+ )
161
+
162
+ @staticmethod
163
+ def __createFilterDefinitions(**kwargs: Any) -> dict[str, Any]:
164
+ """
165
+ Creates a dictionary of filter definitions from the provided keyword arguments.
166
+
167
+ Args:
168
+ **kwargs: Key-value pairs representing filter criteria.
169
+
170
+ Returns:
171
+ A dictionary mapping filter keys to their corresponding values.
172
+ """
173
+ filter_definitions: dict[str, Any] = {}
174
+ for key, value in kwargs.items():
175
+ filter_definitions[key] = value
176
+ return filter_definitions
177
+
178
+ @classmethod
179
+ def getHistoricalRecord(
180
+ cls, instance: GeneralManagerModel, search_date: datetime | None = None
181
+ ) -> GeneralManagerModel:
182
+ """
183
+ Retrieves the most recent historical record of a model instance at or before a specified date.
184
+
185
+ Args:
186
+ instance: The model instance whose history is queried.
187
+ search_date: The cutoff datetime; returns the last record at or before this date.
188
+
189
+ Returns:
190
+ The historical model instance as of the specified date, or None if no such record exists.
191
+ """
192
+ return instance.history.filter(history_date__lte=search_date).last() # type: ignore
193
+
194
+ @classmethod
195
+ def getAttributeTypes(cls) -> dict[str, AttributeTypedDict]:
196
+ """
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.
200
+ """
201
+ TRANSLATION: dict[Type[models.Field[Any, Any]], type] = {
202
+ models.fields.BigAutoField: int,
203
+ models.AutoField: int,
204
+ models.CharField: str,
205
+ models.TextField: str,
206
+ models.BooleanField: bool,
207
+ models.IntegerField: int,
208
+ models.FloatField: float,
209
+ models.DateField: datetime,
210
+ models.DateTimeField: datetime,
211
+ MeasurementField: Measurement,
212
+ models.DecimalField: Decimal,
213
+ models.EmailField: str,
214
+ models.FileField: str,
215
+ models.ImageField: str,
216
+ models.URLField: str,
217
+ models.TimeField: datetime,
218
+ }
219
+ fields: dict[str, AttributeTypedDict] = {}
220
+ field_name_list, to_ignore_list = cls.handleCustomFields(cls._model)
221
+ for field_name in field_name_list:
222
+ field: models.Field = getattr(cls._model, field_name)
223
+ fields[field_name] = {
224
+ "type": type(field),
225
+ "is_required": not field.null,
226
+ "is_editable": field.editable,
227
+ "default": field.default,
228
+ }
229
+
230
+ for field_name in cls.__getModelFields():
231
+ if field_name not in to_ignore_list:
232
+ field: models.Field = getattr(cls._model, field_name).field
233
+ fields[field_name] = {
234
+ "type": type(field),
235
+ "is_required": not field.null,
236
+ "is_editable": field.editable,
237
+ "default": field.default,
238
+ }
239
+
240
+ for field_name in cls.__getForeignKeyFields():
241
+ field = cls._model._meta.get_field(field_name)
242
+ related_model = field.related_model
243
+ if related_model and hasattr(
244
+ related_model,
245
+ "_general_manager_class",
246
+ ):
247
+ related_model = related_model._general_manager_class
248
+
249
+ elif related_model is not None:
250
+ fields[field_name] = {
251
+ "type": related_model,
252
+ "is_required": not field.null,
253
+ "is_editable": field.editable,
254
+ "default": field.default,
255
+ }
256
+
257
+ for field_name, field_call in [
258
+ *cls.__getManyToManyFields(),
259
+ *cls.__getReverseRelations(),
260
+ ]:
261
+ if field_name in fields:
262
+ if field_call not in fields:
263
+ field_name = field_call
264
+ else:
265
+ raise ValueError("Field name already exists.")
266
+ field = cls._model._meta.get_field(field_name)
267
+ related_model = cls._model._meta.get_field(field_name).related_model
268
+ if related_model and hasattr(
269
+ related_model,
270
+ "_general_manager_class",
271
+ ):
272
+ related_model = related_model._general_manager_class
273
+ if related_model is not None:
274
+ fields[f"{field_name}_list"] = {
275
+ "type": related_model,
276
+ "is_required": False,
277
+ "is_editable": bool(field.many_to_many and field.editable),
278
+ "default": None,
279
+ }
280
+
281
+ return {
282
+ field_name: {**field, "type": TRANSLATION.get(field["type"], field["type"])}
283
+ for field_name, field in fields.items()
284
+ }
285
+
286
+ @classmethod
287
+ def getAttributes(cls) -> dict[str, Callable[[DBBasedInterface], Any]]:
288
+ """
289
+ Returns a mapping of attribute names to callables that retrieve their values from a DBBasedInterface instance.
290
+
291
+ The returned dictionary includes accessors for custom fields, model fields, foreign keys (optionally returning related interface instances), many-to-many relations, and reverse relations. For related models that have a general manager class, the accessor returns an instance of that class; otherwise, it returns the related object or queryset directly. Raises a ValueError if a field name conflict occurs.
292
+ """
293
+ field_values: dict[str, Any] = {}
294
+
295
+ field_name_list, to_ignore_list = cls.handleCustomFields(cls._model)
296
+ for field_name in field_name_list:
297
+ field_values[field_name] = lambda self, field_name=field_name: getattr(
298
+ self._instance, field_name
299
+ )
300
+
301
+ for field_name in cls.__getModelFields():
302
+ if field_name not in to_ignore_list:
303
+ field_values[field_name] = lambda self, field_name=field_name: getattr(
304
+ self._instance, field_name
305
+ )
306
+
307
+ for field_name in cls.__getForeignKeyFields():
308
+ related_model = cls._model._meta.get_field(field_name).related_model
309
+ if related_model and hasattr(
310
+ related_model,
311
+ "_general_manager_class",
312
+ ):
313
+ generalManagerClass = related_model._general_manager_class
314
+ field_values[f"{field_name}"] = (
315
+ lambda self, field_name=field_name, manager_class=generalManagerClass: manager_class(
316
+ getattr(self._instance, field_name).pk
317
+ )
318
+ )
319
+ else:
320
+ field_values[f"{field_name}"] = (
321
+ lambda self, field_name=field_name: getattr(
322
+ self._instance, field_name
323
+ )
324
+ )
325
+
326
+ for field_name, field_call in [
327
+ *cls.__getManyToManyFields(),
328
+ *cls.__getReverseRelations(),
329
+ ]:
330
+ if field_name in field_values:
331
+ if field_call not in field_values:
332
+ field_name = field_call
333
+ else:
334
+ raise ValueError("Field name already exists.")
335
+ if hasattr(
336
+ cls._model._meta.get_field(field_name).related_model,
337
+ "_general_manager_class",
338
+ ):
339
+ field_values[
340
+ f"{field_name}_list"
341
+ ] = lambda self, field_name=field_name: self._instance._meta.get_field(
342
+ field_name
343
+ ).related_model._general_manager_class.filter(
344
+ **{self._instance.__class__.__name__.lower(): self.pk}
345
+ )
346
+ else:
347
+ field_values[f"{field_name}_list"] = (
348
+ lambda self, field_call=field_call: getattr(
349
+ self._instance, field_call
350
+ ).all()
351
+ )
352
+ return field_values
353
+
354
+ @staticmethod
355
+ def handleCustomFields(
356
+ model: Type[models.Model] | models.Model,
357
+ ) -> tuple[list[str], list[str]]:
358
+ """
359
+ Identifies custom fields on a model and their associated auxiliary fields to ignore.
360
+
361
+ Returns:
362
+ A tuple containing a list of custom field names and a list of related field names
363
+ (typically suffixed with '_value' and '_unit') that should be ignored.
364
+ """
365
+ field_name_list: list[str] = []
366
+ to_ignore_list: list[str] = []
367
+ for field_name in DBBasedInterface._getCustomFields(model):
368
+ to_ignore_list.append(f"{field_name}_value")
369
+ to_ignore_list.append(f"{field_name}_unit")
370
+ field_name_list.append(field_name)
371
+
372
+ return field_name_list, to_ignore_list
373
+
374
+ @staticmethod
375
+ def _getCustomFields(model: Type[models.Model] | models.Model) -> list[str]:
376
+ """
377
+ Returns a list of custom field names defined directly on the model class.
378
+
379
+ Args:
380
+ model: The Django model class or instance to inspect.
381
+
382
+ Returns:
383
+ A list of field names that are defined as class attributes on the model, not via Django's meta system.
384
+ """
385
+ return [
386
+ field.name
387
+ for field in model.__dict__.values()
388
+ if isinstance(field, models.Field)
389
+ ]
390
+
391
+ @classmethod
392
+ def __getModelFields(cls):
393
+ """
394
+ Returns a list of model field names that are not many-to-many or related fields.
395
+
396
+ Excludes fields representing many-to-many relationships and related models.
397
+ """
398
+ return [
399
+ field.name
400
+ for field in cls._model._meta.get_fields()
401
+ if not field.many_to_many and not field.related_model
402
+ ]
403
+
404
+ @classmethod
405
+ def __getForeignKeyFields(cls):
406
+ """
407
+ Returns a list of field names for all foreign key and one-to-one relations on the model.
408
+ """
409
+ return [
410
+ field.name
411
+ for field in cls._model._meta.get_fields()
412
+ if field.is_relation and (field.many_to_one or field.one_to_one)
413
+ ]
414
+
415
+ @classmethod
416
+ def __getManyToManyFields(cls):
417
+ """
418
+ Returns a list of tuples containing the names of all many-to-many fields on the model.
419
+
420
+ Each tuple consists of the field name repeated twice.
421
+ """
422
+ return [
423
+ (field.name, field.name)
424
+ for field in cls._model._meta.get_fields()
425
+ if field.is_relation and field.many_to_many
426
+ ]
427
+
428
+ @classmethod
429
+ def __getReverseRelations(cls):
430
+ """
431
+ Returns a list of tuples representing reverse one-to-many relations for the model.
432
+
433
+ Each tuple contains the related field's name and its default related accessor name.
434
+ """
435
+ return [
436
+ (field.name, f"{field.name}_set")
437
+ for field in cls._model._meta.get_fields()
438
+ if field.is_relation and field.one_to_many
439
+ ]
440
+
441
+ @staticmethod
442
+ def _preCreate(
443
+ name: generalManagerClassName, attrs: attributes, interface: interfaceBaseClass
444
+ ) -> tuple[attributes, interfaceBaseClass, relatedClass]:
445
+ # Felder aus der Interface-Klasse sammeln
446
+ """
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
+
451
+ Returns:
452
+ A tuple containing the updated attributes dictionary, the new interface class, and the newly created model class.
453
+ """
454
+ model_fields: dict[str, Any] = {}
455
+ meta_class = None
456
+ for attr_name, attr_value in interface.__dict__.items():
457
+ if not attr_name.startswith("__"):
458
+ if attr_name == "Meta" and isinstance(attr_value, type):
459
+ # Meta-Klasse speichern
460
+ meta_class = attr_value
461
+ elif attr_name == "Factory":
462
+ # Factory nicht in model_fields speichern
463
+ pass
464
+ else:
465
+ model_fields[attr_name] = attr_value
466
+ model_fields["__module__"] = attrs.get("__module__")
467
+ # Meta-Klasse hinzufügen oder erstellen
468
+ rules: list[Rule] | None = None
469
+ if meta_class:
470
+ model_fields["Meta"] = meta_class
471
+
472
+ if hasattr(meta_class, "rules"):
473
+ rules = getattr(meta_class, "rules")
474
+ delattr(meta_class, "rules")
475
+
476
+ # Modell erstellen
477
+ model = type(name, (GeneralManagerModel,), model_fields)
478
+ if meta_class and rules:
479
+ setattr(model._meta, "rules", rules)
480
+ # full_clean Methode hinzufügen
481
+ model.full_clean = getFullCleanMethode(model)
482
+ # Interface-Typ bestimmen
483
+ attrs["_interface_type"] = interface._interface_type
484
+ interface_cls = type(interface.__name__, (interface,), {})
485
+ setattr(interface_cls, "_model", model)
486
+ attrs["Interface"] = interface_cls
487
+
488
+ # add factory class
489
+ factory_definition = getattr(interface, "Factory", None)
490
+ factory_attributes: dict[str, Any] = {}
491
+ if factory_definition:
492
+ for attr_name, attr_value in factory_definition.__dict__.items():
493
+ if not attr_name.startswith("__"):
494
+ factory_attributes[attr_name] = attr_value
495
+ factory_attributes["interface"] = interface_cls
496
+ factory_attributes["Meta"] = type("Meta", (), {"model": model})
497
+ factory_class = type(f"{name}Factory", (AutoFactory,), factory_attributes)
498
+ # factory_class._meta.model = model
499
+ attrs["Factory"] = factory_class
500
+
501
+ return attrs, interface_cls, model
502
+
503
+ @staticmethod
504
+ def _postCreate(
505
+ new_class: newlyCreatedGeneralManagerClass,
506
+ interface_class: newlyCreatedInterfaceClass,
507
+ model: relatedClass,
508
+ ) -> None:
509
+ """
510
+ Finalizes the setup of dynamically created classes by linking the interface and model to the new general manager class.
511
+
512
+ This method sets the `_parent_class` attribute on the interface class and attaches the new general manager class to the model via the `_general_manager_class` attribute.
513
+ """
514
+ interface_class._parent_class = new_class
515
+ setattr(model, "_general_manager_class", new_class)
516
+
517
+ @classmethod
518
+ def handleInterface(
519
+ cls,
520
+ ) -> tuple[classPreCreationMethod, classPostCreationMethod]:
521
+ """
522
+ Returns the pre- and post-creation hooks for initializing the interface.
523
+
524
+ The pre-creation method is called before the GeneralManager class is created to allow customization, while the post-creation method is called after creation to finalize setup.
525
+ """
526
+ return cls._preCreate, cls._postCreate
527
+
528
+ @classmethod
529
+ def getFieldType(cls, field_name: str) -> type:
530
+ """
531
+ Returns the type associated with the specified field name.
532
+
533
+ If the field is a relation and its related model defines a `_general_manager_class`, that class is returned; otherwise, returns the Django field type.
534
+ """
535
+ field = cls._model._meta.get_field(field_name)
536
+ if field.is_relation and field.related_model:
537
+ return field.related_model._general_manager_class
538
+ return type(field)