GeneralManager 0.19.0__py3-none-any.whl → 0.19.2__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.
Potentially problematic release.
This version of GeneralManager might be problematic. Click here for more details.
- general_manager/_types/api.py +4 -4
- general_manager/_types/bucket.py +4 -4
- general_manager/_types/cache.py +6 -6
- general_manager/_types/factory.py +35 -35
- general_manager/_types/general_manager.py +9 -9
- general_manager/_types/interface.py +5 -5
- general_manager/_types/manager.py +4 -4
- general_manager/_types/measurement.py +1 -1
- general_manager/_types/permission.py +3 -3
- general_manager/_types/utils.py +12 -12
- general_manager/api/graphql.py +118 -95
- general_manager/api/mutation.py +9 -9
- general_manager/api/property.py +4 -4
- general_manager/apps.py +31 -31
- general_manager/bucket/{baseBucket.py → base_bucket.py} +5 -5
- general_manager/bucket/{calculationBucket.py → calculation_bucket.py} +10 -10
- general_manager/bucket/{databaseBucket.py → database_bucket.py} +16 -19
- general_manager/bucket/{groupBucket.py → group_bucket.py} +8 -8
- general_manager/cache/{cacheDecorator.py → cache_decorator.py} +5 -5
- general_manager/cache/{cacheTracker.py → cache_tracker.py} +1 -1
- general_manager/cache/{dependencyIndex.py → dependency_index.py} +1 -1
- general_manager/cache/{modelDependencyCollector.py → model_dependency_collector.py} +4 -4
- general_manager/cache/signals.py +1 -1
- general_manager/factory/{autoFactory.py → auto_factory.py} +24 -19
- general_manager/factory/factories.py +10 -13
- general_manager/factory/{factoryMethods.py → factory_methods.py} +19 -17
- general_manager/interface/{baseInterface.py → base_interface.py} +29 -21
- general_manager/interface/{calculationInterface.py → calculation_interface.py} +10 -10
- general_manager/interface/{databaseBasedInterface.py → database_based_interface.py} +42 -42
- general_manager/interface/{databaseInterface.py → database_interface.py} +15 -15
- general_manager/interface/models.py +3 -3
- general_manager/interface/{readOnlyInterface.py → read_only_interface.py} +12 -12
- general_manager/manager/{generalManager.py → general_manager.py} +11 -11
- general_manager/manager/{groupManager.py → group_manager.py} +6 -6
- general_manager/manager/input.py +1 -1
- general_manager/manager/meta.py +15 -15
- general_manager/measurement/measurement.py +3 -3
- general_manager/permission/{basePermission.py → base_permission.py} +21 -21
- general_manager/permission/{managerBasedPermission.py → manager_based_permission.py} +21 -21
- general_manager/permission/{mutationPermission.py → mutation_permission.py} +12 -12
- general_manager/permission/{permissionChecks.py → permission_checks.py} +2 -2
- general_manager/permission/{permissionDataManager.py → permission_data_manager.py} +6 -6
- general_manager/permission/utils.py +6 -6
- general_manager/public_api_registry.py +75 -66
- general_manager/rule/handler.py +2 -2
- general_manager/rule/rule.py +17 -11
- general_manager/utils/{filterParser.py → filter_parser.py} +3 -3
- general_manager/utils/{jsonEncoder.py → json_encoder.py} +1 -1
- general_manager/utils/{makeCacheKey.py → make_cache_key.py} +1 -1
- general_manager/utils/{noneToZero.py → none_to_zero.py} +1 -1
- general_manager/utils/{pathMapping.py → path_mapping.py} +14 -14
- general_manager/utils/testing.py +14 -14
- {generalmanager-0.19.0.dist-info → generalmanager-0.19.2.dist-info}/METADATA +1 -1
- generalmanager-0.19.2.dist-info/RECORD +77 -0
- generalmanager-0.19.0.dist-info/RECORD +0 -77
- /general_manager/measurement/{measurementField.py → measurement_field.py} +0 -0
- /general_manager/permission/{fileBasedPermission.py → file_based_permission.py} +0 -0
- /general_manager/utils/{argsToKwargs.py → args_to_kwargs.py} +0 -0
- /general_manager/utils/{formatString.py → format_string.py} +0 -0
- {generalmanager-0.19.0.dist-info → generalmanager-0.19.2.dist-info}/WHEEL +0 -0
- {generalmanager-0.19.0.dist-info → generalmanager-0.19.2.dist-info}/licenses/LICENSE +0 -0
- {generalmanager-0.19.0.dist-info → generalmanager-0.19.2.dist-info}/top_level.txt +0 -0
general_manager/api/graphql.py
CHANGED
|
@@ -11,6 +11,7 @@ from copy import deepcopy
|
|
|
11
11
|
from datetime import date, datetime
|
|
12
12
|
from decimal import Decimal
|
|
13
13
|
import hashlib
|
|
14
|
+
import re
|
|
14
15
|
from types import UnionType
|
|
15
16
|
from typing import (
|
|
16
17
|
Any,
|
|
@@ -38,12 +39,12 @@ from graphql.language.ast import (
|
|
|
38
39
|
from asgiref.sync import async_to_sync
|
|
39
40
|
from channels.layers import BaseChannelLayer, get_channel_layer
|
|
40
41
|
|
|
41
|
-
from general_manager.cache.
|
|
42
|
-
from general_manager.cache.
|
|
42
|
+
from general_manager.cache.cache_tracker import DependencyTracker
|
|
43
|
+
from general_manager.cache.dependency_index import Dependency
|
|
43
44
|
from general_manager.cache.signals import post_data_change
|
|
44
|
-
from general_manager.bucket.
|
|
45
|
-
from general_manager.interface.
|
|
46
|
-
from general_manager.manager.
|
|
45
|
+
from general_manager.bucket.base_bucket import Bucket
|
|
46
|
+
from general_manager.interface.base_interface import InterfaceBase
|
|
47
|
+
from general_manager.manager.general_manager import GeneralManager
|
|
47
48
|
from general_manager.measurement.measurement import Measurement
|
|
48
49
|
|
|
49
50
|
from django.core.exceptions import ValidationError
|
|
@@ -52,7 +53,7 @@ from graphql import GraphQLError
|
|
|
52
53
|
|
|
53
54
|
|
|
54
55
|
if TYPE_CHECKING:
|
|
55
|
-
from general_manager.permission.
|
|
56
|
+
from general_manager.permission.base_permission import BasePermission
|
|
56
57
|
from graphene import ResolveInfo as GraphQLResolveInfo
|
|
57
58
|
|
|
58
59
|
|
|
@@ -191,7 +192,7 @@ class PageInfo(graphene.ObjectType):
|
|
|
191
192
|
total_pages = graphene.Int(required=True)
|
|
192
193
|
|
|
193
194
|
|
|
194
|
-
def
|
|
195
|
+
def get_read_permission_filter(
|
|
195
196
|
generalManagerClass: Type[GeneralManager],
|
|
196
197
|
info: GraphQLResolveInfo,
|
|
197
198
|
) -> list[tuple[dict[str, Any], dict[str, Any]]]:
|
|
@@ -212,7 +213,7 @@ def getReadPermissionFilter(
|
|
|
212
213
|
if PermissionClass:
|
|
213
214
|
permission_filters = PermissionClass(
|
|
214
215
|
generalManagerClass, info.context.user
|
|
215
|
-
).
|
|
216
|
+
).get_permission_filter()
|
|
216
217
|
for permission_filter in permission_filters:
|
|
217
218
|
filter_dict = permission_filter.get("filter", {})
|
|
218
219
|
exclude_dict = permission_filter.get("exclude", {})
|
|
@@ -317,7 +318,7 @@ class GraphQL:
|
|
|
317
318
|
pass
|
|
318
319
|
|
|
319
320
|
@classmethod
|
|
320
|
-
def
|
|
321
|
+
def create_graphql_mutation(cls, generalManagerClass: type[GeneralManager]) -> None:
|
|
321
322
|
"""
|
|
322
323
|
Register GraphQL mutation classes for a GeneralManager based on its Interface.
|
|
323
324
|
|
|
@@ -341,24 +342,26 @@ class GraphQL:
|
|
|
341
342
|
}
|
|
342
343
|
if InterfaceBase.create.__code__ != interface_cls.create.__code__:
|
|
343
344
|
create_name = f"create{generalManagerClass.__name__}"
|
|
344
|
-
cls._mutations[create_name] = cls.
|
|
345
|
+
cls._mutations[create_name] = cls.generate_create_mutation_class(
|
|
345
346
|
generalManagerClass, default_return_values
|
|
346
347
|
)
|
|
347
348
|
|
|
348
349
|
if InterfaceBase.update.__code__ != interface_cls.update.__code__:
|
|
349
350
|
update_name = f"update{generalManagerClass.__name__}"
|
|
350
|
-
cls._mutations[update_name] = cls.
|
|
351
|
+
cls._mutations[update_name] = cls.generate_update_mutation_class(
|
|
351
352
|
generalManagerClass, default_return_values
|
|
352
353
|
)
|
|
353
354
|
|
|
354
355
|
if InterfaceBase.deactivate.__code__ != interface_cls.deactivate.__code__:
|
|
355
356
|
delete_name = f"delete{generalManagerClass.__name__}"
|
|
356
|
-
cls._mutations[delete_name] = cls.
|
|
357
|
+
cls._mutations[delete_name] = cls.generate_delete_mutation_class(
|
|
357
358
|
generalManagerClass, default_return_values
|
|
358
359
|
)
|
|
359
360
|
|
|
360
361
|
@classmethod
|
|
361
|
-
def
|
|
362
|
+
def create_graphql_interface(
|
|
363
|
+
cls, generalManagerClass: Type[GeneralManager]
|
|
364
|
+
) -> None:
|
|
362
365
|
"""
|
|
363
366
|
Create and register a Graphene ObjectType for a GeneralManager class and expose its queries and subscription.
|
|
364
367
|
|
|
@@ -377,17 +380,17 @@ class GraphQL:
|
|
|
377
380
|
fields: dict[str, Any] = {}
|
|
378
381
|
|
|
379
382
|
# Map Attribute Types to Graphene Fields
|
|
380
|
-
for field_name, field_info in interface_cls.
|
|
383
|
+
for field_name, field_info in interface_cls.get_attribute_types().items():
|
|
381
384
|
field_type = field_info["type"]
|
|
382
|
-
fields[field_name] = cls.
|
|
385
|
+
fields[field_name] = cls._map_field_to_graphene_read(field_type, field_name)
|
|
383
386
|
resolver_name = f"resolve_{field_name}"
|
|
384
|
-
fields[resolver_name] = cls.
|
|
387
|
+
fields[resolver_name] = cls._create_resolver(field_name, field_type)
|
|
385
388
|
|
|
386
389
|
# handle GraphQLProperty attributes
|
|
387
390
|
for (
|
|
388
391
|
attr_name,
|
|
389
392
|
attr_value,
|
|
390
|
-
) in generalManagerClass.Interface.
|
|
393
|
+
) in generalManagerClass.Interface.get_graph_ql_properties().items():
|
|
391
394
|
raw_hint = attr_value.graphql_type_hint
|
|
392
395
|
origin = get_origin(raw_hint)
|
|
393
396
|
type_args = [t for t in get_args(raw_hint) if t is not type(None)]
|
|
@@ -406,7 +409,7 @@ class GraphQL:
|
|
|
406
409
|
]
|
|
407
410
|
)
|
|
408
411
|
else:
|
|
409
|
-
base_type = GraphQL.
|
|
412
|
+
base_type = GraphQL._map_field_to_graphene_base_type(
|
|
410
413
|
cast(type, element if isinstance(element, type) else str)
|
|
411
414
|
)
|
|
412
415
|
graphene_field = graphene.List(base_type)
|
|
@@ -417,21 +420,23 @@ class GraphQL:
|
|
|
417
420
|
resolved_type = (
|
|
418
421
|
cast(type, type_args[0]) if type_args else cast(type, raw_hint)
|
|
419
422
|
)
|
|
420
|
-
graphene_field = cls.
|
|
423
|
+
graphene_field = cls._map_field_to_graphene_read(
|
|
424
|
+
resolved_type, attr_name
|
|
425
|
+
)
|
|
421
426
|
|
|
422
427
|
fields[attr_name] = graphene_field
|
|
423
|
-
fields[f"resolve_{attr_name}"] = cls.
|
|
428
|
+
fields[f"resolve_{attr_name}"] = cls._create_resolver(
|
|
424
429
|
attr_name, resolved_type
|
|
425
430
|
)
|
|
426
431
|
|
|
427
432
|
graphene_type = type(graphene_type_name, (graphene.ObjectType,), fields)
|
|
428
433
|
cls.graphql_type_registry[generalManagerClass.__name__] = graphene_type
|
|
429
434
|
cls.manager_registry[generalManagerClass.__name__] = generalManagerClass
|
|
430
|
-
cls.
|
|
431
|
-
cls.
|
|
435
|
+
cls._add_queries_to_schema(graphene_type, generalManagerClass)
|
|
436
|
+
cls._add_subscription_field(graphene_type, generalManagerClass)
|
|
432
437
|
|
|
433
438
|
@staticmethod
|
|
434
|
-
def
|
|
439
|
+
def _sort_by_options(
|
|
435
440
|
generalManagerClass: Type[GeneralManager],
|
|
436
441
|
) -> type[graphene.Enum] | None:
|
|
437
442
|
"""
|
|
@@ -444,7 +449,7 @@ class GraphQL:
|
|
|
444
449
|
for (
|
|
445
450
|
field_name,
|
|
446
451
|
field_info,
|
|
447
|
-
) in generalManagerClass.Interface.
|
|
452
|
+
) in generalManagerClass.Interface.get_attribute_types().items():
|
|
448
453
|
field_type = field_info["type"]
|
|
449
454
|
if issubclass(field_type, GeneralManager):
|
|
450
455
|
continue
|
|
@@ -454,7 +459,7 @@ class GraphQL:
|
|
|
454
459
|
for (
|
|
455
460
|
prop_name,
|
|
456
461
|
prop,
|
|
457
|
-
) in generalManagerClass.Interface.
|
|
462
|
+
) in generalManagerClass.Interface.get_graph_ql_properties().items():
|
|
458
463
|
if prop.sortable is False:
|
|
459
464
|
continue
|
|
460
465
|
type_hints = [
|
|
@@ -475,7 +480,7 @@ class GraphQL:
|
|
|
475
480
|
)
|
|
476
481
|
|
|
477
482
|
@staticmethod
|
|
478
|
-
def
|
|
483
|
+
def _get_filter_options(
|
|
479
484
|
attribute_type: type, attribute_name: str
|
|
480
485
|
) -> Generator[
|
|
481
486
|
tuple[
|
|
@@ -517,20 +522,20 @@ class GraphQL:
|
|
|
517
522
|
else:
|
|
518
523
|
yield (
|
|
519
524
|
attribute_name,
|
|
520
|
-
GraphQL.
|
|
525
|
+
GraphQL._map_field_to_graphene_read(attribute_type, attribute_name),
|
|
521
526
|
)
|
|
522
527
|
if issubclass(attribute_type, (int, float, Decimal, date, datetime)):
|
|
523
528
|
for option in number_options:
|
|
524
529
|
yield (
|
|
525
530
|
f"{attribute_name}__{option}",
|
|
526
531
|
(
|
|
527
|
-
GraphQL.
|
|
532
|
+
GraphQL._map_field_to_graphene_read(
|
|
528
533
|
attribute_type, attribute_name
|
|
529
534
|
)
|
|
530
535
|
),
|
|
531
536
|
)
|
|
532
537
|
elif issubclass(attribute_type, str):
|
|
533
|
-
base_type = GraphQL.
|
|
538
|
+
base_type = GraphQL._map_field_to_graphene_base_type(attribute_type)
|
|
534
539
|
for option in string_options:
|
|
535
540
|
if option == "in":
|
|
536
541
|
yield f"{attribute_name}__in", graphene.List(base_type)
|
|
@@ -538,14 +543,14 @@ class GraphQL:
|
|
|
538
543
|
yield (
|
|
539
544
|
f"{attribute_name}__{option}",
|
|
540
545
|
(
|
|
541
|
-
GraphQL.
|
|
546
|
+
GraphQL._map_field_to_graphene_read(
|
|
542
547
|
attribute_type, attribute_name
|
|
543
548
|
)
|
|
544
549
|
),
|
|
545
550
|
)
|
|
546
551
|
|
|
547
552
|
@staticmethod
|
|
548
|
-
def
|
|
553
|
+
def _create_filter_options(
|
|
549
554
|
field_type: Type[GeneralManager],
|
|
550
555
|
) -> type[graphene.InputObjectType] | None:
|
|
551
556
|
"""
|
|
@@ -565,17 +570,17 @@ class GraphQL:
|
|
|
565
570
|
return GraphQL.graphql_filter_type_registry[graphene_filter_type_name]
|
|
566
571
|
|
|
567
572
|
filter_fields: dict[str, Any] = {}
|
|
568
|
-
for attr_name, attr_info in field_type.Interface.
|
|
573
|
+
for attr_name, attr_info in field_type.Interface.get_attribute_types().items():
|
|
569
574
|
attr_type = attr_info["type"]
|
|
570
575
|
filter_fields = {
|
|
571
576
|
**filter_fields,
|
|
572
577
|
**{
|
|
573
578
|
k: v
|
|
574
|
-
for k, v in GraphQL.
|
|
579
|
+
for k, v in GraphQL._get_filter_options(attr_type, attr_name)
|
|
575
580
|
if v is not None
|
|
576
581
|
},
|
|
577
582
|
}
|
|
578
|
-
for prop_name, prop in field_type.Interface.
|
|
583
|
+
for prop_name, prop in field_type.Interface.get_graph_ql_properties().items():
|
|
579
584
|
if not prop.filterable:
|
|
580
585
|
continue
|
|
581
586
|
hints = [t for t in get_args(prop.graphql_type_hint) if t is not type(None)]
|
|
@@ -584,7 +589,7 @@ class GraphQL:
|
|
|
584
589
|
**filter_fields,
|
|
585
590
|
**{
|
|
586
591
|
k: v
|
|
587
|
-
for k, v in GraphQL.
|
|
592
|
+
for k, v in GraphQL._get_filter_options(prop_type, prop_name)
|
|
588
593
|
if v is not None
|
|
589
594
|
},
|
|
590
595
|
}
|
|
@@ -601,7 +606,7 @@ class GraphQL:
|
|
|
601
606
|
return filter_class
|
|
602
607
|
|
|
603
608
|
@staticmethod
|
|
604
|
-
def
|
|
609
|
+
def _map_field_to_graphene_read(field_type: type, field_name: str) -> Any:
|
|
605
610
|
"""
|
|
606
611
|
Map a field type and name to the appropriate Graphene field for reads.
|
|
607
612
|
|
|
@@ -622,16 +627,16 @@ class GraphQL:
|
|
|
622
627
|
"page_size": graphene.Int(),
|
|
623
628
|
"group_by": graphene.List(graphene.String),
|
|
624
629
|
}
|
|
625
|
-
filter_options = GraphQL.
|
|
630
|
+
filter_options = GraphQL._create_filter_options(field_type)
|
|
626
631
|
if filter_options:
|
|
627
632
|
attributes["filter"] = graphene.Argument(filter_options)
|
|
628
633
|
attributes["exclude"] = graphene.Argument(filter_options)
|
|
629
634
|
|
|
630
|
-
sort_by_options = GraphQL.
|
|
635
|
+
sort_by_options = GraphQL._sort_by_options(field_type)
|
|
631
636
|
if sort_by_options:
|
|
632
637
|
attributes["sort_by"] = graphene.Argument(sort_by_options)
|
|
633
638
|
|
|
634
|
-
page_type = GraphQL.
|
|
639
|
+
page_type = GraphQL._get_or_create_page_type(
|
|
635
640
|
field_type.__name__ + "Page",
|
|
636
641
|
lambda: GraphQL.graphql_type_registry[field_type.__name__],
|
|
637
642
|
)
|
|
@@ -641,10 +646,10 @@ class GraphQL:
|
|
|
641
646
|
lambda: GraphQL.graphql_type_registry[field_type.__name__]
|
|
642
647
|
)
|
|
643
648
|
else:
|
|
644
|
-
return GraphQL.
|
|
649
|
+
return GraphQL._map_field_to_graphene_base_type(field_type)()
|
|
645
650
|
|
|
646
651
|
@staticmethod
|
|
647
|
-
def
|
|
652
|
+
def _map_field_to_graphene_base_type(field_type: type) -> Type[Any]:
|
|
648
653
|
"""
|
|
649
654
|
Map a Python interface type to the corresponding Graphene scalar or custom scalar.
|
|
650
655
|
|
|
@@ -682,7 +687,7 @@ class GraphQL:
|
|
|
682
687
|
return graphene.String
|
|
683
688
|
|
|
684
689
|
@staticmethod
|
|
685
|
-
def
|
|
690
|
+
def _parse_input(input_val: dict[str, Any] | str | None) -> dict[str, Any]:
|
|
686
691
|
"""
|
|
687
692
|
Normalize a filter or exclude input into a dictionary.
|
|
688
693
|
|
|
@@ -704,7 +709,7 @@ class GraphQL:
|
|
|
704
709
|
return input_val
|
|
705
710
|
|
|
706
711
|
@staticmethod
|
|
707
|
-
def
|
|
712
|
+
def _apply_query_parameters(
|
|
708
713
|
queryset: Bucket[GeneralManager],
|
|
709
714
|
filter_input: dict[str, Any] | str | None,
|
|
710
715
|
exclude_input: dict[str, Any] | str | None,
|
|
@@ -723,11 +728,11 @@ class GraphQL:
|
|
|
723
728
|
Returns:
|
|
724
729
|
The queryset after applying filters, exclusions, and sorting.
|
|
725
730
|
"""
|
|
726
|
-
filters = GraphQL.
|
|
731
|
+
filters = GraphQL._parse_input(filter_input)
|
|
727
732
|
if filters:
|
|
728
733
|
queryset = queryset.filter(**filters)
|
|
729
734
|
|
|
730
|
-
excludes = GraphQL.
|
|
735
|
+
excludes = GraphQL._parse_input(exclude_input)
|
|
731
736
|
if excludes:
|
|
732
737
|
queryset = queryset.exclude(**excludes)
|
|
733
738
|
|
|
@@ -738,7 +743,7 @@ class GraphQL:
|
|
|
738
743
|
return queryset
|
|
739
744
|
|
|
740
745
|
@staticmethod
|
|
741
|
-
def
|
|
746
|
+
def _apply_permission_filters(
|
|
742
747
|
queryset: Bucket,
|
|
743
748
|
general_manager_class: type[GeneralManager],
|
|
744
749
|
info: GraphQLResolveInfo,
|
|
@@ -754,7 +759,7 @@ class GraphQL:
|
|
|
754
759
|
Returns:
|
|
755
760
|
Bucket: Queryset constrained by read permissions.
|
|
756
761
|
"""
|
|
757
|
-
permission_filters =
|
|
762
|
+
permission_filters = get_read_permission_filter(general_manager_class, info)
|
|
758
763
|
if not permission_filters:
|
|
759
764
|
return queryset
|
|
760
765
|
|
|
@@ -766,7 +771,7 @@ class GraphQL:
|
|
|
766
771
|
return filtered_queryset
|
|
767
772
|
|
|
768
773
|
@staticmethod
|
|
769
|
-
def
|
|
774
|
+
def _check_read_permission(
|
|
770
775
|
instance: GeneralManager, info: GraphQLResolveInfo, field_name: str
|
|
771
776
|
) -> bool:
|
|
772
777
|
"""Return True if the user may read ``field_name`` on ``instance``."""
|
|
@@ -774,13 +779,13 @@ class GraphQL:
|
|
|
774
779
|
instance, "Permission", None
|
|
775
780
|
)
|
|
776
781
|
if PermissionClass:
|
|
777
|
-
return PermissionClass(instance, info.context.user).
|
|
782
|
+
return PermissionClass(instance, info.context.user).check_permission(
|
|
778
783
|
"read", field_name
|
|
779
784
|
)
|
|
780
785
|
return True
|
|
781
786
|
|
|
782
787
|
@staticmethod
|
|
783
|
-
def
|
|
788
|
+
def _create_list_resolver(
|
|
784
789
|
base_getter: Callable[[Any], Any], fallback_manager_class: type[GeneralManager]
|
|
785
790
|
) -> Callable[..., Any]:
|
|
786
791
|
"""
|
|
@@ -827,13 +832,13 @@ class GraphQL:
|
|
|
827
832
|
manager_class = getattr(
|
|
828
833
|
base_queryset, "_manager_class", fallback_manager_class
|
|
829
834
|
)
|
|
830
|
-
qs = GraphQL.
|
|
831
|
-
qs = GraphQL.
|
|
832
|
-
qs = GraphQL.
|
|
835
|
+
qs = GraphQL._apply_permission_filters(base_queryset, manager_class, info)
|
|
836
|
+
qs = GraphQL._apply_query_parameters(qs, filter, exclude, sort_by, reverse)
|
|
837
|
+
qs = GraphQL._apply_grouping(qs, group_by)
|
|
833
838
|
|
|
834
839
|
total_count = len(qs)
|
|
835
840
|
|
|
836
|
-
qs_paginated = GraphQL.
|
|
841
|
+
qs_paginated = GraphQL._apply_pagination(qs, page, page_size)
|
|
837
842
|
|
|
838
843
|
page_info = {
|
|
839
844
|
"total_count": total_count,
|
|
@@ -851,7 +856,7 @@ class GraphQL:
|
|
|
851
856
|
return resolver
|
|
852
857
|
|
|
853
858
|
@staticmethod
|
|
854
|
-
def
|
|
859
|
+
def _apply_pagination(
|
|
855
860
|
queryset: Bucket[GeneralManager], page: int | None, page_size: int | None
|
|
856
861
|
) -> Bucket[GeneralManager]:
|
|
857
862
|
"""
|
|
@@ -874,7 +879,7 @@ class GraphQL:
|
|
|
874
879
|
return queryset
|
|
875
880
|
|
|
876
881
|
@staticmethod
|
|
877
|
-
def
|
|
882
|
+
def _apply_grouping(
|
|
878
883
|
queryset: Bucket[GeneralManager], group_by: list[str] | None
|
|
879
884
|
) -> Bucket[GeneralManager]:
|
|
880
885
|
"""
|
|
@@ -890,7 +895,7 @@ class GraphQL:
|
|
|
890
895
|
return queryset
|
|
891
896
|
|
|
892
897
|
@staticmethod
|
|
893
|
-
def
|
|
898
|
+
def _create_measurement_resolver(field_name: str) -> Callable[..., Any]:
|
|
894
899
|
"""
|
|
895
900
|
Creates a resolver for a Measurement field that returns its value and unit, with optional unit conversion.
|
|
896
901
|
|
|
@@ -902,7 +907,7 @@ class GraphQL:
|
|
|
902
907
|
info: GraphQLResolveInfo,
|
|
903
908
|
target_unit: str | None = None,
|
|
904
909
|
) -> dict[str, Any] | None:
|
|
905
|
-
if not GraphQL.
|
|
910
|
+
if not GraphQL._check_read_permission(self, info, field_name):
|
|
906
911
|
return None
|
|
907
912
|
result = getattr(self, field_name)
|
|
908
913
|
if not isinstance(result, Measurement):
|
|
@@ -917,35 +922,35 @@ class GraphQL:
|
|
|
917
922
|
return resolver
|
|
918
923
|
|
|
919
924
|
@staticmethod
|
|
920
|
-
def
|
|
925
|
+
def _create_normal_resolver(field_name: str) -> Callable[..., Any]:
|
|
921
926
|
"""
|
|
922
|
-
|
|
927
|
+
Create a resolver for scalar fields (no lists, no Measurement instances).
|
|
923
928
|
"""
|
|
924
929
|
|
|
925
930
|
def resolver(self: GeneralManager, info: GraphQLResolveInfo) -> Any:
|
|
926
|
-
if not GraphQL.
|
|
931
|
+
if not GraphQL._check_read_permission(self, info, field_name):
|
|
927
932
|
return None
|
|
928
933
|
return getattr(self, field_name)
|
|
929
934
|
|
|
930
935
|
return resolver
|
|
931
936
|
|
|
932
937
|
@classmethod
|
|
933
|
-
def
|
|
938
|
+
def _create_resolver(cls, field_name: str, field_type: type) -> Callable[..., Any]:
|
|
934
939
|
"""
|
|
935
940
|
Returns a resolver function for a field, selecting list, measurement, or standard resolution based on the field's type and name.
|
|
936
941
|
|
|
937
942
|
For fields ending with `_list` referencing a `GeneralManager` subclass, provides a resolver supporting pagination and filtering. For `Measurement` fields, returns a resolver that handles unit conversion and permission checks. For all other fields, returns a standard resolver with permission enforcement.
|
|
938
943
|
"""
|
|
939
944
|
if field_name.endswith("_list") and issubclass(field_type, GeneralManager):
|
|
940
|
-
return cls.
|
|
945
|
+
return cls._create_list_resolver(
|
|
941
946
|
lambda self: getattr(self, field_name), field_type
|
|
942
947
|
)
|
|
943
948
|
if issubclass(field_type, Measurement):
|
|
944
|
-
return cls.
|
|
945
|
-
return cls.
|
|
949
|
+
return cls._create_measurement_resolver(field_name)
|
|
950
|
+
return cls._create_normal_resolver(field_name)
|
|
946
951
|
|
|
947
952
|
@classmethod
|
|
948
|
-
def
|
|
953
|
+
def _get_or_create_page_type(
|
|
949
954
|
cls,
|
|
950
955
|
page_type_name: str,
|
|
951
956
|
item_type: type[graphene.ObjectType] | Callable[[], type[graphene.ObjectType]],
|
|
@@ -977,7 +982,7 @@ class GraphQL:
|
|
|
977
982
|
return cls._page_type_registry[page_type_name]
|
|
978
983
|
|
|
979
984
|
@classmethod
|
|
980
|
-
def
|
|
985
|
+
def _build_identification_arguments(
|
|
981
986
|
cls, generalManagerClass: Type[GeneralManager]
|
|
982
987
|
) -> dict[str, Any]:
|
|
983
988
|
"""
|
|
@@ -1006,14 +1011,14 @@ class GraphQL:
|
|
|
1006
1011
|
graphene.ID, required=True
|
|
1007
1012
|
)
|
|
1008
1013
|
else:
|
|
1009
|
-
base_type = cls.
|
|
1014
|
+
base_type = cls._map_field_to_graphene_base_type(input_field.type)
|
|
1010
1015
|
identification_fields[input_field_name] = graphene.Argument(
|
|
1011
1016
|
base_type, required=True
|
|
1012
1017
|
)
|
|
1013
1018
|
return identification_fields
|
|
1014
1019
|
|
|
1015
1020
|
@classmethod
|
|
1016
|
-
def
|
|
1021
|
+
def _add_queries_to_schema(
|
|
1017
1022
|
cls, graphene_type: type, generalManagerClass: Type[GeneralManager]
|
|
1018
1023
|
) -> None:
|
|
1019
1024
|
"""
|
|
@@ -1040,15 +1045,15 @@ class GraphQL:
|
|
|
1040
1045
|
"page_size": graphene.Int(),
|
|
1041
1046
|
"group_by": graphene.List(graphene.String),
|
|
1042
1047
|
}
|
|
1043
|
-
filter_options = cls.
|
|
1048
|
+
filter_options = cls._create_filter_options(generalManagerClass)
|
|
1044
1049
|
if filter_options:
|
|
1045
1050
|
attributes["filter"] = graphene.Argument(filter_options)
|
|
1046
1051
|
attributes["exclude"] = graphene.Argument(filter_options)
|
|
1047
|
-
sort_by_options = cls.
|
|
1052
|
+
sort_by_options = cls._sort_by_options(generalManagerClass)
|
|
1048
1053
|
if sort_by_options:
|
|
1049
1054
|
attributes["sort_by"] = graphene.Argument(sort_by_options)
|
|
1050
1055
|
|
|
1051
|
-
page_type = cls.
|
|
1056
|
+
page_type = cls._get_or_create_page_type(
|
|
1052
1057
|
graphene_type.__name__ + "Page", graphene_type
|
|
1053
1058
|
)
|
|
1054
1059
|
list_field = graphene.Field(page_type, **attributes)
|
|
@@ -1062,13 +1067,13 @@ class GraphQL:
|
|
|
1062
1067
|
"""
|
|
1063
1068
|
return generalManagerClass.all()
|
|
1064
1069
|
|
|
1065
|
-
list_resolver = cls.
|
|
1070
|
+
list_resolver = cls._create_list_resolver(_all_items, generalManagerClass)
|
|
1066
1071
|
cls._query_fields[list_field_name] = list_field
|
|
1067
1072
|
cls._query_fields[f"resolve_{list_field_name}"] = list_resolver
|
|
1068
1073
|
|
|
1069
1074
|
# resolver and field for the single item query
|
|
1070
1075
|
item_field_name = generalManagerClass.__name__.lower()
|
|
1071
|
-
identification_fields = cls.
|
|
1076
|
+
identification_fields = cls._build_identification_arguments(generalManagerClass)
|
|
1072
1077
|
item_field = graphene.Field(graphene_type, **identification_fields)
|
|
1073
1078
|
|
|
1074
1079
|
def resolver(
|
|
@@ -1088,6 +1093,23 @@ class GraphQL:
|
|
|
1088
1093
|
cls._query_fields[item_field_name] = item_field
|
|
1089
1094
|
cls._query_fields[f"resolve_{item_field_name}"] = resolver
|
|
1090
1095
|
|
|
1096
|
+
@staticmethod
|
|
1097
|
+
def _normalize_graphql_name(name: str) -> str:
|
|
1098
|
+
"""
|
|
1099
|
+
Convert a GraphQL selection name (potentially camelCase) to the corresponding Python attribute name.
|
|
1100
|
+
|
|
1101
|
+
Parameters:
|
|
1102
|
+
name (str): GraphQL field name from a selection set.
|
|
1103
|
+
|
|
1104
|
+
Returns:
|
|
1105
|
+
str: The snake_case representation matching the GraphQLProperty definition.
|
|
1106
|
+
"""
|
|
1107
|
+
if "_" in name:
|
|
1108
|
+
return name
|
|
1109
|
+
snake = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
|
|
1110
|
+
snake = re.sub("([a-z0-9])([A-Z])", r"\1_\2", snake)
|
|
1111
|
+
return snake.lower()
|
|
1112
|
+
|
|
1091
1113
|
@staticmethod
|
|
1092
1114
|
def _prime_graphql_properties(
|
|
1093
1115
|
instance: GeneralManager, property_names: Iterable[str] | None = None
|
|
@@ -1104,7 +1126,7 @@ class GraphQL:
|
|
|
1104
1126
|
interface_cls = getattr(instance.__class__, "Interface", None)
|
|
1105
1127
|
if interface_cls is None:
|
|
1106
1128
|
return
|
|
1107
|
-
available_properties = interface_cls.
|
|
1129
|
+
available_properties = interface_cls.get_graph_ql_properties()
|
|
1108
1130
|
if property_names is None:
|
|
1109
1131
|
names = available_properties.keys()
|
|
1110
1132
|
else:
|
|
@@ -1162,7 +1184,7 @@ class GraphQL:
|
|
|
1162
1184
|
interface_cls = getattr(manager_class, "Interface", None)
|
|
1163
1185
|
if interface_cls is None:
|
|
1164
1186
|
return set()
|
|
1165
|
-
available_properties = set(interface_cls.
|
|
1187
|
+
available_properties = set(interface_cls.get_graph_ql_properties().keys())
|
|
1166
1188
|
if not available_properties:
|
|
1167
1189
|
return set()
|
|
1168
1190
|
|
|
@@ -1185,8 +1207,9 @@ class GraphQL:
|
|
|
1185
1207
|
for selection in selection_set.selections:
|
|
1186
1208
|
if isinstance(selection, FieldNode):
|
|
1187
1209
|
name = selection.name.value
|
|
1188
|
-
|
|
1189
|
-
|
|
1210
|
+
normalized = cls._normalize_graphql_name(name)
|
|
1211
|
+
if normalized in available_properties:
|
|
1212
|
+
property_names.add(normalized)
|
|
1190
1213
|
elif isinstance(selection, FragmentSpreadNode):
|
|
1191
1214
|
fragment = info.fragments.get(selection.name.value)
|
|
1192
1215
|
if fragment is not None:
|
|
@@ -1338,7 +1361,7 @@ class GraphQL:
|
|
|
1338
1361
|
return instance, set()
|
|
1339
1362
|
|
|
1340
1363
|
@classmethod
|
|
1341
|
-
def
|
|
1364
|
+
def _add_subscription_field(
|
|
1342
1365
|
cls,
|
|
1343
1366
|
graphene_type: type[graphene.ObjectType],
|
|
1344
1367
|
generalManagerClass: Type[GeneralManager],
|
|
@@ -1380,7 +1403,7 @@ class GraphQL:
|
|
|
1380
1403
|
payload_type
|
|
1381
1404
|
)
|
|
1382
1405
|
|
|
1383
|
-
identification_args = cls.
|
|
1406
|
+
identification_args = cls._build_identification_arguments(generalManagerClass)
|
|
1384
1407
|
subscription_field = graphene.Field(payload_type, **identification_args)
|
|
1385
1408
|
|
|
1386
1409
|
async def subscribe(
|
|
@@ -1501,7 +1524,7 @@ class GraphQL:
|
|
|
1501
1524
|
cls._subscription_fields[f"resolve_{field_name}"] = resolve
|
|
1502
1525
|
|
|
1503
1526
|
@classmethod
|
|
1504
|
-
def
|
|
1527
|
+
def create_write_fields(cls, interface_cls: InterfaceBase) -> dict[str, Any]:
|
|
1505
1528
|
"""
|
|
1506
1529
|
Create Graphene input fields for writable attributes defined by an Interface.
|
|
1507
1530
|
|
|
@@ -1515,7 +1538,7 @@ class GraphQL:
|
|
|
1515
1538
|
"""
|
|
1516
1539
|
fields: dict[str, Any] = {}
|
|
1517
1540
|
|
|
1518
|
-
for name, info in interface_cls.
|
|
1541
|
+
for name, info in interface_cls.get_attribute_types().items():
|
|
1519
1542
|
if name in ["changed_by", "created_at", "updated_at"]:
|
|
1520
1543
|
continue
|
|
1521
1544
|
if info["is_derived"]:
|
|
@@ -1539,7 +1562,7 @@ class GraphQL:
|
|
|
1539
1562
|
default_value=default,
|
|
1540
1563
|
)
|
|
1541
1564
|
else:
|
|
1542
|
-
base_cls = cls.
|
|
1565
|
+
base_cls = cls._map_field_to_graphene_base_type(typ)
|
|
1543
1566
|
fld = base_cls(
|
|
1544
1567
|
required=req,
|
|
1545
1568
|
default_value=default,
|
|
@@ -1557,7 +1580,7 @@ class GraphQL:
|
|
|
1557
1580
|
return fields
|
|
1558
1581
|
|
|
1559
1582
|
@classmethod
|
|
1560
|
-
def
|
|
1583
|
+
def generate_create_mutation_class(
|
|
1561
1584
|
cls,
|
|
1562
1585
|
generalManagerClass: type[GeneralManager],
|
|
1563
1586
|
default_return_values: dict[str, Any],
|
|
@@ -1603,7 +1626,7 @@ class GraphQL:
|
|
|
1603
1626
|
**kwargs, creator_id=info.context.user.id
|
|
1604
1627
|
)
|
|
1605
1628
|
except HANDLED_MANAGER_ERRORS as error:
|
|
1606
|
-
raise GraphQL.
|
|
1629
|
+
raise GraphQL._handle_graph_ql_error(error) from error
|
|
1607
1630
|
|
|
1608
1631
|
return {
|
|
1609
1632
|
"success": True,
|
|
@@ -1621,7 +1644,7 @@ class GraphQL:
|
|
|
1621
1644
|
(),
|
|
1622
1645
|
{
|
|
1623
1646
|
field_name: field
|
|
1624
|
-
for field_name, field in cls.
|
|
1647
|
+
for field_name, field in cls.create_write_fields(
|
|
1625
1648
|
interface_cls
|
|
1626
1649
|
).items()
|
|
1627
1650
|
if field_name not in generalManagerClass.Interface.input_fields
|
|
@@ -1632,7 +1655,7 @@ class GraphQL:
|
|
|
1632
1655
|
)
|
|
1633
1656
|
|
|
1634
1657
|
@classmethod
|
|
1635
|
-
def
|
|
1658
|
+
def generate_update_mutation_class(
|
|
1636
1659
|
cls,
|
|
1637
1660
|
generalManagerClass: type[GeneralManager],
|
|
1638
1661
|
default_return_values: dict[str, Any],
|
|
@@ -1668,13 +1691,13 @@ class GraphQL:
|
|
|
1668
1691
|
"""
|
|
1669
1692
|
manager_id = kwargs.pop("id", None)
|
|
1670
1693
|
if manager_id is None:
|
|
1671
|
-
raise GraphQL.
|
|
1694
|
+
raise GraphQL._handle_graph_ql_error(MissingManagerIdentifierError())
|
|
1672
1695
|
try:
|
|
1673
1696
|
instance = generalManagerClass(id=manager_id).update(
|
|
1674
1697
|
creator_id=info.context.user.id, **kwargs
|
|
1675
1698
|
)
|
|
1676
1699
|
except HANDLED_MANAGER_ERRORS as error:
|
|
1677
|
-
raise GraphQL.
|
|
1700
|
+
raise GraphQL._handle_graph_ql_error(error) from error
|
|
1678
1701
|
|
|
1679
1702
|
return {
|
|
1680
1703
|
"success": True,
|
|
@@ -1694,7 +1717,7 @@ class GraphQL:
|
|
|
1694
1717
|
"id": graphene.ID(required=True),
|
|
1695
1718
|
**{
|
|
1696
1719
|
field_name: field
|
|
1697
|
-
for field_name, field in cls.
|
|
1720
|
+
for field_name, field in cls.create_write_fields(
|
|
1698
1721
|
interface_cls
|
|
1699
1722
|
).items()
|
|
1700
1723
|
if field.editable
|
|
@@ -1706,7 +1729,7 @@ class GraphQL:
|
|
|
1706
1729
|
)
|
|
1707
1730
|
|
|
1708
1731
|
@classmethod
|
|
1709
|
-
def
|
|
1732
|
+
def generate_delete_mutation_class(
|
|
1710
1733
|
cls,
|
|
1711
1734
|
generalManagerClass: type[GeneralManager],
|
|
1712
1735
|
default_return_values: dict[str, Any],
|
|
@@ -1738,13 +1761,13 @@ class GraphQL:
|
|
|
1738
1761
|
"""
|
|
1739
1762
|
manager_id = kwargs.pop("id", None)
|
|
1740
1763
|
if manager_id is None:
|
|
1741
|
-
raise GraphQL.
|
|
1764
|
+
raise GraphQL._handle_graph_ql_error(MissingManagerIdentifierError())
|
|
1742
1765
|
try:
|
|
1743
1766
|
instance = generalManagerClass(id=manager_id).deactivate(
|
|
1744
1767
|
creator_id=info.context.user.id
|
|
1745
1768
|
)
|
|
1746
1769
|
except HANDLED_MANAGER_ERRORS as error:
|
|
1747
|
-
raise GraphQL.
|
|
1770
|
+
raise GraphQL._handle_graph_ql_error(error) from error
|
|
1748
1771
|
|
|
1749
1772
|
return {
|
|
1750
1773
|
"success": True,
|
|
@@ -1762,7 +1785,7 @@ class GraphQL:
|
|
|
1762
1785
|
(),
|
|
1763
1786
|
{
|
|
1764
1787
|
field_name: field
|
|
1765
|
-
for field_name, field in cls.
|
|
1788
|
+
for field_name, field in cls.create_write_fields(
|
|
1766
1789
|
interface_cls
|
|
1767
1790
|
).items()
|
|
1768
1791
|
if field_name in generalManagerClass.Interface.input_fields
|
|
@@ -1773,7 +1796,7 @@ class GraphQL:
|
|
|
1773
1796
|
)
|
|
1774
1797
|
|
|
1775
1798
|
@staticmethod
|
|
1776
|
-
def
|
|
1799
|
+
def _handle_graph_ql_error(error: Exception) -> GraphQLError:
|
|
1777
1800
|
"""
|
|
1778
1801
|
Convert an exception into a GraphQL error with an appropriate extensions['code'].
|
|
1779
1802
|
|