GeneralManager 0.19.1__py3-none-any.whl → 0.20.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.
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 +11 -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 +207 -98
- general_manager/api/mutation.py +9 -9
- general_manager/api/property.py +4 -4
- general_manager/apps.py +120 -65
- 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} +27 -6
- general_manager/cache/{cacheTracker.py → cache_tracker.py} +1 -1
- general_manager/cache/{dependencyIndex.py → dependency_index.py} +24 -8
- 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} +30 -22
- 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} +21 -21
- general_manager/interface/models.py +3 -3
- general_manager/interface/{readOnlyInterface.py → read_only_interface.py} +34 -25
- general_manager/logging.py +133 -0
- general_manager/manager/{generalManager.py → general_manager.py} +75 -17
- general_manager/manager/{groupManager.py → group_manager.py} +6 -6
- general_manager/manager/input.py +1 -1
- general_manager/manager/meta.py +63 -17
- general_manager/measurement/measurement.py +3 -3
- general_manager/permission/{basePermission.py → base_permission.py} +55 -32
- 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 +76 -66
- general_manager/rule/handler.py +2 -2
- general_manager/rule/rule.py +102 -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/public_api.py +19 -0
- general_manager/utils/testing.py +14 -14
- {generalmanager-0.19.1.dist-info → generalmanager-0.20.0.dist-info}/METADATA +1 -1
- generalmanager-0.20.0.dist-info/RECORD +78 -0
- generalmanager-0.19.1.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.1.dist-info → generalmanager-0.20.0.dist-info}/WHEEL +0 -0
- {generalmanager-0.19.1.dist-info → generalmanager-0.20.0.dist-info}/licenses/LICENSE +0 -0
- {generalmanager-0.19.1.dist-info → generalmanager-0.20.0.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,13 @@ 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.
|
|
42
|
-
from general_manager.cache.
|
|
42
|
+
from general_manager.bucket.base_bucket import Bucket
|
|
43
|
+
from general_manager.cache.cache_tracker import DependencyTracker
|
|
44
|
+
from general_manager.cache.dependency_index import Dependency
|
|
43
45
|
from general_manager.cache.signals import post_data_change
|
|
44
|
-
from general_manager.
|
|
45
|
-
from general_manager.
|
|
46
|
-
from general_manager.manager.
|
|
46
|
+
from general_manager.interface.base_interface import InterfaceBase
|
|
47
|
+
from general_manager.logging import get_logger
|
|
48
|
+
from general_manager.manager.general_manager import GeneralManager
|
|
47
49
|
from general_manager.measurement.measurement import Measurement
|
|
48
50
|
|
|
49
51
|
from django.core.exceptions import ValidationError
|
|
@@ -52,10 +54,13 @@ from graphql import GraphQLError
|
|
|
52
54
|
|
|
53
55
|
|
|
54
56
|
if TYPE_CHECKING:
|
|
55
|
-
from general_manager.permission.
|
|
57
|
+
from general_manager.permission.base_permission import BasePermission
|
|
56
58
|
from graphene import ResolveInfo as GraphQLResolveInfo
|
|
57
59
|
|
|
58
60
|
|
|
61
|
+
logger = get_logger("api.graphql")
|
|
62
|
+
|
|
63
|
+
|
|
59
64
|
@dataclass(slots=True)
|
|
60
65
|
class SubscriptionEvent:
|
|
61
66
|
"""Payload delivered to GraphQL subscription resolvers."""
|
|
@@ -191,7 +196,7 @@ class PageInfo(graphene.ObjectType):
|
|
|
191
196
|
total_pages = graphene.Int(required=True)
|
|
192
197
|
|
|
193
198
|
|
|
194
|
-
def
|
|
199
|
+
def get_read_permission_filter(
|
|
195
200
|
generalManagerClass: Type[GeneralManager],
|
|
196
201
|
info: GraphQLResolveInfo,
|
|
197
202
|
) -> list[tuple[dict[str, Any], dict[str, Any]]]:
|
|
@@ -212,7 +217,7 @@ def getReadPermissionFilter(
|
|
|
212
217
|
if PermissionClass:
|
|
213
218
|
permission_filters = PermissionClass(
|
|
214
219
|
generalManagerClass, info.context.user
|
|
215
|
-
).
|
|
220
|
+
).get_permission_filter()
|
|
216
221
|
for permission_filter in permission_filters:
|
|
217
222
|
filter_dict = permission_filter.get("filter", {})
|
|
218
223
|
exclude_dict = permission_filter.get("exclude", {})
|
|
@@ -317,7 +322,7 @@ class GraphQL:
|
|
|
317
322
|
pass
|
|
318
323
|
|
|
319
324
|
@classmethod
|
|
320
|
-
def
|
|
325
|
+
def create_graphql_mutation(cls, generalManagerClass: type[GeneralManager]) -> None:
|
|
321
326
|
"""
|
|
322
327
|
Register GraphQL mutation classes for a GeneralManager based on its Interface.
|
|
323
328
|
|
|
@@ -341,24 +346,47 @@ class GraphQL:
|
|
|
341
346
|
}
|
|
342
347
|
if InterfaceBase.create.__code__ != interface_cls.create.__code__:
|
|
343
348
|
create_name = f"create{generalManagerClass.__name__}"
|
|
344
|
-
cls._mutations[create_name] = cls.
|
|
349
|
+
cls._mutations[create_name] = cls.generate_create_mutation_class(
|
|
345
350
|
generalManagerClass, default_return_values
|
|
346
351
|
)
|
|
352
|
+
logger.debug(
|
|
353
|
+
"registered graphql mutation",
|
|
354
|
+
context={
|
|
355
|
+
"manager": generalManagerClass.__name__,
|
|
356
|
+
"mutation": create_name,
|
|
357
|
+
},
|
|
358
|
+
)
|
|
347
359
|
|
|
348
360
|
if InterfaceBase.update.__code__ != interface_cls.update.__code__:
|
|
349
361
|
update_name = f"update{generalManagerClass.__name__}"
|
|
350
|
-
cls._mutations[update_name] = cls.
|
|
362
|
+
cls._mutations[update_name] = cls.generate_update_mutation_class(
|
|
351
363
|
generalManagerClass, default_return_values
|
|
352
364
|
)
|
|
365
|
+
logger.debug(
|
|
366
|
+
"registered graphql mutation",
|
|
367
|
+
context={
|
|
368
|
+
"manager": generalManagerClass.__name__,
|
|
369
|
+
"mutation": update_name,
|
|
370
|
+
},
|
|
371
|
+
)
|
|
353
372
|
|
|
354
373
|
if InterfaceBase.deactivate.__code__ != interface_cls.deactivate.__code__:
|
|
355
374
|
delete_name = f"delete{generalManagerClass.__name__}"
|
|
356
|
-
cls._mutations[delete_name] = cls.
|
|
375
|
+
cls._mutations[delete_name] = cls.generate_delete_mutation_class(
|
|
357
376
|
generalManagerClass, default_return_values
|
|
358
377
|
)
|
|
378
|
+
logger.debug(
|
|
379
|
+
"registered graphql mutation",
|
|
380
|
+
context={
|
|
381
|
+
"manager": generalManagerClass.__name__,
|
|
382
|
+
"mutation": delete_name,
|
|
383
|
+
},
|
|
384
|
+
)
|
|
359
385
|
|
|
360
386
|
@classmethod
|
|
361
|
-
def
|
|
387
|
+
def create_graphql_interface(
|
|
388
|
+
cls, generalManagerClass: Type[GeneralManager]
|
|
389
|
+
) -> None:
|
|
362
390
|
"""
|
|
363
391
|
Create and register a Graphene ObjectType for a GeneralManager class and expose its queries and subscription.
|
|
364
392
|
|
|
@@ -373,21 +401,26 @@ class GraphQL:
|
|
|
373
401
|
if not interface_cls:
|
|
374
402
|
return None
|
|
375
403
|
|
|
404
|
+
logger.info(
|
|
405
|
+
"building graphql interface",
|
|
406
|
+
context={"manager": generalManagerClass.__name__},
|
|
407
|
+
)
|
|
408
|
+
|
|
376
409
|
graphene_type_name = f"{generalManagerClass.__name__}Type"
|
|
377
410
|
fields: dict[str, Any] = {}
|
|
378
411
|
|
|
379
412
|
# Map Attribute Types to Graphene Fields
|
|
380
|
-
for field_name, field_info in interface_cls.
|
|
413
|
+
for field_name, field_info in interface_cls.get_attribute_types().items():
|
|
381
414
|
field_type = field_info["type"]
|
|
382
|
-
fields[field_name] = cls.
|
|
415
|
+
fields[field_name] = cls._map_field_to_graphene_read(field_type, field_name)
|
|
383
416
|
resolver_name = f"resolve_{field_name}"
|
|
384
|
-
fields[resolver_name] = cls.
|
|
417
|
+
fields[resolver_name] = cls._create_resolver(field_name, field_type)
|
|
385
418
|
|
|
386
419
|
# handle GraphQLProperty attributes
|
|
387
420
|
for (
|
|
388
421
|
attr_name,
|
|
389
422
|
attr_value,
|
|
390
|
-
) in generalManagerClass.Interface.
|
|
423
|
+
) in generalManagerClass.Interface.get_graph_ql_properties().items():
|
|
391
424
|
raw_hint = attr_value.graphql_type_hint
|
|
392
425
|
origin = get_origin(raw_hint)
|
|
393
426
|
type_args = [t for t in get_args(raw_hint) if t is not type(None)]
|
|
@@ -406,7 +439,7 @@ class GraphQL:
|
|
|
406
439
|
]
|
|
407
440
|
)
|
|
408
441
|
else:
|
|
409
|
-
base_type = GraphQL.
|
|
442
|
+
base_type = GraphQL._map_field_to_graphene_base_type(
|
|
410
443
|
cast(type, element if isinstance(element, type) else str)
|
|
411
444
|
)
|
|
412
445
|
graphene_field = graphene.List(base_type)
|
|
@@ -417,21 +450,33 @@ class GraphQL:
|
|
|
417
450
|
resolved_type = (
|
|
418
451
|
cast(type, type_args[0]) if type_args else cast(type, raw_hint)
|
|
419
452
|
)
|
|
420
|
-
graphene_field = cls.
|
|
453
|
+
graphene_field = cls._map_field_to_graphene_read(
|
|
454
|
+
resolved_type, attr_name
|
|
455
|
+
)
|
|
421
456
|
|
|
422
457
|
fields[attr_name] = graphene_field
|
|
423
|
-
fields[f"resolve_{attr_name}"] = cls.
|
|
458
|
+
fields[f"resolve_{attr_name}"] = cls._create_resolver(
|
|
424
459
|
attr_name, resolved_type
|
|
425
460
|
)
|
|
426
461
|
|
|
427
462
|
graphene_type = type(graphene_type_name, (graphene.ObjectType,), fields)
|
|
428
463
|
cls.graphql_type_registry[generalManagerClass.__name__] = graphene_type
|
|
429
464
|
cls.manager_registry[generalManagerClass.__name__] = generalManagerClass
|
|
430
|
-
cls.
|
|
431
|
-
cls.
|
|
465
|
+
cls._add_queries_to_schema(graphene_type, generalManagerClass)
|
|
466
|
+
cls._add_subscription_field(graphene_type, generalManagerClass)
|
|
467
|
+
exposed_fields = sorted(
|
|
468
|
+
name for name in fields.keys() if not name.startswith("resolve_")
|
|
469
|
+
)
|
|
470
|
+
logger.debug(
|
|
471
|
+
"registered graphql interface",
|
|
472
|
+
context={
|
|
473
|
+
"manager": generalManagerClass.__name__,
|
|
474
|
+
"fields": exposed_fields,
|
|
475
|
+
},
|
|
476
|
+
)
|
|
432
477
|
|
|
433
478
|
@staticmethod
|
|
434
|
-
def
|
|
479
|
+
def _sort_by_options(
|
|
435
480
|
generalManagerClass: Type[GeneralManager],
|
|
436
481
|
) -> type[graphene.Enum] | None:
|
|
437
482
|
"""
|
|
@@ -444,7 +489,7 @@ class GraphQL:
|
|
|
444
489
|
for (
|
|
445
490
|
field_name,
|
|
446
491
|
field_info,
|
|
447
|
-
) in generalManagerClass.Interface.
|
|
492
|
+
) in generalManagerClass.Interface.get_attribute_types().items():
|
|
448
493
|
field_type = field_info["type"]
|
|
449
494
|
if issubclass(field_type, GeneralManager):
|
|
450
495
|
continue
|
|
@@ -454,7 +499,7 @@ class GraphQL:
|
|
|
454
499
|
for (
|
|
455
500
|
prop_name,
|
|
456
501
|
prop,
|
|
457
|
-
) in generalManagerClass.Interface.
|
|
502
|
+
) in generalManagerClass.Interface.get_graph_ql_properties().items():
|
|
458
503
|
if prop.sortable is False:
|
|
459
504
|
continue
|
|
460
505
|
type_hints = [
|
|
@@ -475,7 +520,7 @@ class GraphQL:
|
|
|
475
520
|
)
|
|
476
521
|
|
|
477
522
|
@staticmethod
|
|
478
|
-
def
|
|
523
|
+
def _get_filter_options(
|
|
479
524
|
attribute_type: type, attribute_name: str
|
|
480
525
|
) -> Generator[
|
|
481
526
|
tuple[
|
|
@@ -517,20 +562,20 @@ class GraphQL:
|
|
|
517
562
|
else:
|
|
518
563
|
yield (
|
|
519
564
|
attribute_name,
|
|
520
|
-
GraphQL.
|
|
565
|
+
GraphQL._map_field_to_graphene_read(attribute_type, attribute_name),
|
|
521
566
|
)
|
|
522
567
|
if issubclass(attribute_type, (int, float, Decimal, date, datetime)):
|
|
523
568
|
for option in number_options:
|
|
524
569
|
yield (
|
|
525
570
|
f"{attribute_name}__{option}",
|
|
526
571
|
(
|
|
527
|
-
GraphQL.
|
|
572
|
+
GraphQL._map_field_to_graphene_read(
|
|
528
573
|
attribute_type, attribute_name
|
|
529
574
|
)
|
|
530
575
|
),
|
|
531
576
|
)
|
|
532
577
|
elif issubclass(attribute_type, str):
|
|
533
|
-
base_type = GraphQL.
|
|
578
|
+
base_type = GraphQL._map_field_to_graphene_base_type(attribute_type)
|
|
534
579
|
for option in string_options:
|
|
535
580
|
if option == "in":
|
|
536
581
|
yield f"{attribute_name}__in", graphene.List(base_type)
|
|
@@ -538,14 +583,14 @@ class GraphQL:
|
|
|
538
583
|
yield (
|
|
539
584
|
f"{attribute_name}__{option}",
|
|
540
585
|
(
|
|
541
|
-
GraphQL.
|
|
586
|
+
GraphQL._map_field_to_graphene_read(
|
|
542
587
|
attribute_type, attribute_name
|
|
543
588
|
)
|
|
544
589
|
),
|
|
545
590
|
)
|
|
546
591
|
|
|
547
592
|
@staticmethod
|
|
548
|
-
def
|
|
593
|
+
def _create_filter_options(
|
|
549
594
|
field_type: Type[GeneralManager],
|
|
550
595
|
) -> type[graphene.InputObjectType] | None:
|
|
551
596
|
"""
|
|
@@ -565,17 +610,17 @@ class GraphQL:
|
|
|
565
610
|
return GraphQL.graphql_filter_type_registry[graphene_filter_type_name]
|
|
566
611
|
|
|
567
612
|
filter_fields: dict[str, Any] = {}
|
|
568
|
-
for attr_name, attr_info in field_type.Interface.
|
|
613
|
+
for attr_name, attr_info in field_type.Interface.get_attribute_types().items():
|
|
569
614
|
attr_type = attr_info["type"]
|
|
570
615
|
filter_fields = {
|
|
571
616
|
**filter_fields,
|
|
572
617
|
**{
|
|
573
618
|
k: v
|
|
574
|
-
for k, v in GraphQL.
|
|
619
|
+
for k, v in GraphQL._get_filter_options(attr_type, attr_name)
|
|
575
620
|
if v is not None
|
|
576
621
|
},
|
|
577
622
|
}
|
|
578
|
-
for prop_name, prop in field_type.Interface.
|
|
623
|
+
for prop_name, prop in field_type.Interface.get_graph_ql_properties().items():
|
|
579
624
|
if not prop.filterable:
|
|
580
625
|
continue
|
|
581
626
|
hints = [t for t in get_args(prop.graphql_type_hint) if t is not type(None)]
|
|
@@ -584,7 +629,7 @@ class GraphQL:
|
|
|
584
629
|
**filter_fields,
|
|
585
630
|
**{
|
|
586
631
|
k: v
|
|
587
|
-
for k, v in GraphQL.
|
|
632
|
+
for k, v in GraphQL._get_filter_options(prop_type, prop_name)
|
|
588
633
|
if v is not None
|
|
589
634
|
},
|
|
590
635
|
}
|
|
@@ -601,7 +646,7 @@ class GraphQL:
|
|
|
601
646
|
return filter_class
|
|
602
647
|
|
|
603
648
|
@staticmethod
|
|
604
|
-
def
|
|
649
|
+
def _map_field_to_graphene_read(field_type: type, field_name: str) -> Any:
|
|
605
650
|
"""
|
|
606
651
|
Map a field type and name to the appropriate Graphene field for reads.
|
|
607
652
|
|
|
@@ -622,16 +667,16 @@ class GraphQL:
|
|
|
622
667
|
"page_size": graphene.Int(),
|
|
623
668
|
"group_by": graphene.List(graphene.String),
|
|
624
669
|
}
|
|
625
|
-
filter_options = GraphQL.
|
|
670
|
+
filter_options = GraphQL._create_filter_options(field_type)
|
|
626
671
|
if filter_options:
|
|
627
672
|
attributes["filter"] = graphene.Argument(filter_options)
|
|
628
673
|
attributes["exclude"] = graphene.Argument(filter_options)
|
|
629
674
|
|
|
630
|
-
sort_by_options = GraphQL.
|
|
675
|
+
sort_by_options = GraphQL._sort_by_options(field_type)
|
|
631
676
|
if sort_by_options:
|
|
632
677
|
attributes["sort_by"] = graphene.Argument(sort_by_options)
|
|
633
678
|
|
|
634
|
-
page_type = GraphQL.
|
|
679
|
+
page_type = GraphQL._get_or_create_page_type(
|
|
635
680
|
field_type.__name__ + "Page",
|
|
636
681
|
lambda: GraphQL.graphql_type_registry[field_type.__name__],
|
|
637
682
|
)
|
|
@@ -641,10 +686,10 @@ class GraphQL:
|
|
|
641
686
|
lambda: GraphQL.graphql_type_registry[field_type.__name__]
|
|
642
687
|
)
|
|
643
688
|
else:
|
|
644
|
-
return GraphQL.
|
|
689
|
+
return GraphQL._map_field_to_graphene_base_type(field_type)()
|
|
645
690
|
|
|
646
691
|
@staticmethod
|
|
647
|
-
def
|
|
692
|
+
def _map_field_to_graphene_base_type(field_type: type) -> Type[Any]:
|
|
648
693
|
"""
|
|
649
694
|
Map a Python interface type to the corresponding Graphene scalar or custom scalar.
|
|
650
695
|
|
|
@@ -682,7 +727,7 @@ class GraphQL:
|
|
|
682
727
|
return graphene.String
|
|
683
728
|
|
|
684
729
|
@staticmethod
|
|
685
|
-
def
|
|
730
|
+
def _parse_input(input_val: dict[str, Any] | str | None) -> dict[str, Any]:
|
|
686
731
|
"""
|
|
687
732
|
Normalize a filter or exclude input into a dictionary.
|
|
688
733
|
|
|
@@ -704,7 +749,7 @@ class GraphQL:
|
|
|
704
749
|
return input_val
|
|
705
750
|
|
|
706
751
|
@staticmethod
|
|
707
|
-
def
|
|
752
|
+
def _apply_query_parameters(
|
|
708
753
|
queryset: Bucket[GeneralManager],
|
|
709
754
|
filter_input: dict[str, Any] | str | None,
|
|
710
755
|
exclude_input: dict[str, Any] | str | None,
|
|
@@ -723,11 +768,11 @@ class GraphQL:
|
|
|
723
768
|
Returns:
|
|
724
769
|
The queryset after applying filters, exclusions, and sorting.
|
|
725
770
|
"""
|
|
726
|
-
filters = GraphQL.
|
|
771
|
+
filters = GraphQL._parse_input(filter_input)
|
|
727
772
|
if filters:
|
|
728
773
|
queryset = queryset.filter(**filters)
|
|
729
774
|
|
|
730
|
-
excludes = GraphQL.
|
|
775
|
+
excludes = GraphQL._parse_input(exclude_input)
|
|
731
776
|
if excludes:
|
|
732
777
|
queryset = queryset.exclude(**excludes)
|
|
733
778
|
|
|
@@ -738,7 +783,7 @@ class GraphQL:
|
|
|
738
783
|
return queryset
|
|
739
784
|
|
|
740
785
|
@staticmethod
|
|
741
|
-
def
|
|
786
|
+
def _apply_permission_filters(
|
|
742
787
|
queryset: Bucket,
|
|
743
788
|
general_manager_class: type[GeneralManager],
|
|
744
789
|
info: GraphQLResolveInfo,
|
|
@@ -754,7 +799,7 @@ class GraphQL:
|
|
|
754
799
|
Returns:
|
|
755
800
|
Bucket: Queryset constrained by read permissions.
|
|
756
801
|
"""
|
|
757
|
-
permission_filters =
|
|
802
|
+
permission_filters = get_read_permission_filter(general_manager_class, info)
|
|
758
803
|
if not permission_filters:
|
|
759
804
|
return queryset
|
|
760
805
|
|
|
@@ -766,7 +811,7 @@ class GraphQL:
|
|
|
766
811
|
return filtered_queryset
|
|
767
812
|
|
|
768
813
|
@staticmethod
|
|
769
|
-
def
|
|
814
|
+
def _check_read_permission(
|
|
770
815
|
instance: GeneralManager, info: GraphQLResolveInfo, field_name: str
|
|
771
816
|
) -> bool:
|
|
772
817
|
"""Return True if the user may read ``field_name`` on ``instance``."""
|
|
@@ -774,13 +819,13 @@ class GraphQL:
|
|
|
774
819
|
instance, "Permission", None
|
|
775
820
|
)
|
|
776
821
|
if PermissionClass:
|
|
777
|
-
return PermissionClass(instance, info.context.user).
|
|
822
|
+
return PermissionClass(instance, info.context.user).check_permission(
|
|
778
823
|
"read", field_name
|
|
779
824
|
)
|
|
780
825
|
return True
|
|
781
826
|
|
|
782
827
|
@staticmethod
|
|
783
|
-
def
|
|
828
|
+
def _create_list_resolver(
|
|
784
829
|
base_getter: Callable[[Any], Any], fallback_manager_class: type[GeneralManager]
|
|
785
830
|
) -> Callable[..., Any]:
|
|
786
831
|
"""
|
|
@@ -827,13 +872,13 @@ class GraphQL:
|
|
|
827
872
|
manager_class = getattr(
|
|
828
873
|
base_queryset, "_manager_class", fallback_manager_class
|
|
829
874
|
)
|
|
830
|
-
qs = GraphQL.
|
|
831
|
-
qs = GraphQL.
|
|
832
|
-
qs = GraphQL.
|
|
875
|
+
qs = GraphQL._apply_permission_filters(base_queryset, manager_class, info)
|
|
876
|
+
qs = GraphQL._apply_query_parameters(qs, filter, exclude, sort_by, reverse)
|
|
877
|
+
qs = GraphQL._apply_grouping(qs, group_by)
|
|
833
878
|
|
|
834
879
|
total_count = len(qs)
|
|
835
880
|
|
|
836
|
-
qs_paginated = GraphQL.
|
|
881
|
+
qs_paginated = GraphQL._apply_pagination(qs, page, page_size)
|
|
837
882
|
|
|
838
883
|
page_info = {
|
|
839
884
|
"total_count": total_count,
|
|
@@ -851,7 +896,7 @@ class GraphQL:
|
|
|
851
896
|
return resolver
|
|
852
897
|
|
|
853
898
|
@staticmethod
|
|
854
|
-
def
|
|
899
|
+
def _apply_pagination(
|
|
855
900
|
queryset: Bucket[GeneralManager], page: int | None, page_size: int | None
|
|
856
901
|
) -> Bucket[GeneralManager]:
|
|
857
902
|
"""
|
|
@@ -874,7 +919,7 @@ class GraphQL:
|
|
|
874
919
|
return queryset
|
|
875
920
|
|
|
876
921
|
@staticmethod
|
|
877
|
-
def
|
|
922
|
+
def _apply_grouping(
|
|
878
923
|
queryset: Bucket[GeneralManager], group_by: list[str] | None
|
|
879
924
|
) -> Bucket[GeneralManager]:
|
|
880
925
|
"""
|
|
@@ -890,7 +935,7 @@ class GraphQL:
|
|
|
890
935
|
return queryset
|
|
891
936
|
|
|
892
937
|
@staticmethod
|
|
893
|
-
def
|
|
938
|
+
def _create_measurement_resolver(field_name: str) -> Callable[..., Any]:
|
|
894
939
|
"""
|
|
895
940
|
Creates a resolver for a Measurement field that returns its value and unit, with optional unit conversion.
|
|
896
941
|
|
|
@@ -902,7 +947,7 @@ class GraphQL:
|
|
|
902
947
|
info: GraphQLResolveInfo,
|
|
903
948
|
target_unit: str | None = None,
|
|
904
949
|
) -> dict[str, Any] | None:
|
|
905
|
-
if not GraphQL.
|
|
950
|
+
if not GraphQL._check_read_permission(self, info, field_name):
|
|
906
951
|
return None
|
|
907
952
|
result = getattr(self, field_name)
|
|
908
953
|
if not isinstance(result, Measurement):
|
|
@@ -917,35 +962,35 @@ class GraphQL:
|
|
|
917
962
|
return resolver
|
|
918
963
|
|
|
919
964
|
@staticmethod
|
|
920
|
-
def
|
|
965
|
+
def _create_normal_resolver(field_name: str) -> Callable[..., Any]:
|
|
921
966
|
"""
|
|
922
|
-
|
|
967
|
+
Create a resolver for scalar fields (no lists, no Measurement instances).
|
|
923
968
|
"""
|
|
924
969
|
|
|
925
970
|
def resolver(self: GeneralManager, info: GraphQLResolveInfo) -> Any:
|
|
926
|
-
if not GraphQL.
|
|
971
|
+
if not GraphQL._check_read_permission(self, info, field_name):
|
|
927
972
|
return None
|
|
928
973
|
return getattr(self, field_name)
|
|
929
974
|
|
|
930
975
|
return resolver
|
|
931
976
|
|
|
932
977
|
@classmethod
|
|
933
|
-
def
|
|
978
|
+
def _create_resolver(cls, field_name: str, field_type: type) -> Callable[..., Any]:
|
|
934
979
|
"""
|
|
935
980
|
Returns a resolver function for a field, selecting list, measurement, or standard resolution based on the field's type and name.
|
|
936
981
|
|
|
937
982
|
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
983
|
"""
|
|
939
984
|
if field_name.endswith("_list") and issubclass(field_type, GeneralManager):
|
|
940
|
-
return cls.
|
|
985
|
+
return cls._create_list_resolver(
|
|
941
986
|
lambda self: getattr(self, field_name), field_type
|
|
942
987
|
)
|
|
943
988
|
if issubclass(field_type, Measurement):
|
|
944
|
-
return cls.
|
|
945
|
-
return cls.
|
|
989
|
+
return cls._create_measurement_resolver(field_name)
|
|
990
|
+
return cls._create_normal_resolver(field_name)
|
|
946
991
|
|
|
947
992
|
@classmethod
|
|
948
|
-
def
|
|
993
|
+
def _get_or_create_page_type(
|
|
949
994
|
cls,
|
|
950
995
|
page_type_name: str,
|
|
951
996
|
item_type: type[graphene.ObjectType] | Callable[[], type[graphene.ObjectType]],
|
|
@@ -977,7 +1022,7 @@ class GraphQL:
|
|
|
977
1022
|
return cls._page_type_registry[page_type_name]
|
|
978
1023
|
|
|
979
1024
|
@classmethod
|
|
980
|
-
def
|
|
1025
|
+
def _build_identification_arguments(
|
|
981
1026
|
cls, generalManagerClass: Type[GeneralManager]
|
|
982
1027
|
) -> dict[str, Any]:
|
|
983
1028
|
"""
|
|
@@ -1006,14 +1051,14 @@ class GraphQL:
|
|
|
1006
1051
|
graphene.ID, required=True
|
|
1007
1052
|
)
|
|
1008
1053
|
else:
|
|
1009
|
-
base_type = cls.
|
|
1054
|
+
base_type = cls._map_field_to_graphene_base_type(input_field.type)
|
|
1010
1055
|
identification_fields[input_field_name] = graphene.Argument(
|
|
1011
1056
|
base_type, required=True
|
|
1012
1057
|
)
|
|
1013
1058
|
return identification_fields
|
|
1014
1059
|
|
|
1015
1060
|
@classmethod
|
|
1016
|
-
def
|
|
1061
|
+
def _add_queries_to_schema(
|
|
1017
1062
|
cls, graphene_type: type, generalManagerClass: Type[GeneralManager]
|
|
1018
1063
|
) -> None:
|
|
1019
1064
|
"""
|
|
@@ -1040,15 +1085,15 @@ class GraphQL:
|
|
|
1040
1085
|
"page_size": graphene.Int(),
|
|
1041
1086
|
"group_by": graphene.List(graphene.String),
|
|
1042
1087
|
}
|
|
1043
|
-
filter_options = cls.
|
|
1088
|
+
filter_options = cls._create_filter_options(generalManagerClass)
|
|
1044
1089
|
if filter_options:
|
|
1045
1090
|
attributes["filter"] = graphene.Argument(filter_options)
|
|
1046
1091
|
attributes["exclude"] = graphene.Argument(filter_options)
|
|
1047
|
-
sort_by_options = cls.
|
|
1092
|
+
sort_by_options = cls._sort_by_options(generalManagerClass)
|
|
1048
1093
|
if sort_by_options:
|
|
1049
1094
|
attributes["sort_by"] = graphene.Argument(sort_by_options)
|
|
1050
1095
|
|
|
1051
|
-
page_type = cls.
|
|
1096
|
+
page_type = cls._get_or_create_page_type(
|
|
1052
1097
|
graphene_type.__name__ + "Page", graphene_type
|
|
1053
1098
|
)
|
|
1054
1099
|
list_field = graphene.Field(page_type, **attributes)
|
|
@@ -1062,13 +1107,13 @@ class GraphQL:
|
|
|
1062
1107
|
"""
|
|
1063
1108
|
return generalManagerClass.all()
|
|
1064
1109
|
|
|
1065
|
-
list_resolver = cls.
|
|
1110
|
+
list_resolver = cls._create_list_resolver(_all_items, generalManagerClass)
|
|
1066
1111
|
cls._query_fields[list_field_name] = list_field
|
|
1067
1112
|
cls._query_fields[f"resolve_{list_field_name}"] = list_resolver
|
|
1068
1113
|
|
|
1069
1114
|
# resolver and field for the single item query
|
|
1070
1115
|
item_field_name = generalManagerClass.__name__.lower()
|
|
1071
|
-
identification_fields = cls.
|
|
1116
|
+
identification_fields = cls._build_identification_arguments(generalManagerClass)
|
|
1072
1117
|
item_field = graphene.Field(graphene_type, **identification_fields)
|
|
1073
1118
|
|
|
1074
1119
|
def resolver(
|
|
@@ -1088,6 +1133,23 @@ class GraphQL:
|
|
|
1088
1133
|
cls._query_fields[item_field_name] = item_field
|
|
1089
1134
|
cls._query_fields[f"resolve_{item_field_name}"] = resolver
|
|
1090
1135
|
|
|
1136
|
+
@staticmethod
|
|
1137
|
+
def _normalize_graphql_name(name: str) -> str:
|
|
1138
|
+
"""
|
|
1139
|
+
Convert a GraphQL selection name (potentially camelCase) to the corresponding Python attribute name.
|
|
1140
|
+
|
|
1141
|
+
Parameters:
|
|
1142
|
+
name (str): GraphQL field name from a selection set.
|
|
1143
|
+
|
|
1144
|
+
Returns:
|
|
1145
|
+
str: The snake_case representation matching the GraphQLProperty definition.
|
|
1146
|
+
"""
|
|
1147
|
+
if "_" in name:
|
|
1148
|
+
return name
|
|
1149
|
+
snake = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
|
|
1150
|
+
snake = re.sub("([a-z0-9])([A-Z])", r"\1_\2", snake)
|
|
1151
|
+
return snake.lower()
|
|
1152
|
+
|
|
1091
1153
|
@staticmethod
|
|
1092
1154
|
def _prime_graphql_properties(
|
|
1093
1155
|
instance: GeneralManager, property_names: Iterable[str] | None = None
|
|
@@ -1104,7 +1166,7 @@ class GraphQL:
|
|
|
1104
1166
|
interface_cls = getattr(instance.__class__, "Interface", None)
|
|
1105
1167
|
if interface_cls is None:
|
|
1106
1168
|
return
|
|
1107
|
-
available_properties = interface_cls.
|
|
1169
|
+
available_properties = interface_cls.get_graph_ql_properties()
|
|
1108
1170
|
if property_names is None:
|
|
1109
1171
|
names = available_properties.keys()
|
|
1110
1172
|
else:
|
|
@@ -1162,7 +1224,7 @@ class GraphQL:
|
|
|
1162
1224
|
interface_cls = getattr(manager_class, "Interface", None)
|
|
1163
1225
|
if interface_cls is None:
|
|
1164
1226
|
return set()
|
|
1165
|
-
available_properties = set(interface_cls.
|
|
1227
|
+
available_properties = set(interface_cls.get_graph_ql_properties().keys())
|
|
1166
1228
|
if not available_properties:
|
|
1167
1229
|
return set()
|
|
1168
1230
|
|
|
@@ -1185,8 +1247,9 @@ class GraphQL:
|
|
|
1185
1247
|
for selection in selection_set.selections:
|
|
1186
1248
|
if isinstance(selection, FieldNode):
|
|
1187
1249
|
name = selection.name.value
|
|
1188
|
-
|
|
1189
|
-
|
|
1250
|
+
normalized = cls._normalize_graphql_name(name)
|
|
1251
|
+
if normalized in available_properties:
|
|
1252
|
+
property_names.add(normalized)
|
|
1190
1253
|
elif isinstance(selection, FragmentSpreadNode):
|
|
1191
1254
|
fragment = info.fragments.get(selection.name.value)
|
|
1192
1255
|
if fragment is not None:
|
|
@@ -1338,7 +1401,7 @@ class GraphQL:
|
|
|
1338
1401
|
return instance, set()
|
|
1339
1402
|
|
|
1340
1403
|
@classmethod
|
|
1341
|
-
def
|
|
1404
|
+
def _add_subscription_field(
|
|
1342
1405
|
cls,
|
|
1343
1406
|
graphene_type: type[graphene.ObjectType],
|
|
1344
1407
|
generalManagerClass: Type[GeneralManager],
|
|
@@ -1380,7 +1443,7 @@ class GraphQL:
|
|
|
1380
1443
|
payload_type
|
|
1381
1444
|
)
|
|
1382
1445
|
|
|
1383
|
-
identification_args = cls.
|
|
1446
|
+
identification_args = cls._build_identification_arguments(generalManagerClass)
|
|
1384
1447
|
subscription_field = graphene.Field(payload_type, **identification_args)
|
|
1385
1448
|
|
|
1386
1449
|
async def subscribe(
|
|
@@ -1501,7 +1564,7 @@ class GraphQL:
|
|
|
1501
1564
|
cls._subscription_fields[f"resolve_{field_name}"] = resolve
|
|
1502
1565
|
|
|
1503
1566
|
@classmethod
|
|
1504
|
-
def
|
|
1567
|
+
def create_write_fields(cls, interface_cls: InterfaceBase) -> dict[str, Any]:
|
|
1505
1568
|
"""
|
|
1506
1569
|
Create Graphene input fields for writable attributes defined by an Interface.
|
|
1507
1570
|
|
|
@@ -1515,7 +1578,7 @@ class GraphQL:
|
|
|
1515
1578
|
"""
|
|
1516
1579
|
fields: dict[str, Any] = {}
|
|
1517
1580
|
|
|
1518
|
-
for name, info in interface_cls.
|
|
1581
|
+
for name, info in interface_cls.get_attribute_types().items():
|
|
1519
1582
|
if name in ["changed_by", "created_at", "updated_at"]:
|
|
1520
1583
|
continue
|
|
1521
1584
|
if info["is_derived"]:
|
|
@@ -1539,7 +1602,7 @@ class GraphQL:
|
|
|
1539
1602
|
default_value=default,
|
|
1540
1603
|
)
|
|
1541
1604
|
else:
|
|
1542
|
-
base_cls = cls.
|
|
1605
|
+
base_cls = cls._map_field_to_graphene_base_type(typ)
|
|
1543
1606
|
fld = base_cls(
|
|
1544
1607
|
required=req,
|
|
1545
1608
|
default_value=default,
|
|
@@ -1557,7 +1620,7 @@ class GraphQL:
|
|
|
1557
1620
|
return fields
|
|
1558
1621
|
|
|
1559
1622
|
@classmethod
|
|
1560
|
-
def
|
|
1623
|
+
def generate_create_mutation_class(
|
|
1561
1624
|
cls,
|
|
1562
1625
|
generalManagerClass: type[GeneralManager],
|
|
1563
1626
|
default_return_values: dict[str, Any],
|
|
@@ -1603,7 +1666,7 @@ class GraphQL:
|
|
|
1603
1666
|
**kwargs, creator_id=info.context.user.id
|
|
1604
1667
|
)
|
|
1605
1668
|
except HANDLED_MANAGER_ERRORS as error:
|
|
1606
|
-
raise GraphQL.
|
|
1669
|
+
raise GraphQL._handle_graph_ql_error(error) from error
|
|
1607
1670
|
|
|
1608
1671
|
return {
|
|
1609
1672
|
"success": True,
|
|
@@ -1621,7 +1684,7 @@ class GraphQL:
|
|
|
1621
1684
|
(),
|
|
1622
1685
|
{
|
|
1623
1686
|
field_name: field
|
|
1624
|
-
for field_name, field in cls.
|
|
1687
|
+
for field_name, field in cls.create_write_fields(
|
|
1625
1688
|
interface_cls
|
|
1626
1689
|
).items()
|
|
1627
1690
|
if field_name not in generalManagerClass.Interface.input_fields
|
|
@@ -1632,7 +1695,7 @@ class GraphQL:
|
|
|
1632
1695
|
)
|
|
1633
1696
|
|
|
1634
1697
|
@classmethod
|
|
1635
|
-
def
|
|
1698
|
+
def generate_update_mutation_class(
|
|
1636
1699
|
cls,
|
|
1637
1700
|
generalManagerClass: type[GeneralManager],
|
|
1638
1701
|
default_return_values: dict[str, Any],
|
|
@@ -1668,13 +1731,13 @@ class GraphQL:
|
|
|
1668
1731
|
"""
|
|
1669
1732
|
manager_id = kwargs.pop("id", None)
|
|
1670
1733
|
if manager_id is None:
|
|
1671
|
-
raise GraphQL.
|
|
1734
|
+
raise GraphQL._handle_graph_ql_error(MissingManagerIdentifierError())
|
|
1672
1735
|
try:
|
|
1673
1736
|
instance = generalManagerClass(id=manager_id).update(
|
|
1674
1737
|
creator_id=info.context.user.id, **kwargs
|
|
1675
1738
|
)
|
|
1676
1739
|
except HANDLED_MANAGER_ERRORS as error:
|
|
1677
|
-
raise GraphQL.
|
|
1740
|
+
raise GraphQL._handle_graph_ql_error(error) from error
|
|
1678
1741
|
|
|
1679
1742
|
return {
|
|
1680
1743
|
"success": True,
|
|
@@ -1694,7 +1757,7 @@ class GraphQL:
|
|
|
1694
1757
|
"id": graphene.ID(required=True),
|
|
1695
1758
|
**{
|
|
1696
1759
|
field_name: field
|
|
1697
|
-
for field_name, field in cls.
|
|
1760
|
+
for field_name, field in cls.create_write_fields(
|
|
1698
1761
|
interface_cls
|
|
1699
1762
|
).items()
|
|
1700
1763
|
if field.editable
|
|
@@ -1706,7 +1769,7 @@ class GraphQL:
|
|
|
1706
1769
|
)
|
|
1707
1770
|
|
|
1708
1771
|
@classmethod
|
|
1709
|
-
def
|
|
1772
|
+
def generate_delete_mutation_class(
|
|
1710
1773
|
cls,
|
|
1711
1774
|
generalManagerClass: type[GeneralManager],
|
|
1712
1775
|
default_return_values: dict[str, Any],
|
|
@@ -1738,13 +1801,13 @@ class GraphQL:
|
|
|
1738
1801
|
"""
|
|
1739
1802
|
manager_id = kwargs.pop("id", None)
|
|
1740
1803
|
if manager_id is None:
|
|
1741
|
-
raise GraphQL.
|
|
1804
|
+
raise GraphQL._handle_graph_ql_error(MissingManagerIdentifierError())
|
|
1742
1805
|
try:
|
|
1743
1806
|
instance = generalManagerClass(id=manager_id).deactivate(
|
|
1744
1807
|
creator_id=info.context.user.id
|
|
1745
1808
|
)
|
|
1746
1809
|
except HANDLED_MANAGER_ERRORS as error:
|
|
1747
|
-
raise GraphQL.
|
|
1810
|
+
raise GraphQL._handle_graph_ql_error(error) from error
|
|
1748
1811
|
|
|
1749
1812
|
return {
|
|
1750
1813
|
"success": True,
|
|
@@ -1762,7 +1825,7 @@ class GraphQL:
|
|
|
1762
1825
|
(),
|
|
1763
1826
|
{
|
|
1764
1827
|
field_name: field
|
|
1765
|
-
for field_name, field in cls.
|
|
1828
|
+
for field_name, field in cls.create_write_fields(
|
|
1766
1829
|
interface_cls
|
|
1767
1830
|
).items()
|
|
1768
1831
|
if field_name in generalManagerClass.Interface.input_fields
|
|
@@ -1773,7 +1836,7 @@ class GraphQL:
|
|
|
1773
1836
|
)
|
|
1774
1837
|
|
|
1775
1838
|
@staticmethod
|
|
1776
|
-
def
|
|
1839
|
+
def _handle_graph_ql_error(error: Exception) -> GraphQLError:
|
|
1777
1840
|
"""
|
|
1778
1841
|
Convert an exception into a GraphQL error with an appropriate extensions['code'].
|
|
1779
1842
|
|
|
@@ -1788,23 +1851,47 @@ class GraphQL:
|
|
|
1788
1851
|
Returns:
|
|
1789
1852
|
GraphQLError: GraphQL error containing the original message and an `extensions['code']` indicating the error category.
|
|
1790
1853
|
"""
|
|
1854
|
+
message = str(error)
|
|
1855
|
+
error_name = type(error).__name__
|
|
1791
1856
|
if isinstance(error, PermissionError):
|
|
1857
|
+
logger.info(
|
|
1858
|
+
"graphql permission error",
|
|
1859
|
+
context={
|
|
1860
|
+
"error": error_name,
|
|
1861
|
+
"message": message,
|
|
1862
|
+
},
|
|
1863
|
+
)
|
|
1792
1864
|
return GraphQLError(
|
|
1793
|
-
|
|
1865
|
+
message,
|
|
1794
1866
|
extensions={
|
|
1795
1867
|
"code": "PERMISSION_DENIED",
|
|
1796
1868
|
},
|
|
1797
1869
|
)
|
|
1798
1870
|
elif isinstance(error, (ValueError, ValidationError, TypeError)):
|
|
1871
|
+
logger.warning(
|
|
1872
|
+
"graphql user error",
|
|
1873
|
+
context={
|
|
1874
|
+
"error": error_name,
|
|
1875
|
+
"message": message,
|
|
1876
|
+
},
|
|
1877
|
+
)
|
|
1799
1878
|
return GraphQLError(
|
|
1800
|
-
|
|
1879
|
+
message,
|
|
1801
1880
|
extensions={
|
|
1802
1881
|
"code": "BAD_USER_INPUT",
|
|
1803
1882
|
},
|
|
1804
1883
|
)
|
|
1805
1884
|
else:
|
|
1885
|
+
logger.error(
|
|
1886
|
+
"graphql internal error",
|
|
1887
|
+
context={
|
|
1888
|
+
"error": error_name,
|
|
1889
|
+
"message": message,
|
|
1890
|
+
},
|
|
1891
|
+
exc_info=error,
|
|
1892
|
+
)
|
|
1806
1893
|
return GraphQLError(
|
|
1807
|
-
|
|
1894
|
+
message,
|
|
1808
1895
|
extensions={
|
|
1809
1896
|
"code": "INTERNAL_SERVER_ERROR",
|
|
1810
1897
|
},
|
|
@@ -1837,10 +1924,24 @@ class GraphQL:
|
|
|
1837
1924
|
manager_class = instance.__class__
|
|
1838
1925
|
|
|
1839
1926
|
if manager_class.__name__ not in cls.manager_registry:
|
|
1927
|
+
logger.debug(
|
|
1928
|
+
"skipping subscription event for unregistered manager",
|
|
1929
|
+
context={
|
|
1930
|
+
"manager": manager_class.__name__,
|
|
1931
|
+
"action": action,
|
|
1932
|
+
},
|
|
1933
|
+
)
|
|
1840
1934
|
return
|
|
1841
1935
|
|
|
1842
1936
|
channel_layer = cls._get_channel_layer()
|
|
1843
1937
|
if channel_layer is None:
|
|
1938
|
+
logger.warning(
|
|
1939
|
+
"channel layer unavailable for subscription event",
|
|
1940
|
+
context={
|
|
1941
|
+
"manager": manager_class.__name__,
|
|
1942
|
+
"action": action,
|
|
1943
|
+
},
|
|
1944
|
+
)
|
|
1844
1945
|
return
|
|
1845
1946
|
|
|
1846
1947
|
group_name = cls._group_name(manager_class, instance.identification)
|
|
@@ -1851,6 +1952,14 @@ class GraphQL:
|
|
|
1851
1952
|
"action": action,
|
|
1852
1953
|
},
|
|
1853
1954
|
)
|
|
1955
|
+
logger.debug(
|
|
1956
|
+
"dispatched subscription event",
|
|
1957
|
+
context={
|
|
1958
|
+
"manager": manager_class.__name__,
|
|
1959
|
+
"action": action,
|
|
1960
|
+
"group": group_name,
|
|
1961
|
+
},
|
|
1962
|
+
)
|
|
1854
1963
|
|
|
1855
1964
|
|
|
1856
1965
|
post_data_change.connect(GraphQL._handle_data_change, weak=False)
|