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.
- general_manager/api/graphql.py +117 -65
- general_manager/api/mutation.py +11 -10
- general_manager/apps.py +30 -17
- general_manager/auxiliary/formatString.py +60 -0
- general_manager/interface/baseInterface.py +1 -0
- general_manager/interface/databaseBasedInterface.py +31 -23
- general_manager/interface/databaseInterface.py +43 -13
- general_manager/interface/readOnlyInterface.py +35 -28
- general_manager/manager/generalManager.py +73 -14
- general_manager/manager/input.py +9 -12
- general_manager/manager/meta.py +17 -11
- {generalmanager-0.7.0.dist-info → generalmanager-0.8.0.dist-info}/METADATA +1 -1
- {generalmanager-0.7.0.dist-info → generalmanager-0.8.0.dist-info}/RECORD +16 -15
- {generalmanager-0.7.0.dist-info → generalmanager-0.8.0.dist-info}/WHEEL +0 -0
- {generalmanager-0.7.0.dist-info → generalmanager-0.8.0.dist-info}/licenses/LICENSE +0 -0
- {generalmanager-0.7.0.dist-info → generalmanager-0.8.0.dist-info}/top_level.txt +0 -0
general_manager/api/graphql.py
CHANGED
@@ -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):
|
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
|
-
|
106
|
-
|
107
|
-
|
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
|
-
#
|
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
|
-
#
|
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
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
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
|
-
#
|
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
|
-
|
445
|
-
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
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
|
-
|
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
|
-
) ->
|
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
|
573
|
-
|
574
|
-
|
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
|
-
|
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
|
-
) ->
|
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
|
635
|
-
|
636
|
-
|
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
|
-
|
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
|
-
) ->
|
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
|
697
|
-
|
698
|
-
|
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__}",
|
general_manager/api/mutation.py
CHANGED
@@ -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
|
21
|
-
|
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
|
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
|
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
|
-
|
43
|
+
Configures synchronization and schema validation for all registered read-only interfaces.
|
42
44
|
|
43
|
-
This method
|
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
|
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(
|
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
|
68
|
+
Monkey-patches Django's management command runner to synchronize data for all provided read-only interfaces before executing management commands.
|
63
69
|
|
64
|
-
|
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
|
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
|
-
|
79
|
+
Executes a Django management command, synchronizing all registered read-only interfaces before execution unless running an autoreload subprocess of 'runserver'.
|
74
80
|
|
75
|
-
|
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
|
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
|
105
|
+
Initializes attributes and sets up dynamic relationships for all registered GeneralManager classes.
|
94
106
|
|
95
|
-
For each pending GeneralManager class,
|
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
|
167
|
+
Dynamically appends a GraphQL endpoint to the Django URL configuration using the given schema.
|
156
168
|
|
157
|
-
Raises
|
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)
|
@@ -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(
|
80
|
-
|
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
|
-
|
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
|
-
|
104
|
-
|
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
|
-
|
115
|
-
|
116
|
-
If
|
115
|
+
Initialize the interface instance and load the associated model record.
|
116
|
+
|
117
|
+
If `search_date` is provided, retrieves the historical record as of that date; otherwise, loads the current record.
|
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
|
-
|
204
|
-
|
205
|
-
The returned dictionary includes all model fields, custom fields, foreign keys, many-to-many, and reverse relation fields.
|
204
|
+
Return a dictionary mapping attribute names to metadata describing their type and properties.
|
205
|
+
|
206
|
+
The returned dictionary includes all model fields, custom fields, foreign keys, many-to-many, and reverse relation fields. For each attribute, the metadata includes its Python type (translated from Django field types when possible), whether it is required, editable, derived, and its default value. For related models with a general manager class, the type is set to that class.
|
207
|
+
|
208
|
+
Returns:
|
209
|
+
dict[str, AttributeTypedDict]: Mapping of attribute names to their type information and metadata.
|
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
|
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(
|
60
|
+
def deactivate(
|
61
|
+
self, creator_id: int | None, history_comment: str | None = None
|
62
|
+
) -> int:
|
63
|
+
"""
|
64
|
+
Deactivate the current model instance by setting its `is_active` flag to `False` and recording the change with an optional history comment.
|
65
|
+
|
66
|
+
Parameters:
|
67
|
+
creator_id (int | None): The ID of the user performing the deactivation, or None if not specified.
|
68
|
+
history_comment (str | None): An optional comment to include in the instance's history log.
|
69
|
+
|
70
|
+
Returns:
|
71
|
+
int: The primary key of the deactivated instance.
|
72
|
+
"""
|
41
73
|
instance = self._model.objects.get(pk=self.pk)
|
42
74
|
instance.is_active = False
|
43
75
|
if history_comment:
|
@@ -89,7 +121,7 @@ class DatabaseInterface(DBBasedInterface):
|
|
89
121
|
for key in kwargs:
|
90
122
|
temp_key = key.split("_id_list")[0] # Remove '_id_list' suffix
|
91
123
|
if temp_key not in attributes and temp_key not in field_names:
|
92
|
-
raise ValueError(f"{key} does not
|
124
|
+
raise ValueError(f"{key} does not exist in {model.__name__}")
|
93
125
|
|
94
126
|
@staticmethod
|
95
127
|
def _sortKwargs(
|
@@ -106,17 +138,15 @@ class DatabaseInterface(DBBasedInterface):
|
|
106
138
|
@classmethod
|
107
139
|
@transaction.atomic
|
108
140
|
def _save_with_history(
|
109
|
-
cls,
|
141
|
+
cls,
|
142
|
+
instance: GeneralManagerModel,
|
143
|
+
creator_id: int | None,
|
144
|
+
history_comment: str | None,
|
110
145
|
) -> int:
|
111
146
|
"""
|
112
|
-
|
147
|
+
Atomically saves a model instance with validation and optional history comment.
|
113
148
|
|
114
|
-
Sets the `changed_by_id` field, validates the instance, applies a history comment if provided, and saves the instance within
|
115
|
-
|
116
|
-
Args:
|
117
|
-
instance: The model instance to save.
|
118
|
-
creator_id: The ID of the user making the change.
|
119
|
-
history_comment: Optional comment describing the reason for the change.
|
149
|
+
Sets the `changed_by_id` field, validates the instance, applies a history comment if provided, and saves the instance within a database transaction.
|
120
150
|
|
121
151
|
Returns:
|
122
152
|
The primary key of the saved instance.
|
@@ -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
|
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
|
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
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
148
|
+
Determine whether a database table with the specified name exists.
|
154
149
|
|
155
150
|
Parameters:
|
156
|
-
table_name (str):
|
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
|
-
|
164
|
+
Compares the fields of a Django model to the columns of a specified database table.
|
170
165
|
|
171
166
|
Returns:
|
172
|
-
|
173
|
-
|
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
|
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
|
216
|
+
Registers a newly created manager class as read-only after executing the wrapped post-creation function.
|
221
217
|
|
222
|
-
|
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
|
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
|
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
|
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
|
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:
|
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(
|
19
|
-
|
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,
|
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
|
-
|
192
|
+
Return a dictionary with all GeneralManager instances in the input replaced by their identification dictionaries.
|
134
193
|
|
135
|
-
For each key-value pair,
|
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
|
-
|
138
|
-
kwargs: Dictionary to process.
|
196
|
+
Parameters:
|
197
|
+
kwargs (dict[str, Any]): Dictionary to process.
|
139
198
|
|
140
199
|
Returns:
|
141
|
-
|
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():
|
general_manager/manager/input.py
CHANGED
@@ -18,12 +18,12 @@ class Input(Generic[INPUT_TYPE]):
|
|
18
18
|
depends_on: Optional[List[str]] = None,
|
19
19
|
):
|
20
20
|
"""
|
21
|
-
|
21
|
+
Create an Input specification with type information, allowed values, and dependency metadata.
|
22
22
|
|
23
|
-
|
24
|
-
type: The expected type for the input value.
|
25
|
-
possible_values:
|
26
|
-
depends_on:
|
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
|
-
|
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
|
-
|
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):
|
general_manager/manager/meta.py
CHANGED
@@ -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
|
-
|
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
|
-
|
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,
|
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.
|
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=
|
3
|
-
general_manager/api/graphql.py,sha256=
|
4
|
-
general_manager/api/mutation.py,sha256=
|
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=
|
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=
|
30
|
-
general_manager/interface/databaseInterface.py,sha256=
|
31
|
-
general_manager/interface/readOnlyInterface.py,sha256=
|
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=
|
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
|
36
|
-
general_manager/manager/meta.py,sha256=
|
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.
|
50
|
-
generalmanager-0.
|
51
|
-
generalmanager-0.
|
52
|
-
generalmanager-0.
|
53
|
-
generalmanager-0.
|
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,,
|
File without changes
|
File without changes
|
File without changes
|