GeneralManager 0.6.2__py3-none-any.whl → 0.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- general_manager/api/graphql.py +117 -65
- general_manager/api/mutation.py +11 -10
- general_manager/apps.py +131 -31
- general_manager/auxiliary/formatString.py +60 -0
- general_manager/cache/dependencyIndex.py +10 -2
- general_manager/interface/baseInterface.py +9 -9
- general_manager/interface/databaseBasedInterface.py +55 -32
- general_manager/interface/databaseInterface.py +43 -13
- general_manager/interface/readOnlyInterface.py +202 -37
- general_manager/manager/generalManager.py +73 -14
- general_manager/manager/input.py +9 -12
- general_manager/manager/meta.py +17 -11
- {generalmanager-0.6.2.dist-info → generalmanager-0.8.0.dist-info}/METADATA +1 -1
- {generalmanager-0.6.2.dist-info → generalmanager-0.8.0.dist-info}/RECORD +17 -16
- {generalmanager-0.6.2.dist-info → generalmanager-0.8.0.dist-info}/WHEEL +0 -0
- {generalmanager-0.6.2.dist-info → generalmanager-0.8.0.dist-info}/licenses/LICENSE +0 -0
- {generalmanager-0.6.2.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,5 +1,7 @@
|
|
1
|
+
from __future__ import annotations
|
1
2
|
from django.apps import AppConfig
|
2
3
|
import graphene
|
4
|
+
import os
|
3
5
|
from django.conf import settings
|
4
6
|
from django.urls import path
|
5
7
|
from graphene_django.views import GraphQLView
|
@@ -9,6 +11,16 @@ from general_manager.manager.meta import GeneralManagerMeta
|
|
9
11
|
from general_manager.manager.input import Input
|
10
12
|
from general_manager.api.property import graphQlProperty
|
11
13
|
from general_manager.api.graphql import GraphQL
|
14
|
+
from typing import TYPE_CHECKING, Type, Any, cast
|
15
|
+
from django.core.checks import register
|
16
|
+
import logging
|
17
|
+
from django.core.management.base import BaseCommand
|
18
|
+
|
19
|
+
|
20
|
+
if TYPE_CHECKING:
|
21
|
+
from general_manager.interface.readOnlyInterface import ReadOnlyInterface
|
22
|
+
|
23
|
+
logger = logging.getLogger(__name__)
|
12
24
|
|
13
25
|
|
14
26
|
class GeneralmanagerConfig(AppConfig):
|
@@ -16,7 +28,97 @@ class GeneralmanagerConfig(AppConfig):
|
|
16
28
|
name = "general_manager"
|
17
29
|
|
18
30
|
def ready(self):
|
31
|
+
"""
|
32
|
+
Initializes the general_manager app on Django startup.
|
33
|
+
|
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.
|
35
|
+
"""
|
36
|
+
self.handleReadOnlyInterface()
|
37
|
+
self.initializeGeneralManagerClasses()
|
38
|
+
if getattr(settings, "AUTOCREATE_GRAPHQL", False):
|
39
|
+
self.handleGraphQL()
|
40
|
+
|
41
|
+
def handleReadOnlyInterface(self):
|
42
|
+
"""
|
43
|
+
Configures synchronization and schema validation for all registered read-only interfaces.
|
44
|
+
|
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.
|
46
|
+
"""
|
47
|
+
self.patchReadOnlyInterfaceSync(GeneralManagerMeta.read_only_classes)
|
48
|
+
from general_manager.interface.readOnlyInterface import ReadOnlyInterface
|
49
|
+
|
50
|
+
logger.debug("starting to register ReadOnlyInterface schema warnings...")
|
51
|
+
for general_manager_class in GeneralManagerMeta.read_only_classes:
|
52
|
+
read_only_interface = cast(
|
53
|
+
Type[ReadOnlyInterface], general_manager_class.Interface
|
54
|
+
)
|
55
|
+
|
56
|
+
register(
|
57
|
+
lambda app_configs, model=read_only_interface._model, manager_class=general_manager_class, **kwargs: ReadOnlyInterface.ensureSchemaIsUpToDate(
|
58
|
+
manager_class, model
|
59
|
+
),
|
60
|
+
"general_manager",
|
61
|
+
)
|
62
|
+
|
63
|
+
@staticmethod
|
64
|
+
def patchReadOnlyInterfaceSync(
|
65
|
+
general_manager_classes: list[Type[GeneralManager[Any, ReadOnlyInterface]]],
|
66
|
+
):
|
67
|
+
"""
|
68
|
+
Monkey-patches Django's management command runner to synchronize data for all provided read-only interfaces before executing management commands.
|
69
|
+
|
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`.
|
71
|
+
"""
|
72
|
+
from general_manager.interface.readOnlyInterface import ReadOnlyInterface
|
73
|
+
|
74
|
+
original_run_from_argv = BaseCommand.run_from_argv
|
19
75
|
|
76
|
+
def run_from_argv_with_sync(self, argv):
|
77
|
+
# Ensure syncData is only called at real run of runserver
|
78
|
+
"""
|
79
|
+
Executes a Django management command, synchronizing all registered read-only interfaces before execution unless running an autoreload subprocess of 'runserver'.
|
80
|
+
|
81
|
+
Parameters:
|
82
|
+
argv (list): Command-line arguments for the management command.
|
83
|
+
|
84
|
+
Returns:
|
85
|
+
The result of the original management command execution.
|
86
|
+
"""
|
87
|
+
run_main = os.environ.get("RUN_MAIN") == "true"
|
88
|
+
command = argv[1] if len(argv) > 1 else None
|
89
|
+
if command != "runserver" or run_main:
|
90
|
+
logger.debug("start syncing ReadOnlyInterface data...")
|
91
|
+
for general_manager_class in general_manager_classes:
|
92
|
+
read_only_interface = cast(
|
93
|
+
Type[ReadOnlyInterface], general_manager_class.Interface
|
94
|
+
)
|
95
|
+
read_only_interface.syncData()
|
96
|
+
|
97
|
+
logger.debug("finished syncing ReadOnlyInterface data.")
|
98
|
+
|
99
|
+
return original_run_from_argv(self, argv)
|
100
|
+
|
101
|
+
BaseCommand.run_from_argv = run_from_argv_with_sync
|
102
|
+
|
103
|
+
def initializeGeneralManagerClasses(self):
|
104
|
+
"""
|
105
|
+
Initializes attributes and sets up dynamic relationships for all registered GeneralManager classes.
|
106
|
+
|
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.
|
108
|
+
"""
|
109
|
+
logger.debug("Initializing GeneralManager classes...")
|
110
|
+
|
111
|
+
logger.debug("starting to create attributes for GeneralManager classes...")
|
112
|
+
for (
|
113
|
+
general_manager_class
|
114
|
+
) in GeneralManagerMeta.pending_attribute_initialization:
|
115
|
+
attributes = general_manager_class.Interface.getAttributes()
|
116
|
+
setattr(general_manager_class, "_attributes", attributes)
|
117
|
+
GeneralManagerMeta.createAtPropertiesForAttributes(
|
118
|
+
attributes.keys(), general_manager_class
|
119
|
+
)
|
120
|
+
|
121
|
+
logger.debug("starting to connect inputs to other general manager classes...")
|
20
122
|
for general_manager_class in GeneralManagerMeta.all_classes:
|
21
123
|
attributes = getattr(general_manager_class.Interface, "input_fields", {})
|
22
124
|
for attribute_name, attribute in attributes.items():
|
@@ -35,41 +137,39 @@ class GeneralmanagerConfig(AppConfig):
|
|
35
137
|
graphQlProperty(func),
|
36
138
|
)
|
37
139
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
)
|
46
|
-
|
47
|
-
if getattr(settings, "AUTOCREATE_GRAPHQL", False):
|
48
|
-
|
49
|
-
for general_manager_class in GeneralManagerMeta.pending_graphql_interfaces:
|
50
|
-
GraphQL.createGraphqlInterface(general_manager_class)
|
51
|
-
GraphQL.createGraphqlMutation(general_manager_class)
|
140
|
+
def handleGraphQL(self):
|
141
|
+
"""
|
142
|
+
Sets up GraphQL interfaces, mutations, and schema for all pending general manager classes, and adds the GraphQL endpoint to the Django URL configuration.
|
143
|
+
"""
|
144
|
+
logger.debug("Starting to create GraphQL interfaces and mutations...")
|
145
|
+
for general_manager_class in GeneralManagerMeta.pending_graphql_interfaces:
|
146
|
+
GraphQL.createGraphqlInterface(general_manager_class)
|
147
|
+
GraphQL.createGraphqlMutation(general_manager_class)
|
52
148
|
|
53
|
-
|
54
|
-
|
149
|
+
query_class = type("Query", (graphene.ObjectType,), GraphQL._query_fields)
|
150
|
+
GraphQL._query_class = query_class
|
55
151
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
},
|
63
|
-
)
|
64
|
-
GraphQL._mutation_class = mutation_class
|
152
|
+
mutation_class = type(
|
153
|
+
"Mutation",
|
154
|
+
(graphene.ObjectType,),
|
155
|
+
{name: mutation.Field() for name, mutation in GraphQL._mutations.items()},
|
156
|
+
)
|
157
|
+
GraphQL._mutation_class = mutation_class
|
65
158
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
159
|
+
schema = graphene.Schema(
|
160
|
+
query=GraphQL._query_class,
|
161
|
+
mutation=GraphQL._mutation_class,
|
162
|
+
)
|
163
|
+
self.addGraphqlUrl(schema)
|
71
164
|
|
72
|
-
def
|
165
|
+
def addGraphqlUrl(self, schema):
|
166
|
+
"""
|
167
|
+
Dynamically appends a GraphQL endpoint to the Django URL configuration using the given schema.
|
168
|
+
|
169
|
+
Raises:
|
170
|
+
Exception: If the ROOT_URLCONF setting is not defined in Django settings.
|
171
|
+
"""
|
172
|
+
logging.debug("Adding GraphQL URL to Django settings...")
|
73
173
|
root_url_conf_path = getattr(settings, "ROOT_URLCONF", None)
|
74
174
|
graph_ql_url = getattr(settings, "GRAPHQL_URL", "graphql/")
|
75
175
|
if not root_url_conf_path:
|
@@ -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)
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
2
2
|
import time
|
3
3
|
import ast
|
4
4
|
import re
|
5
|
+
import logging
|
5
6
|
|
6
7
|
from django.core.cache import cache
|
7
8
|
from general_manager.cache.signals import post_data_change, pre_data_change
|
@@ -27,6 +28,8 @@ type dependency_index = dict[
|
|
27
28
|
type filter_type = Literal["filter", "exclude", "identification"]
|
28
29
|
type Dependency = Tuple[general_manager_name, filter_type, str]
|
29
30
|
|
31
|
+
logger = logging.getLogger(__name__)
|
32
|
+
|
30
33
|
# -----------------------------------------------------------------------------
|
31
34
|
# CONFIG
|
32
35
|
# -----------------------------------------------------------------------------
|
@@ -181,6 +184,11 @@ def generic_cache_invalidation(
|
|
181
184
|
old_relevant_values: dict[str, Any],
|
182
185
|
**kwargs,
|
183
186
|
):
|
187
|
+
"""
|
188
|
+
Invalidates cached query results related to a model instance when its data changes.
|
189
|
+
|
190
|
+
This function is intended to be used as a Django signal handler. It compares old and new values of relevant fields on a model instance against registered cache dependencies (filters and excludes). If a change affects any cached queryset result, the corresponding cache keys are invalidated and removed from the dependency index.
|
191
|
+
"""
|
184
192
|
manager_name = sender.__name__
|
185
193
|
idx = get_full_index()
|
186
194
|
|
@@ -280,7 +288,7 @@ def generic_cache_invalidation(
|
|
280
288
|
if action == "filter":
|
281
289
|
# Filter: invalidate if new match or old match
|
282
290
|
if new_match or old_match:
|
283
|
-
|
291
|
+
logger.info(
|
284
292
|
f"Invalidate cache key {cache_keys} for filter {lookup} with value {val_key}"
|
285
293
|
)
|
286
294
|
for ck in list(cache_keys):
|
@@ -290,7 +298,7 @@ def generic_cache_invalidation(
|
|
290
298
|
else: # action == 'exclude'
|
291
299
|
# Excludes: invalidate only if matches changed
|
292
300
|
if old_match != new_match:
|
293
|
-
|
301
|
+
logger.info(
|
294
302
|
f"Invalidate cache key {cache_keys} for exclude {lookup} with value {val_key}"
|
295
303
|
)
|
296
304
|
for ck in list(cache_keys):
|