GeneralManager 0.6.2__py3-none-any.whl → 0.7.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/apps.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from django.apps import AppConfig
2
2
  import graphene
3
+ import os
3
4
  from django.conf import settings
4
5
  from django.urls import path
5
6
  from graphene_django.views import GraphQLView
@@ -9,6 +10,15 @@ from general_manager.manager.meta import GeneralManagerMeta
9
10
  from general_manager.manager.input import Input
10
11
  from general_manager.api.property import graphQlProperty
11
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__)
12
22
 
13
23
 
14
24
  class GeneralmanagerConfig(AppConfig):
@@ -16,7 +26,87 @@ class GeneralmanagerConfig(AppConfig):
16
26
  name = "general_manager"
17
27
 
18
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
19
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...")
20
110
  for general_manager_class in GeneralManagerMeta.all_classes:
21
111
  attributes = getattr(general_manager_class.Interface, "input_fields", {})
22
112
  for attribute_name, attribute in attributes.items():
@@ -35,41 +125,38 @@ class GeneralmanagerConfig(AppConfig):
35
125
  graphQlProperty(func),
36
126
  )
37
127
 
38
- for (
39
- general_manager_class
40
- ) in GeneralManagerMeta.pending_attribute_initialization:
41
- attributes = general_manager_class.Interface.getAttributes()
42
- setattr(general_manager_class, "_attributes", attributes)
43
- GeneralManagerMeta.createAtPropertiesForAttributes(
44
- attributes.keys(), general_manager_class
45
- )
46
-
47
- if getattr(settings, "AUTOCREATE_GRAPHQL", False):
48
-
49
- for general_manager_class in GeneralManagerMeta.pending_graphql_interfaces:
50
- GraphQL.createGraphqlInterface(general_manager_class)
51
- GraphQL.createGraphqlMutation(general_manager_class)
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)
52
136
 
53
- query_class = type("Query", (graphene.ObjectType,), GraphQL._query_fields)
54
- GraphQL._query_class = query_class
137
+ query_class = type("Query", (graphene.ObjectType,), GraphQL._query_fields)
138
+ GraphQL._query_class = query_class
55
139
 
56
- mutation_class = type(
57
- "Mutation",
58
- (graphene.ObjectType,),
59
- {
60
- name: mutation.Field()
61
- for name, mutation in GraphQL._mutations.items()
62
- },
63
- )
64
- GraphQL._mutation_class = mutation_class
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
65
146
 
66
- schema = graphene.Schema(
67
- query=GraphQL._query_class,
68
- mutation=GraphQL._mutation_class,
69
- )
70
- self.add_graphql_url(schema)
147
+ schema = graphene.Schema(
148
+ query=GraphQL._query_class,
149
+ mutation=GraphQL._mutation_class,
150
+ )
151
+ self.addGraphqlUrl(schema)
71
152
 
72
- def add_graphql_url(self, schema):
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...")
73
160
  root_url_conf_path = getattr(settings, "ROOT_URLCONF", None)
74
161
  graph_ql_url = getattr(settings, "GRAPHQL_URL", "graphql/")
75
162
  if not root_url_conf_path:
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
  import time
3
3
  import ast
4
4
  import re
5
+ import logging
5
6
 
6
7
  from django.core.cache import cache
7
8
  from general_manager.cache.signals import post_data_change, pre_data_change
@@ -27,6 +28,8 @@ type dependency_index = dict[
27
28
  type filter_type = Literal["filter", "exclude", "identification"]
28
29
  type Dependency = Tuple[general_manager_name, filter_type, str]
29
30
 
31
+ logger = logging.getLogger(__name__)
32
+
30
33
  # -----------------------------------------------------------------------------
31
34
  # CONFIG
32
35
  # -----------------------------------------------------------------------------
@@ -181,6 +184,11 @@ def generic_cache_invalidation(
181
184
  old_relevant_values: dict[str, Any],
182
185
  **kwargs,
183
186
  ):
187
+ """
188
+ Invalidates cached query results related to a model instance when its data changes.
189
+
190
+ This function is intended to be used as a Django signal handler. It compares old and new values of relevant fields on a model instance against registered cache dependencies (filters and excludes). If a change affects any cached queryset result, the corresponding cache keys are invalidated and removed from the dependency index.
191
+ """
184
192
  manager_name = sender.__name__
185
193
  idx = get_full_index()
186
194
 
@@ -280,7 +288,7 @@ def generic_cache_invalidation(
280
288
  if action == "filter":
281
289
  # Filter: invalidate if new match or old match
282
290
  if new_match or old_match:
283
- print(
291
+ logger.info(
284
292
  f"Invalidate cache key {cache_keys} for filter {lookup} with value {val_key}"
285
293
  )
286
294
  for ck in list(cache_keys):
@@ -290,7 +298,7 @@ def generic_cache_invalidation(
290
298
  else: # action == 'exclude'
291
299
  # Excludes: invalidate only if matches changed
292
300
  if old_match != new_match:
293
- print(
301
+ logger.info(
294
302
  f"Invalidate cache key {cache_keys} for exclude {lookup} with value {val_key}"
295
303
  )
296
304
  for ck in list(cache_keys):
@@ -18,7 +18,6 @@ from general_manager.auxiliary import args_to_kwargs
18
18
  if TYPE_CHECKING:
19
19
  from general_manager.manager.input import Input
20
20
  from general_manager.manager.generalManager import GeneralManager
21
- from general_manager.manager.meta import GeneralManagerMeta
22
21
  from general_manager.bucket.baseBucket import Bucket
23
22
 
24
23
 
@@ -28,7 +27,7 @@ type attributes = dict[str, Any]
28
27
  type interfaceBaseClass = Type[InterfaceBase]
29
28
  type newlyCreatedInterfaceClass = Type[InterfaceBase]
30
29
  type relatedClass = Type[Model] | None
31
- type newlyCreatedGeneralManagerClass = GeneralManagerMeta
30
+ type newlyCreatedGeneralManagerClass = Type[GeneralManager]
32
31
 
33
32
  type classPreCreationMethod = Callable[
34
33
  [generalManagerClassName, attributes, interfaceBaseClass],
@@ -55,7 +54,7 @@ class AttributeTypedDict(TypedDict):
55
54
 
56
55
 
57
56
  class InterfaceBase(ABC):
58
- _parent_class: ClassVar[Type[Any]]
57
+ _parent_class: Type[GeneralManager]
59
58
  _interface_type: ClassVar[str]
60
59
  input_fields: dict[str, Input]
61
60
 
@@ -70,9 +69,9 @@ class InterfaceBase(ABC):
70
69
  ) -> dict[str, Any]:
71
70
  """
72
71
  Parses and validates input arguments into a structured identification dictionary.
73
-
72
+
74
73
  Converts positional and keyword arguments into a dictionary keyed by input field names, normalizing argument names and ensuring all required fields are present. Processes input fields in dependency order, casting and validating each value. Raises a `TypeError` for unexpected or missing arguments and a `ValueError` if circular dependencies among input fields are detected.
75
-
74
+
76
75
  Returns:
77
76
  A dictionary mapping input field names to their validated and cast values.
78
77
  """
@@ -131,7 +130,7 @@ class InterfaceBase(ABC):
131
130
  ) -> None:
132
131
  """
133
132
  Validates the type and allowed values of an input field.
134
-
133
+
135
134
  Ensures that the provided value matches the expected type for the specified input field. In debug mode, also checks that the value is among the allowed possible values if defined, supporting both callables and iterables. Raises a TypeError for invalid types or possible value definitions, and a ValueError if the value is not permitted.
136
135
  """
137
136
  input_field = self.input_fields[name]
@@ -218,13 +217,13 @@ class InterfaceBase(ABC):
218
217
  def getFieldType(cls, field_name: str) -> type:
219
218
  """
220
219
  Returns the type of the specified input field.
221
-
220
+
222
221
  Args:
223
222
  field_name: The name of the input field.
224
-
223
+
225
224
  Returns:
226
225
  The Python type associated with the given field name.
227
-
226
+
228
227
  Raises:
229
228
  NotImplementedError: This method must be implemented by subclasses.
230
229
  """
@@ -66,12 +66,18 @@ def getFullCleanMethode(model: Type[models.Model]) -> Callable[..., None]:
66
66
  return full_clean
67
67
 
68
68
 
69
- class GeneralManagerModel(models.Model):
69
+ class GeneralManagerBasisModel(models.Model):
70
70
  _general_manager_class: ClassVar[Type[GeneralManager]]
71
71
  is_active = models.BooleanField(default=True)
72
+ history = HistoricalRecords(inherit=True)
73
+
74
+ class Meta:
75
+ abstract = True
76
+
77
+
78
+ class GeneralManagerModel(GeneralManagerBasisModel):
72
79
  changed_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
73
80
  changed_by_id: int
74
- history = HistoricalRecords(inherit=True)
75
81
 
76
82
  @property
77
83
  def _history_user(self) -> AbstractUser:
@@ -90,12 +96,12 @@ class GeneralManagerModel(models.Model):
90
96
  """
91
97
  self.changed_by = value
92
98
 
93
- class Meta:
99
+ class Meta: # type: ignore
94
100
  abstract = True
95
101
 
96
102
 
97
103
  class DBBasedInterface(InterfaceBase):
98
- _model: ClassVar[Type[GeneralManagerModel]]
104
+ _model: Type[GeneralManagerBasisModel]
99
105
  input_fields: dict[str, Input] = {"id": Input(int)}
100
106
 
101
107
  def __init__(
@@ -113,7 +119,7 @@ class DBBasedInterface(InterfaceBase):
113
119
  self.pk = self.identification["id"]
114
120
  self._instance = self.getData(search_date)
115
121
 
116
- def getData(self, search_date: datetime | None = None) -> GeneralManagerModel:
122
+ def getData(self, search_date: datetime | None = None) -> GeneralManagerBasisModel:
117
123
  """
118
124
  Retrieves the model instance by primary key, optionally as of a specified historical date.
119
125
 
@@ -177,8 +183,8 @@ class DBBasedInterface(InterfaceBase):
177
183
 
178
184
  @classmethod
179
185
  def getHistoricalRecord(
180
- cls, instance: GeneralManagerModel, search_date: datetime | None = None
181
- ) -> GeneralManagerModel:
186
+ cls, instance: GeneralManagerBasisModel, search_date: datetime | None = None
187
+ ) -> GeneralManagerBasisModel:
182
188
  """
183
189
  Retrieves the most recent historical record of a model instance at or before a specified date.
184
190
 
@@ -440,14 +446,23 @@ class DBBasedInterface(InterfaceBase):
440
446
 
441
447
  @staticmethod
442
448
  def _preCreate(
443
- name: generalManagerClassName, attrs: attributes, interface: interfaceBaseClass
449
+ name: generalManagerClassName,
450
+ attrs: attributes,
451
+ interface: interfaceBaseClass,
452
+ base_model_class=GeneralManagerModel,
444
453
  ) -> tuple[attributes, interfaceBaseClass, relatedClass]:
445
454
  # Felder aus der Interface-Klasse sammeln
446
455
  """
447
- Dynamically creates a Django model, its associated interface class, and a factory class based on the provided interface definition.
448
-
449
- This method extracts fields and meta information from the interface class, constructs a new Django model inheriting from `GeneralManagerModel`, attaches custom validation rules if present, and generates a corresponding interface and factory class. The resulting classes are returned for further use in the general manager framework.
450
-
456
+ Dynamically creates a Django model class, its associated interface class, and a factory class based on the provided interface definition.
457
+
458
+ This method extracts fields and meta information from the interface class, constructs a new Django model inheriting from the specified base model class, attaches custom validation rules if present, and generates corresponding interface and factory classes. The resulting classes are returned for integration into the general manager framework.
459
+
460
+ Parameters:
461
+ name: The name for the dynamically created model class.
462
+ attrs: The attributes dictionary to be updated with the new interface and factory classes.
463
+ interface: The interface base class defining the model structure and metadata.
464
+ base_model_class: The base class to use for the new model (defaults to GeneralManagerModel).
465
+
451
466
  Returns:
452
467
  A tuple containing the updated attributes dictionary, the new interface class, and the newly created model class.
453
468
  """
@@ -474,7 +489,7 @@ class DBBasedInterface(InterfaceBase):
474
489
  delattr(meta_class, "rules")
475
490
 
476
491
  # Modell erstellen
477
- model = type(name, (GeneralManagerModel,), model_fields)
492
+ model = type(name, (base_model_class,), model_fields)
478
493
  if meta_class and rules:
479
494
  setattr(model._meta, "rules", rules)
480
495
  # full_clean Methode hinzufügen
@@ -5,34 +5,74 @@ 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
22
+
23
+
24
+ logger = logging.getLogger(__name__)
16
25
 
17
26
 
18
27
  class ReadOnlyInterface(DBBasedInterface):
19
28
  _interface_type = "readonly"
29
+ _model: Type[GeneralManagerBasisModel]
30
+ _parent_class: Type[GeneralManager]
20
31
 
21
- @classmethod
22
- def sync_data(cls) -> None:
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.
23
38
  """
24
- Synchronizes the database model with JSON data, ensuring exact correspondence.
39
+ opts = model._meta
40
+ unique_fields: set[str] = set()
25
41
 
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.
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:
27
59
  """
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:
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:
34
74
  raise ValueError(
35
- f"For ReadOnlyInterface '{parent_class.__name__}' must be set '_json_data'"
75
+ f"For ReadOnlyInterface '{parent_class.__name__}' must set '_data'"
36
76
  )
37
77
 
38
78
  # JSON-Daten parsen
@@ -41,67 +81,185 @@ class ReadOnlyInterface(DBBasedInterface):
41
81
  elif isinstance(json_data, list):
42
82
  data_list: list[Any] = json_data
43
83
  else:
44
- raise ValueError(
45
- "_json_data must be a JSON string or a list of dictionaries"
46
- )
84
+ raise ValueError("_data must be a JSON string or a list of dictionaries")
47
85
 
48
- unique_fields = getattr(parent_class, "_unique_fields", [])
86
+ unique_fields = cls.getUniqueFields(model)
49
87
  if not unique_fields:
50
88
  raise ValueError(
51
- f"For ReadOnlyInterface '{parent_class.__name__}' must be defined '_unique_fields'"
89
+ f"For ReadOnlyInterface '{parent_class.__name__}' must have at least one unique field."
52
90
  )
53
91
 
92
+ changes = {
93
+ "created": [],
94
+ "updated": [],
95
+ "deactivated": [],
96
+ }
97
+
54
98
  with transaction.atomic():
55
99
  json_unique_values: set[Any] = set()
56
100
 
57
- # Daten synchronisieren
101
+ # data synchronization
58
102
  for data in data_list:
59
103
  lookup = {field: data[field] for field in unique_fields}
60
104
  unique_identifier = tuple(lookup[field] for field in unique_fields)
61
105
  json_unique_values.add(unique_identifier)
62
106
 
63
- instance, _ = model.objects.get_or_create(**lookup)
107
+ instance, is_created = model.objects.get_or_create(**lookup)
64
108
  updated = False
65
109
  for field_name, value in data.items():
66
110
  if getattr(instance, field_name, None) != value:
67
111
  setattr(instance, field_name, value)
68
112
  updated = True
69
- if updated:
113
+ if updated or not instance.is_active:
114
+ instance.is_active = True
70
115
  instance.save()
116
+ changes["created" if is_created else "updated"].append(instance)
71
117
 
72
- # Existierende Einträge abrufen und löschen, wenn nicht im JSON vorhanden
73
- existing_instances = model.objects.all()
118
+ # deactivate instances not in JSON data
119
+ existing_instances = model.objects.filter(is_active=True)
74
120
  for instance in existing_instances:
75
121
  lookup = {field: getattr(instance, field) for field in unique_fields}
76
122
  unique_identifier = tuple(lookup[field] for field in unique_fields)
77
123
  if unique_identifier not in json_unique_values:
78
- instance.delete()
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
+ )
79
135
 
80
136
  @staticmethod
81
- def readOnlyPostCreate(func: Callable[..., Any]) -> Callable[..., Any]:
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.
82
149
  """
83
- Decorator for post-creation hooks that registers the interface class as read-only.
84
150
 
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.
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.
87
212
  """
88
213
 
89
214
  def wrapper(
90
- mcs: Type[GeneralManagerMeta],
91
215
  new_class: Type[GeneralManager],
92
216
  interface_cls: Type[ReadOnlyInterface],
93
- model: Type[GeneralManagerModel],
217
+ model: Type[GeneralManagerBasisModel],
94
218
  ):
95
- func(mcs, new_class, interface_cls, model)
96
- mcs.read_only_classes.append(interface_cls)
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
+ )
97
250
 
98
251
  return wrapper
99
252
 
100
253
  @classmethod
101
254
  def handleInterface(cls) -> tuple[classPreCreationMethod, classPostCreationMethod]:
102
255
  """
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.
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.
106
262
  """
107
- return cls._preCreate, cls.readOnlyPostCreate(cls._postCreate)
263
+ return cls.readOnlyPreCreate(cls._preCreate), cls.readOnlyPostCreate(
264
+ cls._postCreate
265
+ )
@@ -18,7 +18,7 @@ class _nonExistent:
18
18
 
19
19
  class GeneralManagerMeta(type):
20
20
  all_classes: list[Type[GeneralManager]] = []
21
- read_only_classes: list[Type[ReadOnlyInterface]] = []
21
+ read_only_classes: list[Type[GeneralManager]] = []
22
22
  pending_graphql_interfaces: list[Type[GeneralManager]] = []
23
23
  pending_attribute_initialization: list[Type[GeneralManager]] = []
24
24
  Interface: type[InterfaceBase]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GeneralManager
3
- Version: 0.6.2
3
+ Version: 0.7.0
4
4
  Summary: Modular Django-based data management framework with ORM, GraphQL, fine-grained permissions, rule validation, calculations and caching.
5
5
  Author-email: Tim Kleindick <tkleindick@yahoo.de>
6
6
  License-Expression: MIT
@@ -1,5 +1,5 @@
1
1
  general_manager/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- general_manager/apps.py,sha256=0QwuIAnhHm5u0wTlCiDVsl8k0RU0BEgfFjmBMl9zvsw,3320
2
+ general_manager/apps.py,sha256=sfH8a1iaAdTjTmmf7R9TZfZl_8yrce7zlsKGB1mFSQY,7935
3
3
  general_manager/api/graphql.py,sha256=a_mH5dvM3yXC1WyQKFCYpzSE00DQFLNKhRsZrwvLrNM,27453
4
4
  general_manager/api/mutation.py,sha256=uu5RVxc9wbb-Zrbtt4azegvyKymMqEsxk_CkerKCd9Q,5178
5
5
  general_manager/api/property.py,sha256=oc93p1P8dcIvrNorRuqD1EJVsd6eYttYhZuAS0s28gs,696
@@ -16,7 +16,7 @@ general_manager/bucket/databaseBucket.py,sha256=V_xiPa8ErnPHVh_-i-oaH8qCa818UJxm
16
16
  general_manager/bucket/groupBucket.py,sha256=55QdUaH_qO1JFJ2Jc1f2WcxniiLE5LB3vNwbnksKk8A,10939
17
17
  general_manager/cache/cacheDecorator.py,sha256=DK2ANIJgPpMxazfMSiFrI9OuVE_7K9zlIZQRrgaC2Lw,3268
18
18
  general_manager/cache/cacheTracker.py,sha256=rRw3OhBDf86hTC2Xbt1ocRgZqwu8_kXk4lczamcADFg,2955
19
- general_manager/cache/dependencyIndex.py,sha256=kEbIAzzMzKlQgplKfcMYBPZ562zCBkOBKvJusxO_iC4,10537
19
+ general_manager/cache/dependencyIndex.py,sha256=lxD7IfnWVsBNt9l0_yDfJlHDRHAFC7N7p-Typ2tJp88,11044
20
20
  general_manager/cache/modelDependencyCollector.py,sha256=lIqBvo0QygoxxZPJ32_vMs_-eJaVJDznGyrEmxPV41E,2436
21
21
  general_manager/cache/signals.py,sha256=ZHeXKFMN7tj9t0J-vSqf_05_NhGqEF2sZtbZO3vaRqI,1234
22
22
  general_manager/factory/__init__.py,sha256=wbPIGyBlWBHa7aGWUd-1IUMPWUS-M6YqtPUL1iKXW8U,93
@@ -24,16 +24,16 @@ general_manager/factory/autoFactory.py,sha256=WBhSuMVsxkPAPLhlZhYXwHVIqiomUveS7v
24
24
  general_manager/factory/factories.py,sha256=F6_nYFyJRYYc3LQApfoVFdctfLzsWUDHKafn6xjckB8,7224
25
25
  general_manager/factory/factoryMethods.py,sha256=9Bag891j0XHe3dUBAFi7gUKcKeUwcBZN3cDLBobyBiI,3225
26
26
  general_manager/interface/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
- general_manager/interface/baseInterface.py,sha256=b7MH-jVADcpGluvJMogPn2O3Yiy37uCnWOM9j0Ec0o0,8692
27
+ general_manager/interface/baseInterface.py,sha256=WjavtLW_vzyv3PDIIGpw0cydFXjIBJfzxflMXDNF7ac,8583
28
28
  general_manager/interface/calculationInterface.py,sha256=Kg_OqLw67tcLwdzYNLq31eKVLzkM7taw-8Mzmk0CYi0,4232
29
- general_manager/interface/databaseBasedInterface.py,sha256=K2PYxANuLQU8wzrajcl922tuWwko642KJAS3f3PnAmg,21566
29
+ general_manager/interface/databaseBasedInterface.py,sha256=NoHJp1A9kWRa5R8aal3MS--4PRsHY4JMBzpcuNd69kY,22182
30
30
  general_manager/interface/databaseInterface.py,sha256=08dFgxoLQNa13RK2NQ4cDNbNPIG-X9ChLs3NvJcSp6Y,4923
31
- general_manager/interface/readOnlyInterface.py,sha256=d2CM2gj5XZNEaVFZeNCgqZf46rwUAetVS3SyePCKNsY,4691
31
+ general_manager/interface/readOnlyInterface.py,sha256=USr4k6Jr1gz91Cdi8bbZY59cTy5MPGZ3oC40xCdWS88,10873
32
32
  general_manager/manager/__init__.py,sha256=l3RYp62aEhj3Y975_XUTIzo35LUnkTJHkb_hgChnXXI,111
33
33
  general_manager/manager/generalManager.py,sha256=HX69KhrnSGVkuJwHY_jzff5gS0VD-6fRxKnd59A5Ct4,6100
34
34
  general_manager/manager/groupManager.py,sha256=8dpZUfm7aFL4lraUWv4qbbDRClQZaYxw4prclhBZYZs,4367
35
35
  general_manager/manager/input.py,sha256=iKawV3P1QICz-0AQUF00OvH7LZYxussg3svpvCUl8hE,2977
36
- general_manager/manager/meta.py,sha256=g94L92hDy5gdGhgaTu3pjgZtt_fLC8LruNj9T34ApdA,4414
36
+ general_manager/manager/meta.py,sha256=I4HO7Tp2P0-eRKicVzZRFrYzyMshjWY2ZGP8no9Su-Y,4411
37
37
  general_manager/measurement/__init__.py,sha256=X97meFujBldE5v0WMF7SmKeGpC5R0JTczfLo_Lq1Xek,84
38
38
  general_manager/measurement/measurement.py,sha256=e_FjHieeJbBtjXGCO9J7vRPw6KCkMrOxwWjaD0m8ee4,11777
39
39
  general_manager/measurement/measurementField.py,sha256=iq9Hqe6ZGX8CxXm4nIqTAWTRkQVptzpqE9ExX-jFyNs,5928
@@ -46,8 +46,8 @@ general_manager/permission/permissionDataManager.py,sha256=Ji7fsnuaKTa6M8yzCGyzr
46
46
  general_manager/rule/__init__.py,sha256=4Har5cfPD1fmOsilTDod-ZUz3Com-tkl58jz7yY4fD0,23
47
47
  general_manager/rule/handler.py,sha256=z8SFHTIZ0LbLh3fV56Mud0V4_OvWkqJjlHvFqau7Qfk,7334
48
48
  general_manager/rule/rule.py,sha256=3FVCKGL7BTVoStdgOTdWQwuoVRIxAIAilV4VOzouDpc,10759
49
- generalmanager-0.6.2.dist-info/licenses/LICENSE,sha256=YGFm0ieb4KpkMRRt2qnWue6uFh0cUMtobwEBkHwajhc,1450
50
- generalmanager-0.6.2.dist-info/METADATA,sha256=LpZhar2vKhlCPwavT15s1yWSeMCHH6M9rqtC69BjraQ,6205
51
- generalmanager-0.6.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
52
- generalmanager-0.6.2.dist-info/top_level.txt,sha256=sTDtExP9ga-YP3h3h42yivUY-A2Q23C2nw6LNKOho4I,16
53
- generalmanager-0.6.2.dist-info/RECORD,,
49
+ generalmanager-0.7.0.dist-info/licenses/LICENSE,sha256=YGFm0ieb4KpkMRRt2qnWue6uFh0cUMtobwEBkHwajhc,1450
50
+ generalmanager-0.7.0.dist-info/METADATA,sha256=S1_khOXyPT4HXYuMLADTSI3pAHiI0bV_WRPzWUFRTCc,6205
51
+ generalmanager-0.7.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
52
+ generalmanager-0.7.0.dist-info/top_level.txt,sha256=sTDtExP9ga-YP3h3h42yivUY-A2Q23C2nw6LNKOho4I,16
53
+ generalmanager-0.7.0.dist-info/RECORD,,