GeneralManager 0.7.0__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.
@@ -5,19 +5,19 @@ from decimal import Decimal
5
5
  from datetime import date, datetime
6
6
  import json
7
7
 
8
- # Eigene Module
9
8
  from general_manager.measurement.measurement import Measurement
10
9
  from general_manager.manager.generalManager import GeneralManagerMeta, GeneralManager
11
10
  from general_manager.api.property import GraphQLProperty
12
11
  from general_manager.bucket.baseBucket import Bucket
13
12
  from general_manager.interface.baseInterface import InterfaceBase
13
+ from django.db.models import NOT_PROVIDED
14
14
 
15
15
  if TYPE_CHECKING:
16
16
  from general_manager.permission.basePermission import BasePermission
17
17
  from graphene import ResolveInfo as GraphQLResolveInfo
18
18
 
19
19
 
20
- class MeasurementType(graphene.ObjectType): # type: ignore
20
+ class MeasurementType(graphene.ObjectType):
21
21
  value = graphene.Float()
22
22
  unit = graphene.String()
23
23
 
@@ -102,11 +102,9 @@ class GraphQL:
102
102
  @classmethod
103
103
  def createGraphqlInterface(cls, generalManagerClass: GeneralManagerMeta) -> None:
104
104
  """
105
- Erzeugt ein GraphQL-Interface für die übergebene Manager-Klasse.
106
- Dabei werden:
107
- - Attribute aus dem Interface in Graphene-Felder abgebildet
108
- - Zu jedem Feld ein Resolver generiert und hinzugefügt
109
- - Der neue Type in das Registry eingetragen und Queries angehängt.
105
+ Generates and registers a GraphQL ObjectType for the specified GeneralManager class.
106
+
107
+ This method maps interface attributes and GraphQLProperty attributes to Graphene fields, creates corresponding resolvers, registers the resulting type in the internal registry, and attaches relevant query fields to the schema.
110
108
  """
111
109
  interface_cls: InterfaceBase | None = getattr(
112
110
  generalManagerClass, "Interface", None
@@ -117,14 +115,14 @@ class GraphQL:
117
115
  graphene_type_name = f"{generalManagerClass.__name__}Type"
118
116
  fields: dict[str, Any] = {}
119
117
 
120
- # Felder aus dem Interface mappen
118
+ # Map Attribute Types to Graphene Fields
121
119
  for field_name, field_info in interface_cls.getAttributeTypes().items():
122
120
  field_type = field_info["type"]
123
121
  fields[field_name] = cls._mapFieldToGrapheneRead(field_type, field_name)
124
122
  resolver_name = f"resolve_{field_name}"
125
123
  fields[resolver_name] = cls._createResolver(field_name, field_type)
126
124
 
127
- # Zusätzliche GraphQLPropertys verarbeiten
125
+ # handle GraphQLProperty attributes
128
126
  for attr_name, attr_value in generalManagerClass.__dict__.items():
129
127
  if isinstance(attr_value, GraphQLProperty):
130
128
  type_hints = get_args(attr_value.graphql_type_hint)
@@ -350,10 +348,14 @@ class GraphQL:
350
348
  base_getter: Callable[[Any], Any], fallback_manager_class: type[GeneralManager]
351
349
  ) -> Callable[..., Any]:
352
350
  """
353
- Erzeugt einen Resolver für List-Felder, der:
354
- - Eine Basisabfrage (base_queryset) über den base_getter ermittelt
355
- - Zuerst die permission-basierten Filter anwendet
356
- - Anschließend Filter, Excludes, Sortierung und Paginierung übernimmt
351
+ Creates a resolver function for list fields that applies permission filters, query filters, sorting, pagination, and optional grouping to a queryset.
352
+
353
+ Parameters:
354
+ base_getter (Callable): Function to obtain the base queryset from the parent instance.
355
+ fallback_manager_class (type[GeneralManager]): Manager class to use if the queryset does not specify one.
356
+
357
+ Returns:
358
+ Callable: A resolver function for use in GraphQL list fields.
357
359
  """
358
360
 
359
361
  def resolver(
@@ -367,8 +369,23 @@ class GraphQL:
367
369
  page_size: int | None = None,
368
370
  group_by: list[str] | None = None,
369
371
  ) -> Any:
372
+ """
373
+ Resolves a list field by applying permission filters, query parameters, sorting, pagination, and optional grouping to a queryset.
374
+
375
+ Parameters:
376
+ filter: Optional filter criteria as a dictionary or JSON string.
377
+ exclude: Optional exclusion criteria as a dictionary or JSON string.
378
+ sort_by: Optional sorting field as a Graphene Enum.
379
+ reverse: If True, reverses the sort order.
380
+ page: Optional page number for pagination.
381
+ page_size: Optional number of items per page.
382
+ group_by: Optional list of field names to group results by.
383
+
384
+ Returns:
385
+ The filtered, sorted, paginated, and optionally grouped queryset.
386
+ """
370
387
  base_queryset = base_getter(self)
371
- # Verwende _manager_class aus dem Attribut falls vorhanden, ansonsten das Fallback
388
+ # use _manager_class from the attribute if available, otherwise fallback
372
389
  manager_class = getattr(
373
390
  base_queryset, "_manager_class", fallback_manager_class
374
391
  )
@@ -441,8 +458,9 @@ class GraphQL:
441
458
  cls, graphene_type: type, generalManagerClass: GeneralManagerMeta
442
459
  ) -> None:
443
460
  """
444
- Fügt dem Schema Abfragen hinzu (Liste und Einzelobjekt) basierend auf der
445
- GeneralManager-Klasse.
461
+ Adds list and single-item query fields for a GeneralManager-derived class to the GraphQL schema.
462
+
463
+ This method registers both a list query (with filtering, sorting, pagination, and grouping) and a single-item query (using identification fields) for the specified manager class. The corresponding resolvers are also attached to the schema.
446
464
  """
447
465
  if not issubclass(generalManagerClass, GeneralManager):
448
466
  raise TypeError(
@@ -452,7 +470,7 @@ class GraphQL:
452
470
  if not hasattr(cls, "_query_fields"):
453
471
  cls._query_fields: dict[str, Any] = {}
454
472
 
455
- # Resolver und Feld für die Listenabfrage
473
+ # resolver and field for the list query
456
474
  list_field_name = f"{generalManagerClass.__name__.lower()}_list"
457
475
  filter_options = cls._createFilterOptions(
458
476
  generalManagerClass.__name__.lower(), generalManagerClass
@@ -475,7 +493,7 @@ class GraphQL:
475
493
  cls._query_fields[list_field_name] = list_field
476
494
  cls._query_fields[f"resolve_{list_field_name}"] = list_resolver
477
495
 
478
- # Resolver und Feld für die Einzelobjektabfrage
496
+ # resolver and field for the single item query
479
497
  item_field_name = generalManagerClass.__name__.lower()
480
498
  identification_fields = {}
481
499
  for (
@@ -505,11 +523,21 @@ class GraphQL:
505
523
 
506
524
  @classmethod
507
525
  def createWriteFields(cls, interface_cls: InterfaceBase) -> dict[str, Any]:
526
+ """
527
+ Generate a dictionary of Graphene input fields for mutations based on the attributes of the provided interface class.
528
+
529
+ Skips fields that are system-managed (`changed_by`, `created_at`, `updated_at`) or marked as derived. For attributes referencing `GeneralManager` subclasses, uses ID fields; for list references, uses a list of IDs. All other types are mapped to their corresponding Graphene scalar types. Each field is annotated with an `editable` attribute indicating if it can be modified. Adds an optional `history_comment` field marked as editable.
530
+
531
+ Returns:
532
+ dict[str, Any]: A dictionary mapping attribute names to Graphene input fields for use in mutation arguments.
533
+ """
508
534
  fields: dict[str, Any] = {}
509
535
 
510
536
  for name, info in interface_cls.getAttributeTypes().items():
511
537
  if name in ["changed_by", "created_at", "updated_at"]:
512
538
  continue
539
+ if info["is_derived"]:
540
+ continue
513
541
 
514
542
  typ = info["type"]
515
543
  req = info["is_required"]
@@ -534,11 +562,11 @@ class GraphQL:
534
562
  default_value=default,
535
563
  )
536
564
 
537
- # Markierung, damit Dein generate*-Code weiß, was editable ist
565
+ # mark for generate* code to know what is editable
538
566
  setattr(fld, "editable", info["is_editable"])
539
567
  fields[name] = fld
540
568
 
541
- # history_comment bleibt optional ohne Default
569
+ # history_comment is always optional without a default value
542
570
  fields["history_comment"] = graphene.String()
543
571
  setattr(fields["history_comment"], "editable", True)
544
572
 
@@ -551,7 +579,12 @@ class GraphQL:
551
579
  default_return_values: dict[str, Any],
552
580
  ) -> type[graphene.Mutation] | None:
553
581
  """
554
- Generiert eine Mutation-Klasse für die Erstellung eines Objekts.
582
+ Generates a Graphene mutation class for creating an instance of the specified GeneralManager subclass.
583
+
584
+ The generated mutation class defines a `mutate` method that filters out fields with `NOT_PROVIDED` values, invokes the `create` method on the manager class with the provided arguments and the current user's ID, and returns a dictionary indicating success or failure along with any errors and the created instance.
585
+
586
+ Returns:
587
+ The generated Graphene mutation class, or None if the manager class does not define an interface.
555
588
  """
556
589
  interface_cls: InterfaceBase | None = getattr(
557
590
  generalManagerClass, "Interface", None
@@ -563,26 +596,32 @@ class GraphQL:
563
596
  self,
564
597
  info: GraphQLResolveInfo,
565
598
  **kwargs: dict[str, Any],
566
- ) -> GeneralManager:
599
+ ) -> dict:
600
+ """
601
+ Creates a new instance of the specified manager class using provided input arguments.
602
+
603
+ Filters out fields with default "not provided" values before creation. Returns a dictionary indicating success status, any errors encountered, and the created instance under a key named after the manager class.
604
+ """
567
605
  try:
606
+ kwargs = {
607
+ field_name: value
608
+ for field_name, value in kwargs.items()
609
+ if value is not NOT_PROVIDED
610
+ }
568
611
  instance = generalManagerClass.create(
569
612
  **kwargs, creator_id=info.context.user.id
570
613
  )
571
614
  except Exception as e:
572
- return self.__class__(
573
- **{
574
- "success": False,
575
- "errors": [str(e)],
576
- generalManagerClass.__name__: None,
577
- }
578
- )
579
- return self.__class__(
580
- **{
581
- "success": True,
582
- "errors": [],
583
- generalManagerClass.__name__: instance,
615
+ return {
616
+ "success": False,
617
+ "errors": [str(e)],
584
618
  }
585
- )
619
+
620
+ return {
621
+ "success": True,
622
+ "errors": [],
623
+ generalManagerClass.__name__: instance,
624
+ }
586
625
 
587
626
  return type(
588
627
  f"Create{generalManagerClass.__name__}",
@@ -612,7 +651,12 @@ class GraphQL:
612
651
  default_return_values: dict[str, Any],
613
652
  ) -> type[graphene.Mutation] | None:
614
653
  """
615
- Generiert eine Mutation-Klasse für die Aktualisierung eines Objekts.
654
+ Generates a GraphQL mutation class for updating an instance of a GeneralManager subclass.
655
+
656
+ The generated mutation accepts editable fields as arguments, calls the `update` method on the manager instance, and returns a dictionary indicating success, errors, and the updated instance. If the manager class does not define an `Interface`, returns None.
657
+
658
+ Returns:
659
+ The generated Graphene mutation class, or None if no interface is defined.
616
660
  """
617
661
  interface_cls: InterfaceBase | None = getattr(
618
662
  generalManagerClass, "Interface", None
@@ -624,27 +668,32 @@ class GraphQL:
624
668
  self,
625
669
  info: GraphQLResolveInfo,
626
670
  **kwargs: dict[str, Any],
627
- ) -> GeneralManager:
671
+ ) -> dict:
672
+ """
673
+ Handles the update mutation for a GeneralManager instance, applying provided field updates and returning the operation result.
674
+
675
+ Parameters:
676
+ info (GraphQLResolveInfo): GraphQL resolver context containing user and request information.
677
+ **kwargs: Fields to update, including the required 'id' of the instance.
678
+
679
+ Returns:
680
+ dict: A dictionary containing the success status, any error messages, and the updated instance keyed by its class name.
681
+ """
628
682
  try:
629
683
  manager_id = kwargs.pop("id", None)
630
684
  instance = generalManagerClass(manager_id).update(
631
685
  creator_id=info.context.user.id, **kwargs
632
686
  )
633
687
  except Exception as e:
634
- return self.__class__(
635
- **{
636
- "success": False,
637
- "errors": [str(e)],
638
- generalManagerClass.__name__: None,
639
- }
640
- )
641
- return self.__class__(
642
- **{
643
- "success": True,
644
- "errors": [],
645
- generalManagerClass.__name__: instance,
688
+ return {
689
+ "success": False,
690
+ "errors": [str(e)],
646
691
  }
647
- )
692
+ return {
693
+ "success": True,
694
+ "errors": [],
695
+ generalManagerClass.__name__: instance,
696
+ }
648
697
 
649
698
  return type(
650
699
  f"Create{generalManagerClass.__name__}",
@@ -674,7 +723,9 @@ class GraphQL:
674
723
  default_return_values: dict[str, Any],
675
724
  ) -> type[graphene.Mutation] | None:
676
725
  """
677
- Generiert eine Mutation-Klasse für die Löschung eines Objekts.
726
+ Generates a GraphQL mutation class for deleting (deactivating) an instance of a GeneralManager subclass.
727
+
728
+ The generated mutation accepts input fields defined in the manager's interface, deactivates the specified instance, and returns a dictionary indicating success or failure, along with any errors and the deleted instance.
678
729
  """
679
730
  interface_cls: InterfaceBase | None = getattr(
680
731
  generalManagerClass, "Interface", None
@@ -686,27 +737,28 @@ class GraphQL:
686
737
  self,
687
738
  info: GraphQLResolveInfo,
688
739
  **kwargs: dict[str, Any],
689
- ) -> GeneralManager:
740
+ ) -> dict:
741
+ """
742
+ Deletes (deactivates) an instance of the specified GeneralManager class and returns the operation result.
743
+
744
+ Returns:
745
+ dict: A dictionary containing the success status, any error messages, and the deactivated instance under the class name key.
746
+ """
690
747
  try:
691
748
  manager_id = kwargs.pop("id", None)
692
749
  instance = generalManagerClass(manager_id).deactivate(
693
750
  creator_id=info.context.user.id
694
751
  )
695
752
  except Exception as e:
696
- return self.__class__(
697
- **{
698
- "success": False,
699
- "errors": [str(e)],
700
- generalManagerClass.__name__: None,
701
- }
702
- )
703
- return self.__class__(
704
- **{
705
- "success": True,
706
- "errors": [],
707
- generalManagerClass.__name__: instance,
753
+ return {
754
+ "success": False,
755
+ "errors": [str(e)],
708
756
  }
709
- )
757
+ return {
758
+ "success": True,
759
+ "errors": [],
760
+ generalManagerClass.__name__: instance,
761
+ }
710
762
 
711
763
  return type(
712
764
  f"Delete{generalManagerClass.__name__}",
@@ -5,20 +5,21 @@ import graphene
5
5
  from general_manager.api.graphql import GraphQL
6
6
  from general_manager.manager.generalManager import GeneralManager
7
7
 
8
-
9
- def snake_to_pascal(s: str) -> str:
10
- return "".join(p.title() for p in s.split("_"))
11
-
12
-
13
- def snake_to_camel(s: str) -> str:
14
- parts = s.split("_")
15
- return parts[0] + "".join(p.title() for p in parts[1:])
8
+ from general_manager.auxiliary.formatString import snake_to_camel
16
9
 
17
10
 
18
11
  def graphQlMutation(needs_role: Optional[str] = None, auth_required: bool = False):
19
12
  """
20
- Decorator to generate a graphene.Mutation from a function and register it.
21
- :param auth_required: if True, enforces that info.context.user is authenticated.
13
+ Decorator that transforms a function into a GraphQL mutation class and registers it for use in a Graphene-based API.
14
+
15
+ The decorated function must have type hints for all parameters (except `info`) and a return annotation. The decorator dynamically generates a mutation class with arguments and output fields based on the function's signature and return type. It also enforces authentication if `auth_required` is set to True, returning an error if the user is not authenticated.
16
+
17
+ Parameters:
18
+ needs_role (Optional[str]): Reserved for future use to specify a required user role.
19
+ auth_required (bool): If True, the mutation requires an authenticated user.
20
+
21
+ Returns:
22
+ Callable: A decorator that registers the mutation and returns the original function.
22
23
  """
23
24
 
24
25
  def decorator(fn):
general_manager/apps.py CHANGED
@@ -1,3 +1,4 @@
1
+ from __future__ import annotations
1
2
  from django.apps import AppConfig
2
3
  import graphene
3
4
  import os
@@ -10,9 +11,10 @@ from general_manager.manager.meta import GeneralManagerMeta
10
11
  from general_manager.manager.input import Input
11
12
  from general_manager.api.property import graphQlProperty
12
13
  from general_manager.api.graphql import GraphQL
13
- from typing import TYPE_CHECKING, Type
14
+ from typing import TYPE_CHECKING, Type, Any, cast
14
15
  from django.core.checks import register
15
16
  import logging
17
+ from django.core.management.base import BaseCommand
16
18
 
17
19
 
18
20
  if TYPE_CHECKING:
@@ -27,9 +29,9 @@ class GeneralmanagerConfig(AppConfig):
27
29
 
28
30
  def ready(self):
29
31
  """
30
- Initializes the general_manager app when Django starts.
32
+ Initializes the general_manager app on Django startup.
31
33
 
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.
34
+ Sets up read-only interface synchronization and schema validation, initializes general manager class attributes and interconnections, and configures the GraphQL schema and endpoint if enabled in settings.
33
35
  """
34
36
  self.handleReadOnlyInterface()
35
37
  self.initializeGeneralManagerClasses()
@@ -38,16 +40,18 @@ class GeneralmanagerConfig(AppConfig):
38
40
 
39
41
  def handleReadOnlyInterface(self):
40
42
  """
41
- Sets up synchronization and schema validation for all registered read-only interfaces.
43
+ Configures synchronization and schema validation for all registered read-only interfaces.
42
44
 
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.
45
+ This method ensures that read-only interfaces are synchronized before Django management commands execute and registers system checks to validate that each read-only interface's schema remains current.
44
46
  """
45
47
  self.patchReadOnlyInterfaceSync(GeneralManagerMeta.read_only_classes)
46
48
  from general_manager.interface.readOnlyInterface import ReadOnlyInterface
47
49
 
48
50
  logger.debug("starting to register ReadOnlyInterface schema warnings...")
49
51
  for general_manager_class in GeneralManagerMeta.read_only_classes:
50
- read_only_interface: ReadOnlyInterface = general_manager_class.Interface # type: ignore
52
+ read_only_interface = cast(
53
+ Type[ReadOnlyInterface], general_manager_class.Interface
54
+ )
51
55
 
52
56
  register(
53
57
  lambda app_configs, model=read_only_interface._model, manager_class=general_manager_class, **kwargs: ReadOnlyInterface.ensureSchemaIsUpToDate(
@@ -57,29 +61,37 @@ class GeneralmanagerConfig(AppConfig):
57
61
  )
58
62
 
59
63
  @staticmethod
60
- def patchReadOnlyInterfaceSync(general_manager_classes: list[Type[GeneralManager]]):
64
+ def patchReadOnlyInterfaceSync(
65
+ general_manager_classes: list[Type[GeneralManager[Any, ReadOnlyInterface]]],
66
+ ):
61
67
  """
62
- Monkey-patches Django's management command runner to synchronize read-only interface data before executing commands.
68
+ Monkey-patches Django's management command runner to synchronize data for all provided read-only interfaces before executing management commands.
63
69
 
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`.
70
+ For each general manager class, its associated read-only interface's `syncData` method is called prior to command execution, except during autoreload subprocesses of `runserver`.
65
71
  """
66
- from django.core.management.base import BaseCommand
72
+ from general_manager.interface.readOnlyInterface import ReadOnlyInterface
67
73
 
68
74
  original_run_from_argv = BaseCommand.run_from_argv
69
75
 
70
76
  def run_from_argv_with_sync(self, argv):
71
77
  # Ensure syncData is only called at real run of runserver
72
78
  """
73
- Runs the management command and synchronizes read-only interface data before execution when appropriate.
79
+ Executes a Django management command, synchronizing all registered read-only interfaces before execution unless running an autoreload subprocess of 'runserver'.
74
80
 
75
- Synchronization occurs for all registered read-only interfaces unless the command is 'runserver' in an autoreload subprocess.
81
+ Parameters:
82
+ argv (list): Command-line arguments for the management command.
83
+
84
+ Returns:
85
+ The result of the original management command execution.
76
86
  """
77
87
  run_main = os.environ.get("RUN_MAIN") == "true"
78
88
  command = argv[1] if len(argv) > 1 else None
79
89
  if command != "runserver" or run_main:
80
90
  logger.debug("start syncing ReadOnlyInterface data...")
81
91
  for general_manager_class in general_manager_classes:
82
- read_only_interface: ReadOnlyInterface = general_manager_class.Interface # type: ignore
92
+ read_only_interface = cast(
93
+ Type[ReadOnlyInterface], general_manager_class.Interface
94
+ )
83
95
  read_only_interface.syncData()
84
96
 
85
97
  logger.debug("finished syncing ReadOnlyInterface data.")
@@ -90,9 +102,9 @@ class GeneralmanagerConfig(AppConfig):
90
102
 
91
103
  def initializeGeneralManagerClasses(self):
92
104
  """
93
- Initializes attributes and interconnections for all GeneralManager classes.
105
+ Initializes attributes and sets up dynamic relationships for all registered GeneralManager classes.
94
106
 
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.
107
+ For each pending GeneralManager class, assigns its interface attributes and creates property accessors. Then, for all GeneralManager classes, dynamically connects input fields that reference other GeneralManager subclasses by adding GraphQL properties to enable filtered access to related objects.
96
108
  """
97
109
  logger.debug("Initializing GeneralManager classes...")
98
110
 
@@ -152,9 +164,10 @@ class GeneralmanagerConfig(AppConfig):
152
164
 
153
165
  def addGraphqlUrl(self, schema):
154
166
  """
155
- Dynamically adds a GraphQL endpoint to the Django URL configuration using the provided schema.
167
+ Dynamically appends a GraphQL endpoint to the Django URL configuration using the given schema.
156
168
 
157
- Raises an exception if the ROOT_URLCONF setting is not defined.
169
+ Raises:
170
+ Exception: If the ROOT_URLCONF setting is not defined in Django settings.
158
171
  """
159
172
  logging.debug("Adding GraphQL URL to Django settings...")
160
173
  root_url_conf_path = getattr(settings, "ROOT_URLCONF", None)
@@ -0,0 +1,60 @@
1
+ def snake_to_pascal(s: str) -> str:
2
+ """
3
+ Convert a snake_case string to PascalCase.
4
+
5
+ Parameters:
6
+ s (str): The input string in snake_case format.
7
+
8
+ Returns:
9
+ str: The converted string in PascalCase format.
10
+ """
11
+ return "".join(p.title() for p in s.split("_"))
12
+
13
+
14
+ def snake_to_camel(s: str) -> str:
15
+ """
16
+ Convert a snake_case string to camelCase.
17
+
18
+ Parameters:
19
+ s (str): The snake_case string to convert.
20
+
21
+ Returns:
22
+ str: The input string converted to camelCase.
23
+ """
24
+ parts = s.split("_")
25
+ return parts[0] + "".join(p.title() for p in parts[1:])
26
+
27
+
28
+ def pascal_to_snake(s: str) -> str:
29
+ """
30
+ Convert a PascalCase string to snake_case.
31
+
32
+ Parameters:
33
+ s (str): The PascalCase string to convert.
34
+
35
+ Returns:
36
+ str: The converted snake_case string.
37
+ """
38
+ return "".join(["_" + c.lower() if c.isupper() else c for c in s]).lstrip("_")
39
+
40
+
41
+ def camel_to_snake(s: str) -> str:
42
+ """
43
+ Convert a camelCase string to snake_case.
44
+
45
+ Parameters:
46
+ s (str): The camelCase string to convert.
47
+
48
+ Returns:
49
+ str: The converted snake_case string. Returns an empty string if the input is empty.
50
+ """
51
+ if not s:
52
+ return ""
53
+ parts = [s[0].lower()]
54
+ for c in s[1:]:
55
+ if c.isupper():
56
+ parts.append("_")
57
+ parts.append(c.lower())
58
+ else:
59
+ parts.append(c)
60
+ return "".join(parts)
@@ -51,6 +51,7 @@ class AttributeTypedDict(TypedDict):
51
51
  default: Any
52
52
  is_required: bool
53
53
  is_editable: bool
54
+ is_derived: bool
54
55
 
55
56
 
56
57
  class InterfaceBase(ABC):
@@ -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
@@ -76,13 +69,18 @@ class GeneralManagerBasisModel(models.Model):
76
69
 
77
70
 
78
71
  class GeneralManagerModel(GeneralManagerBasisModel):
79
- changed_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
80
- changed_by_id: int
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
81
76
 
82
77
  @property
83
- def _history_user(self) -> AbstractUser:
78
+ def _history_user(self) -> AbstractUser | None:
84
79
  """
85
- 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.
86
84
  """
87
85
  return self.changed_by
88
86
 
@@ -100,8 +98,11 @@ class GeneralManagerModel(GeneralManagerBasisModel):
100
98
  abstract = True
101
99
 
102
100
 
103
- class DBBasedInterface(InterfaceBase):
104
- _model: Type[GeneralManagerBasisModel]
101
+ MODEL_TYPE = TypeVar("MODEL_TYPE", bound=GeneralManagerBasisModel)
102
+
103
+
104
+ class DBBasedInterface(InterfaceBase, Generic[MODEL_TYPE]):
105
+ _model: Type[MODEL_TYPE]
105
106
  input_fields: dict[str, Input] = {"id": Input(int)}
106
107
 
107
108
  def __init__(
@@ -111,9 +112,9 @@ class DBBasedInterface(InterfaceBase):
111
112
  **kwargs: dict[str, Any],
112
113
  ):
113
114
  """
114
- Initializes the interface instance and loads the corresponding model record.
115
-
116
- 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.
117
118
  """
118
119
  super().__init__(*args, **kwargs)
119
120
  self.pk = self.identification["id"]
@@ -200,9 +201,12 @@ class DBBasedInterface(InterfaceBase):
200
201
  @classmethod
201
202
  def getAttributeTypes(cls) -> dict[str, AttributeTypedDict]:
202
203
  """
203
- Returns a dictionary mapping attribute names to their type information and metadata.
204
-
205
- 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.
206
210
  """
207
211
  TRANSLATION: dict[Type[models.Field[Any, Any]], type] = {
208
212
  models.fields.BigAutoField: int,
@@ -228,6 +232,7 @@ class DBBasedInterface(InterfaceBase):
228
232
  field: models.Field = getattr(cls._model, field_name)
229
233
  fields[field_name] = {
230
234
  "type": type(field),
235
+ "is_derived": False,
231
236
  "is_required": not field.null,
232
237
  "is_editable": field.editable,
233
238
  "default": field.default,
@@ -238,6 +243,7 @@ class DBBasedInterface(InterfaceBase):
238
243
  field: models.Field = getattr(cls._model, field_name).field
239
244
  fields[field_name] = {
240
245
  "type": type(field),
246
+ "is_derived": False,
241
247
  "is_required": not field.null,
242
248
  "is_editable": field.editable,
243
249
  "default": field.default,
@@ -255,6 +261,7 @@ class DBBasedInterface(InterfaceBase):
255
261
  elif related_model is not None:
256
262
  fields[field_name] = {
257
263
  "type": related_model,
264
+ "is_derived": False,
258
265
  "is_required": not field.null,
259
266
  "is_editable": field.editable,
260
267
  "default": field.default,
@@ -280,6 +287,7 @@ class DBBasedInterface(InterfaceBase):
280
287
  fields[f"{field_name}_list"] = {
281
288
  "type": related_model,
282
289
  "is_required": False,
290
+ "is_derived": not bool(field.many_to_many),
283
291
  "is_editable": bool(field.many_to_many and field.editable),
284
292
  "default": None,
285
293
  }
@@ -453,9 +461,9 @@ class DBBasedInterface(InterfaceBase):
453
461
  ) -> tuple[attributes, interfaceBaseClass, relatedClass]:
454
462
  # Felder aus der Interface-Klasse sammeln
455
463
  """
456
- Dynamically creates a Django model class, its associated interface class, and a factory class based on the provided interface definition.
464
+ Dynamically creates a Django model class, its associated interface class, and a factory class from an interface definition.
457
465
 
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.
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.
459
467
 
460
468
  Parameters:
461
469
  name: The name for the dynamically created model class.
@@ -464,7 +472,7 @@ class DBBasedInterface(InterfaceBase):
464
472
  base_model_class: The base class to use for the new model (defaults to GeneralManagerModel).
465
473
 
466
474
  Returns:
467
- 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.
468
476
  """
469
477
  model_fields: dict[str, Any] = {}
470
478
  meta_class = None
@@ -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.
@@ -24,17 +24,16 @@ if TYPE_CHECKING:
24
24
  logger = logging.getLogger(__name__)
25
25
 
26
26
 
27
- class ReadOnlyInterface(DBBasedInterface):
27
+ class ReadOnlyInterface(DBBasedInterface[GeneralManagerBasisModel]):
28
28
  _interface_type = "readonly"
29
- _model: Type[GeneralManagerBasisModel]
30
29
  _parent_class: Type[GeneralManager]
31
30
 
32
31
  @staticmethod
33
32
  def getUniqueFields(model: Type[models.Model]) -> set[str]:
34
33
  """
35
- Return the set of field names that uniquely identify instances of the given Django model.
34
+ Return a set of field names that uniquely identify instances of the specified Django model.
36
35
 
37
- Considers fields marked as unique (excluding "id"), unique_together constraints, and UniqueConstraint definitions.
36
+ Considers fields marked as unique (excluding "id"), as well as fields defined in `unique_together` and `UniqueConstraint` constraints.
38
37
  """
39
38
  opts = model._meta
40
39
  unique_fields: set[str] = set()
@@ -57,9 +56,9 @@ class ReadOnlyInterface(DBBasedInterface):
57
56
  @classmethod
58
57
  def syncData(cls) -> None:
59
58
  """
60
- Synchronizes the associated Django model with the JSON data from the parent class, ensuring records match exactly.
59
+ Synchronizes the associated Django model with JSON data from the parent class, ensuring the database records match the provided data exactly.
61
60
 
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.
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.
63
62
  """
64
63
  if cls.ensureSchemaIsUpToDate(cls._parent_class, cls._model):
65
64
  logger.warning(
@@ -138,22 +137,18 @@ class ReadOnlyInterface(DBBasedInterface):
138
137
  new_manager_class: Type[GeneralManager], model: Type[models.Model]
139
138
  ) -> list[Warning]:
140
139
  """
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.
140
+ Check if the database schema for a Django model matches its model definition.
146
141
 
147
142
  Returns:
148
- list[Warning]: A list of Django Warning objects describing schema issues, or an empty list if the schema is up to date.
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.
149
144
  """
150
145
 
151
146
  def table_exists(table_name: str) -> bool:
152
147
  """
153
- Check if a database table with the given name exists.
148
+ Determine whether a database table with the specified name exists.
154
149
 
155
150
  Parameters:
156
- table_name (str): The name of the database table to check.
151
+ table_name (str): Name of the database table to check.
157
152
 
158
153
  Returns:
159
154
  bool: True if the table exists, False otherwise.
@@ -166,11 +161,12 @@ class ReadOnlyInterface(DBBasedInterface):
166
161
  model: Type[models.Model], table: str
167
162
  ) -> tuple[list[str], list[str]]:
168
163
  """
169
- Compare a Django model's fields to the columns of a database table.
164
+ Compares the fields of a Django model to the columns of a specified database table.
170
165
 
171
166
  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.
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.
174
170
  """
175
171
  with connection.cursor() as cursor:
176
172
  desc = connection.introspection.get_table_description(cursor, table)
@@ -208,7 +204,7 @@ class ReadOnlyInterface(DBBasedInterface):
208
204
  """
209
205
  Decorator for post-creation hooks that registers a new manager class as read-only.
210
206
 
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.
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.
212
208
  """
213
209
 
214
210
  def wrapper(
@@ -217,12 +213,9 @@ class ReadOnlyInterface(DBBasedInterface):
217
213
  model: Type[GeneralManagerBasisModel],
218
214
  ):
219
215
  """
220
- Registers the newly created class as a read-only class after invoking the wrapped post-creation function.
216
+ Registers a newly created manager class as read-only after executing the wrapped post-creation function.
221
217
 
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.
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.
226
219
  """
227
220
  from general_manager.manager.meta import GeneralManagerMeta
228
221
 
@@ -234,16 +227,28 @@ class ReadOnlyInterface(DBBasedInterface):
234
227
  @staticmethod
235
228
  def readOnlyPreCreate(func: Callable[..., Any]) -> Callable[..., Any]:
236
229
  """
237
- Decorator for pre-creation hook functions to ensure the base model class is set to ReadOnlyModel.
230
+ Decorator for pre-creation hook functions that ensures the base model class is set to `GeneralManagerBasisModel`.
238
231
 
239
- Wraps a pre-creation function, injecting ReadOnlyModel as the base model class argument before the GeneralManager instance is created.
232
+ Wraps a pre-creation function, injecting `GeneralManagerBasisModel` as the `base_model_class` argument before the manager class is created.
240
233
  """
234
+
241
235
  def wrapper(
242
236
  name: generalManagerClassName,
243
237
  attrs: attributes,
244
238
  interface: interfaceBaseClass,
245
239
  base_model_class=GeneralManagerBasisModel,
246
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
+ """
247
252
  return func(
248
253
  name, attrs, interface, base_model_class=GeneralManagerBasisModel
249
254
  )
@@ -253,12 +258,14 @@ class ReadOnlyInterface(DBBasedInterface):
253
258
  @classmethod
254
259
  def handleInterface(cls) -> tuple[classPreCreationMethod, classPostCreationMethod]:
255
260
  """
256
- Return the pre- and post-creation hook methods for integrating this interface with a GeneralManager.
261
+ Return the pre- and post-creation hook methods for integrating the interface with a manager meta-class system.
257
262
 
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.
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.
259
266
 
260
267
  Returns:
261
- tuple: A pair of methods for pre- and post-creation processing.
268
+ tuple: The pre-creation and post-creation hook methods for manager class lifecycle integration.
262
269
  """
263
270
  return cls.readOnlyPreCreate(cls._preCreate), cls.readOnlyPostCreate(
264
271
  cls._postCreate
@@ -13,13 +13,21 @@ if TYPE_CHECKING:
13
13
  InterfaceBase,
14
14
  )
15
15
  GeneralManagerType = TypeVar("GeneralManagerType", bound="GeneralManager")
16
+ InterfaceType = TypeVar("InterfaceType", bound="InterfaceBase", covariant=True)
16
17
 
17
18
 
18
- class GeneralManager(Generic[GeneralManagerType], metaclass=GeneralManagerMeta):
19
- Interface: Type[InterfaceBase]
19
+ class GeneralManager(
20
+ Generic[GeneralManagerType, InterfaceType], metaclass=GeneralManagerMeta
21
+ ):
22
+ Interface: Type[InterfaceType]
20
23
  _attributes: dict[str, Any]
21
24
 
22
25
  def __init__(self, *args: Any, **kwargs: Any):
26
+ """
27
+ Initialize the manager by creating an interface instance with the provided arguments and storing its identification.
28
+
29
+ The identification is registered with the dependency tracker for tracking purposes.
30
+ """
23
31
  self._interface = self.Interface(*args, **kwargs)
24
32
  self.__id: dict[str, Any] = self._interface.identification
25
33
  DependencyTracker.track(
@@ -33,11 +41,26 @@ class GeneralManager(Generic[GeneralManagerType], metaclass=GeneralManagerMeta):
33
41
  return f"{self.__class__.__name__}(**{self.__id})"
34
42
 
35
43
  def __reduce__(self) -> str | tuple[Any, ...]:
44
+ """
45
+ Support object serialization by returning a tuple containing the class and identification values for pickling.
46
+ """
36
47
  return (self.__class__, tuple(self.__id.values()))
37
48
 
38
49
  def __or__(
39
- self, other: GeneralManager[GeneralManagerType] | Bucket[GeneralManagerType]
50
+ self,
51
+ other: (
52
+ GeneralManager[GeneralManagerType, InterfaceType]
53
+ | Bucket[GeneralManagerType]
54
+ ),
40
55
  ) -> Bucket[GeneralManagerType]:
56
+ """
57
+ Combine this manager with another manager of the same class or a Bucket using the union operator.
58
+
59
+ If combined with a Bucket, returns the union of the Bucket and this manager. If combined with another manager of the same class, returns a Bucket containing both instances. Raises a TypeError for unsupported types.
60
+
61
+ Returns:
62
+ Bucket[GeneralManagerType]: A Bucket containing the union of the involved managers.
63
+ """
41
64
  if isinstance(other, Bucket):
42
65
  return other | self
43
66
  elif isinstance(other, GeneralManager) and other.__class__ == self.__class__:
@@ -63,11 +86,24 @@ class GeneralManager(Generic[GeneralManagerType], metaclass=GeneralManagerMeta):
63
86
  @dataChange
64
87
  def create(
65
88
  cls,
66
- creator_id: int,
89
+ creator_id: int | None = None,
67
90
  history_comment: str | None = None,
68
91
  ignore_permission: bool = False,
69
92
  **kwargs: dict[str, Any],
70
- ) -> GeneralManager[GeneralManagerType]:
93
+ ) -> GeneralManager[GeneralManagerType, InterfaceType]:
94
+ """
95
+ Creates a new managed object using the underlying interface and returns a corresponding manager instance.
96
+
97
+ Performs a permission check if a `Permission` class is defined and permission checks are not ignored. Passes all provided arguments to the interface's `create` method.
98
+
99
+ Parameters:
100
+ creator_id (int | None): Optional identifier for the creator of the object.
101
+ history_comment (str | None): Optional comment for audit or history tracking.
102
+ ignore_permission (bool): If True, skips the permission check.
103
+
104
+ Returns:
105
+ GeneralManager[GeneralManagerType, InterfaceType]: A new manager instance for the created object.
106
+ """
71
107
  Permission: Type[BasePermission] | None = getattr(cls, "Permission", None)
72
108
  if Permission is not None and not ignore_permission:
73
109
  Permission.checkCreatePermission(kwargs, cls, creator_id)
@@ -79,11 +115,23 @@ class GeneralManager(Generic[GeneralManagerType], metaclass=GeneralManagerMeta):
79
115
  @dataChange
80
116
  def update(
81
117
  self,
82
- creator_id: int,
118
+ creator_id: int | None = None,
83
119
  history_comment: str | None = None,
84
120
  ignore_permission: bool = False,
85
121
  **kwargs: dict[str, Any],
86
- ) -> GeneralManager[GeneralManagerType]:
122
+ ) -> GeneralManager[GeneralManagerType, InterfaceType]:
123
+ """
124
+ Update the underlying interface object with new data and return a new manager instance.
125
+
126
+ Parameters:
127
+ creator_id (int | None): Optional identifier for the user performing the update.
128
+ history_comment (str | None): Optional comment describing the update.
129
+ ignore_permission (bool): If True, skips permission checks.
130
+ **kwargs: Additional fields to update on the interface object.
131
+
132
+ Returns:
133
+ GeneralManager[GeneralManagerType, InterfaceType]: A new manager instance reflecting the updated object.
134
+ """
87
135
  Permission: Type[BasePermission] | None = getattr(self, "Permission", None)
88
136
  if Permission is not None and not ignore_permission:
89
137
  Permission.checkUpdatePermission(kwargs, self, creator_id)
@@ -97,10 +145,21 @@ class GeneralManager(Generic[GeneralManagerType], metaclass=GeneralManagerMeta):
97
145
  @dataChange
98
146
  def deactivate(
99
147
  self,
100
- creator_id: int,
148
+ creator_id: int | None = None,
101
149
  history_comment: str | None = None,
102
150
  ignore_permission: bool = False,
103
- ) -> GeneralManager[GeneralManagerType]:
151
+ ) -> GeneralManager[GeneralManagerType, InterfaceType]:
152
+ """
153
+ Deactivates the underlying interface object and returns a new manager instance.
154
+
155
+ Parameters:
156
+ creator_id (int | None): Optional identifier for the user performing the deactivation.
157
+ history_comment (str | None): Optional comment describing the reason for deactivation.
158
+ ignore_permission (bool): If True, skips permission checks.
159
+
160
+ Returns:
161
+ GeneralManager[GeneralManagerType, InterfaceType]: A new instance representing the deactivated object.
162
+ """
104
163
  Permission: Type[BasePermission] | None = getattr(self, "Permission", None)
105
164
  if Permission is not None and not ignore_permission:
106
165
  Permission.checkDeletePermission(self, creator_id)
@@ -130,15 +189,15 @@ class GeneralManager(Generic[GeneralManagerType], metaclass=GeneralManagerMeta):
130
189
  @staticmethod
131
190
  def __parse_identification(kwargs: dict[str, Any]) -> dict[str, Any] | None:
132
191
  """
133
- Processes a dictionary by replacing GeneralManager instances with their identifications.
192
+ Return a dictionary with all GeneralManager instances in the input replaced by their identification dictionaries.
134
193
 
135
- For each key-value pair, replaces any GeneralManager instance with its identification. Lists and tuples are processed recursively, substituting contained GeneralManager instances with their identifications. Returns None if the resulting dictionary is empty.
194
+ For each key-value pair in the input, any GeneralManager instance is replaced by its identification. Lists and tuples are processed recursively, substituting contained GeneralManager instances with their identifications. Returns None if the resulting dictionary is empty.
136
195
 
137
- Args:
138
- kwargs: Dictionary to process.
196
+ Parameters:
197
+ kwargs (dict[str, Any]): Dictionary to process.
139
198
 
140
199
  Returns:
141
- A new dictionary with GeneralManager instances replaced by their identifications, or None if empty.
200
+ dict[str, Any] | None: Processed dictionary with identifications, or None if empty.
142
201
  """
143
202
  output = {}
144
203
  for key, value in kwargs.items():
@@ -18,12 +18,12 @@ class Input(Generic[INPUT_TYPE]):
18
18
  depends_on: Optional[List[str]] = None,
19
19
  ):
20
20
  """
21
- Initializes an Input instance with type information, possible values, and dependencies.
21
+ Create an Input specification with type information, allowed values, and dependency metadata.
22
22
 
23
- Args:
24
- type: The expected type for the input value.
25
- possible_values: An optional iterable or callable that defines allowed values.
26
- depends_on: An optional list of dependency names. If not provided and possible_values is callable, dependencies are inferred from its parameters.
23
+ Parameters:
24
+ type: The expected Python type for the input value.
25
+ possible_values: Optional; an iterable of allowed values or a callable returning allowed values.
26
+ depends_on: Optional; a list of dependency names. If not provided and possible_values is callable, dependencies are inferred from the callable's parameter names.
27
27
  """
28
28
  self.type = type
29
29
  self.possible_values = possible_values
@@ -42,14 +42,9 @@ class Input(Generic[INPUT_TYPE]):
42
42
 
43
43
  def cast(self, value: Any) -> Any:
44
44
  """
45
- Casts the input value to the type specified by this Input instance.
45
+ Converts the input value to the type specified by this Input instance, handling special cases for dates, datetimes, GeneralManager subclasses, and Measurement types.
46
46
 
47
- Handles special cases for date, datetime, GeneralManager subclasses, and Measurement types.
48
- If the value is already of the target type, it is returned unchanged. Otherwise, attempts to
49
- convert or construct the value as appropriate for the target type.
50
-
51
- Args:
52
- value: The value to be cast or converted.
47
+ If the value is already of the target type, it is returned unchanged. For date and datetime types, string and cross-type conversions are supported. For GeneralManager subclasses, instances are constructed from a dictionary or an ID. For Measurement, string values are parsed accordingly. Otherwise, the value is cast using the target type's constructor.
53
48
 
54
49
  Returns:
55
50
  The value converted to the target type, or an instance of the target type.
@@ -57,6 +52,8 @@ class Input(Generic[INPUT_TYPE]):
57
52
  if self.type == date:
58
53
  if isinstance(value, datetime) and type(value) is not date:
59
54
  return value.date()
55
+ elif isinstance(value, date):
56
+ return value
60
57
  return date.fromisoformat(value)
61
58
  if self.type == datetime:
62
59
  if isinstance(value, date):
@@ -18,29 +18,30 @@ class _nonExistent:
18
18
 
19
19
  class GeneralManagerMeta(type):
20
20
  all_classes: list[Type[GeneralManager]] = []
21
- read_only_classes: list[Type[GeneralManager]] = []
21
+ read_only_classes: list[Type[GeneralManager[Any, ReadOnlyInterface]]] = []
22
22
  pending_graphql_interfaces: list[Type[GeneralManager]] = []
23
23
  pending_attribute_initialization: list[Type[GeneralManager]] = []
24
24
  Interface: type[InterfaceBase]
25
25
 
26
26
  def __new__(mcs, name: str, bases: tuple[type, ...], attrs: dict[str, Any]) -> type:
27
-
28
27
  """
29
- Creates a new class, handling interface integration and registration for the general manager framework.
30
-
31
- If an 'Interface' attribute is present in the class definition, validates and processes it using the interface's pre- and post-creation hooks, then registers the resulting class for attribute initialization and tracking. If the 'AUTOCREATE_GRAPHQL' setting is enabled, also registers the class for pending GraphQL interface creation.
28
+ Create a new class using the metaclass, integrating interface hooks and registering the class for attribute initialization and tracking.
32
29
 
33
- Args:
34
- name: The name of the class being created.
35
- bases: Base classes for the new class.
36
- attrs: Attribute dictionary for the new class.
30
+ If the class definition includes an 'Interface' attribute, validates it as a subclass of `InterfaceBase`, applies pre- and post-creation hooks from the interface, and registers the resulting class for attribute initialization and management. If the `AUTOCREATE_GRAPHQL` setting is enabled, also registers the class for pending GraphQL interface creation.
37
31
 
38
32
  Returns:
39
- The newly created class, possibly augmented with interface and registration logic.
33
+ The newly created class, potentially augmented with interface integration and registration logic.
40
34
  """
35
+
41
36
  def createNewGeneralManagerClass(
42
37
  mcs, name: str, bases: tuple[type, ...], attrs: dict[str, Any]
43
38
  ) -> Type[GeneralManager]:
39
+ """
40
+ Create a new general manager class using the standard metaclass instantiation process.
41
+
42
+ Returns:
43
+ The newly created general manager class.
44
+ """
44
45
  return super().__new__(mcs, name, bases, attrs)
45
46
 
46
47
  if "Interface" in attrs:
@@ -69,6 +70,11 @@ class GeneralManagerMeta(type):
69
70
  attributes: Iterable[str], new_class: Type[GeneralManager]
70
71
  ):
71
72
 
73
+ """
74
+ Dynamically assigns property descriptors to a class for the specified attribute names.
75
+
76
+ For each attribute name, creates a descriptor that retrieves the value from an instance's `_attributes` dictionary. If accessed on the class, returns the field type from the class's interface. If the attribute is callable, it is invoked with the instance's interface. Raises `AttributeError` if the attribute is missing or if an error occurs during callable invocation.
77
+ """
72
78
  def desciptorMethod(attr_name: str, new_class: type):
73
79
  class Descriptor(Generic[GeneralManagerType]):
74
80
  def __init__(self, attr_name: str, new_class: Type[GeneralManager]):
@@ -77,7 +83,7 @@ class GeneralManagerMeta(type):
77
83
 
78
84
  def __get__(
79
85
  self,
80
- instance: GeneralManager[GeneralManagerType] | None,
86
+ instance: GeneralManager[GeneralManagerType, InterfaceBase] | None,
81
87
  owner: type | None = None,
82
88
  ):
83
89
  if instance is None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GeneralManager
3
- Version: 0.7.0
3
+ Version: 0.8.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,11 +1,12 @@
1
1
  general_manager/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- general_manager/apps.py,sha256=sfH8a1iaAdTjTmmf7R9TZfZl_8yrce7zlsKGB1mFSQY,7935
3
- general_manager/api/graphql.py,sha256=a_mH5dvM3yXC1WyQKFCYpzSE00DQFLNKhRsZrwvLrNM,27453
4
- general_manager/api/mutation.py,sha256=uu5RVxc9wbb-Zrbtt4azegvyKymMqEsxk_CkerKCd9Q,5178
2
+ general_manager/apps.py,sha256=P_SmEPHto0Wl7cgP6l2MAOQJgXjuGBLzRIidpfHsdc0,8316
3
+ general_manager/api/graphql.py,sha256=ZMzlpgmyn2X9Zwxo12r-b5UZ2R-u1pXys_nhNczxyX8,31622
4
+ general_manager/api/mutation.py,sha256=RYCogAdUpUedyh2B9keMAzq9u-iIhEKAsoiw5xXmhrQ,5669
5
5
  general_manager/api/property.py,sha256=oc93p1P8dcIvrNorRuqD1EJVsd6eYttYhZuAS0s28gs,696
6
6
  general_manager/auxiliary/__init__.py,sha256=4IwKJzsNxGduF-Ej0u1BNHVaMhkql8PjHbVtx9DOTSY,76
7
7
  general_manager/auxiliary/argsToKwargs.py,sha256=kmp1xonpQp4X_y8ZJG6c5uOW7zQwo0HtPqsHWVzXRSM,921
8
8
  general_manager/auxiliary/filterParser.py,sha256=wmR4YzsnYgjI2Co5eyvCFROldotAraHx_GiCDJo79IY,5410
9
+ general_manager/auxiliary/formatString.py,sha256=gZSbsKvpywBmf5bIx6YW43LmNJcqsCP7ZfrB7YPvaFo,1436
9
10
  general_manager/auxiliary/jsonEncoder.py,sha256=TDsgFQvheITHZgdmn-m8tk1_QCzpT0XwEHo7bY3Qe-M,638
10
11
  general_manager/auxiliary/makeCacheKey.py,sha256=lczutqxlofLSUnheKRi8nazhOyPa04TZOFNxNn5vDu4,1126
11
12
  general_manager/auxiliary/noneToZero.py,sha256=KfQtMQnrT6vsYST0K7lv6pVujkDcK3XL8czHYOhgqKQ,539
@@ -24,16 +25,16 @@ general_manager/factory/autoFactory.py,sha256=WBhSuMVsxkPAPLhlZhYXwHVIqiomUveS7v
24
25
  general_manager/factory/factories.py,sha256=F6_nYFyJRYYc3LQApfoVFdctfLzsWUDHKafn6xjckB8,7224
25
26
  general_manager/factory/factoryMethods.py,sha256=9Bag891j0XHe3dUBAFi7gUKcKeUwcBZN3cDLBobyBiI,3225
26
27
  general_manager/interface/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
- general_manager/interface/baseInterface.py,sha256=WjavtLW_vzyv3PDIIGpw0cydFXjIBJfzxflMXDNF7ac,8583
28
+ general_manager/interface/baseInterface.py,sha256=GCMo0MGlaRAElovfI34qfuWuVYOyTQLG0OA-ZJx8i3s,8604
28
29
  general_manager/interface/calculationInterface.py,sha256=Kg_OqLw67tcLwdzYNLq31eKVLzkM7taw-8Mzmk0CYi0,4232
29
- general_manager/interface/databaseBasedInterface.py,sha256=NoHJp1A9kWRa5R8aal3MS--4PRsHY4JMBzpcuNd69kY,22182
30
- general_manager/interface/databaseInterface.py,sha256=08dFgxoLQNa13RK2NQ4cDNbNPIG-X9ChLs3NvJcSp6Y,4923
31
- general_manager/interface/readOnlyInterface.py,sha256=USr4k6Jr1gz91Cdi8bbZY59cTy5MPGZ3oC40xCdWS88,10873
30
+ general_manager/interface/databaseBasedInterface.py,sha256=twdqUyvPci77WXqCX0k2dpFajhx38bbUfgxrV_BjcxQ,22788
31
+ general_manager/interface/databaseInterface.py,sha256=kYPuwjAVcVMbxozc-eg-LwGY1C0k-sqt73b7DSlVxWI,6389
32
+ general_manager/interface/readOnlyInterface.py,sha256=TkfbOeaa2wCq5kCv0a3IwJWcYOTVbtNsdNWmGAz0Mns,11217
32
33
  general_manager/manager/__init__.py,sha256=l3RYp62aEhj3Y975_XUTIzo35LUnkTJHkb_hgChnXXI,111
33
- general_manager/manager/generalManager.py,sha256=HX69KhrnSGVkuJwHY_jzff5gS0VD-6fRxKnd59A5Ct4,6100
34
+ general_manager/manager/generalManager.py,sha256=AVFZICHzqiIyn7lgPU_WLH8X8WLP1edvWAE5CljZPrk,9178
34
35
  general_manager/manager/groupManager.py,sha256=8dpZUfm7aFL4lraUWv4qbbDRClQZaYxw4prclhBZYZs,4367
35
- general_manager/manager/input.py,sha256=iKawV3P1QICz-0AQUF00OvH7LZYxussg3svpvCUl8hE,2977
36
- general_manager/manager/meta.py,sha256=I4HO7Tp2P0-eRKicVzZRFrYzyMshjWY2ZGP8no9Su-Y,4411
36
+ general_manager/manager/input.py,sha256=-pJXGJ-g2-OxZfl4Buj3mQkf05fN4p8MsR2Lh9BQcEo,3208
37
+ general_manager/manager/meta.py,sha256=gtzFKBMCvyAx5qo2BVuTBqHZzngR-ivu8YLY7oiSUEk,5059
37
38
  general_manager/measurement/__init__.py,sha256=X97meFujBldE5v0WMF7SmKeGpC5R0JTczfLo_Lq1Xek,84
38
39
  general_manager/measurement/measurement.py,sha256=e_FjHieeJbBtjXGCO9J7vRPw6KCkMrOxwWjaD0m8ee4,11777
39
40
  general_manager/measurement/measurementField.py,sha256=iq9Hqe6ZGX8CxXm4nIqTAWTRkQVptzpqE9ExX-jFyNs,5928
@@ -46,8 +47,8 @@ general_manager/permission/permissionDataManager.py,sha256=Ji7fsnuaKTa6M8yzCGyzr
46
47
  general_manager/rule/__init__.py,sha256=4Har5cfPD1fmOsilTDod-ZUz3Com-tkl58jz7yY4fD0,23
47
48
  general_manager/rule/handler.py,sha256=z8SFHTIZ0LbLh3fV56Mud0V4_OvWkqJjlHvFqau7Qfk,7334
48
49
  general_manager/rule/rule.py,sha256=3FVCKGL7BTVoStdgOTdWQwuoVRIxAIAilV4VOzouDpc,10759
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,,
50
+ generalmanager-0.8.0.dist-info/licenses/LICENSE,sha256=YGFm0ieb4KpkMRRt2qnWue6uFh0cUMtobwEBkHwajhc,1450
51
+ generalmanager-0.8.0.dist-info/METADATA,sha256=awQ6aVm9myhb0KpGFMC_2Pc_McXqbMzECdrwxeORSkM,6205
52
+ generalmanager-0.8.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
53
+ generalmanager-0.8.0.dist-info/top_level.txt,sha256=sTDtExP9ga-YP3h3h42yivUY-A2Q23C2nw6LNKOho4I,16
54
+ generalmanager-0.8.0.dist-info/RECORD,,