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.

Files changed (64) hide show
  1. general_manager/_types/api.py +4 -4
  2. general_manager/_types/bucket.py +4 -4
  3. general_manager/_types/cache.py +6 -6
  4. general_manager/_types/factory.py +35 -35
  5. general_manager/_types/general_manager.py +11 -9
  6. general_manager/_types/interface.py +5 -5
  7. general_manager/_types/manager.py +4 -4
  8. general_manager/_types/measurement.py +1 -1
  9. general_manager/_types/permission.py +3 -3
  10. general_manager/_types/utils.py +12 -12
  11. general_manager/api/graphql.py +207 -98
  12. general_manager/api/mutation.py +9 -9
  13. general_manager/api/property.py +4 -4
  14. general_manager/apps.py +120 -65
  15. general_manager/bucket/{baseBucket.py → base_bucket.py} +5 -5
  16. general_manager/bucket/{calculationBucket.py → calculation_bucket.py} +10 -10
  17. general_manager/bucket/{databaseBucket.py → database_bucket.py} +16 -19
  18. general_manager/bucket/{groupBucket.py → group_bucket.py} +8 -8
  19. general_manager/cache/{cacheDecorator.py → cache_decorator.py} +27 -6
  20. general_manager/cache/{cacheTracker.py → cache_tracker.py} +1 -1
  21. general_manager/cache/{dependencyIndex.py → dependency_index.py} +24 -8
  22. general_manager/cache/{modelDependencyCollector.py → model_dependency_collector.py} +4 -4
  23. general_manager/cache/signals.py +1 -1
  24. general_manager/factory/{autoFactory.py → auto_factory.py} +24 -19
  25. general_manager/factory/factories.py +10 -13
  26. general_manager/factory/{factoryMethods.py → factory_methods.py} +19 -17
  27. general_manager/interface/{baseInterface.py → base_interface.py} +30 -22
  28. general_manager/interface/{calculationInterface.py → calculation_interface.py} +10 -10
  29. general_manager/interface/{databaseBasedInterface.py → database_based_interface.py} +42 -42
  30. general_manager/interface/{databaseInterface.py → database_interface.py} +21 -21
  31. general_manager/interface/models.py +3 -3
  32. general_manager/interface/{readOnlyInterface.py → read_only_interface.py} +34 -25
  33. general_manager/logging.py +133 -0
  34. general_manager/manager/{generalManager.py → general_manager.py} +75 -17
  35. general_manager/manager/{groupManager.py → group_manager.py} +6 -6
  36. general_manager/manager/input.py +1 -1
  37. general_manager/manager/meta.py +63 -17
  38. general_manager/measurement/measurement.py +3 -3
  39. general_manager/permission/{basePermission.py → base_permission.py} +55 -32
  40. general_manager/permission/{managerBasedPermission.py → manager_based_permission.py} +21 -21
  41. general_manager/permission/{mutationPermission.py → mutation_permission.py} +12 -12
  42. general_manager/permission/{permissionChecks.py → permission_checks.py} +2 -2
  43. general_manager/permission/{permissionDataManager.py → permission_data_manager.py} +6 -6
  44. general_manager/permission/utils.py +6 -6
  45. general_manager/public_api_registry.py +76 -66
  46. general_manager/rule/handler.py +2 -2
  47. general_manager/rule/rule.py +102 -11
  48. general_manager/utils/{filterParser.py → filter_parser.py} +3 -3
  49. general_manager/utils/{jsonEncoder.py → json_encoder.py} +1 -1
  50. general_manager/utils/{makeCacheKey.py → make_cache_key.py} +1 -1
  51. general_manager/utils/{noneToZero.py → none_to_zero.py} +1 -1
  52. general_manager/utils/{pathMapping.py → path_mapping.py} +14 -14
  53. general_manager/utils/public_api.py +19 -0
  54. general_manager/utils/testing.py +14 -14
  55. {generalmanager-0.19.1.dist-info → generalmanager-0.20.0.dist-info}/METADATA +1 -1
  56. generalmanager-0.20.0.dist-info/RECORD +78 -0
  57. generalmanager-0.19.1.dist-info/RECORD +0 -77
  58. /general_manager/measurement/{measurementField.py → measurement_field.py} +0 -0
  59. /general_manager/permission/{fileBasedPermission.py → file_based_permission.py} +0 -0
  60. /general_manager/utils/{argsToKwargs.py → args_to_kwargs.py} +0 -0
  61. /general_manager/utils/{formatString.py → format_string.py} +0 -0
  62. {generalmanager-0.19.1.dist-info → generalmanager-0.20.0.dist-info}/WHEEL +0 -0
  63. {generalmanager-0.19.1.dist-info → generalmanager-0.20.0.dist-info}/licenses/LICENSE +0 -0
  64. {generalmanager-0.19.1.dist-info → generalmanager-0.20.0.dist-info}/top_level.txt +0 -0
@@ -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.cache.cacheTracker import DependencyTracker
42
- from general_manager.cache.dependencyIndex import Dependency
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.bucket.baseBucket import Bucket
45
- from general_manager.interface.baseInterface import InterfaceBase
46
- from general_manager.manager.generalManager import GeneralManager
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.basePermission import BasePermission
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 getReadPermissionFilter(
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
- ).getPermissionFilter()
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 createGraphqlMutation(cls, generalManagerClass: type[GeneralManager]) -> None:
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.generateCreateMutationClass(
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.generateUpdateMutationClass(
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.generateDeleteMutationClass(
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 createGraphqlInterface(cls, generalManagerClass: Type[GeneralManager]) -> None:
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.getAttributeTypes().items():
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._mapFieldToGrapheneRead(field_type, field_name)
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._createResolver(field_name, field_type)
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.getGraphQLProperties().items():
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._mapFieldToGrapheneBaseType(
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._mapFieldToGrapheneRead(resolved_type, attr_name)
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._createResolver(
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._addQueriesToSchema(graphene_type, generalManagerClass)
431
- cls._addSubscriptionField(graphene_type, generalManagerClass)
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 _sortByOptions(
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.getAttributeTypes().items():
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.getGraphQLProperties().items():
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 _getFilterOptions(
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._mapFieldToGrapheneRead(attribute_type, attribute_name),
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._mapFieldToGrapheneRead(
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._mapFieldToGrapheneBaseType(attribute_type)
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._mapFieldToGrapheneRead(
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 _createFilterOptions(
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.getAttributeTypes().items():
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._getFilterOptions(attr_type, attr_name)
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.getGraphQLProperties().items():
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._getFilterOptions(prop_type, prop_name)
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 _mapFieldToGrapheneRead(field_type: type, field_name: str) -> Any:
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._createFilterOptions(field_type)
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._sortByOptions(field_type)
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._getOrCreatePageType(
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._mapFieldToGrapheneBaseType(field_type)()
689
+ return GraphQL._map_field_to_graphene_base_type(field_type)()
645
690
 
646
691
  @staticmethod
647
- def _mapFieldToGrapheneBaseType(field_type: type) -> Type[Any]:
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 _parseInput(input_val: dict[str, Any] | str | None) -> dict[str, Any]:
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 _applyQueryParameters(
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._parseInput(filter_input)
771
+ filters = GraphQL._parse_input(filter_input)
727
772
  if filters:
728
773
  queryset = queryset.filter(**filters)
729
774
 
730
- excludes = GraphQL._parseInput(exclude_input)
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 _applyPermissionFilters(
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 = getReadPermissionFilter(general_manager_class, info)
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 _checkReadPermission(
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).checkPermission(
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 _createListResolver(
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._applyPermissionFilters(base_queryset, manager_class, info)
831
- qs = GraphQL._applyQueryParameters(qs, filter, exclude, sort_by, reverse)
832
- qs = GraphQL._applyGrouping(qs, group_by)
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._applyPagination(qs, page, page_size)
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 _applyPagination(
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 _applyGrouping(
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 _createMeasurementResolver(field_name: str) -> Callable[..., Any]:
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._checkReadPermission(self, info, field_name):
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 _createNormalResolver(field_name: str) -> Callable[..., Any]:
965
+ def _create_normal_resolver(field_name: str) -> Callable[..., Any]:
921
966
  """
922
- Erzeugt einen Resolver für Standardfelder (keine Listen, keine Measurements).
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._checkReadPermission(self, info, field_name):
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 _createResolver(cls, field_name: str, field_type: type) -> Callable[..., Any]:
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._createListResolver(
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._createMeasurementResolver(field_name)
945
- return cls._createNormalResolver(field_name)
989
+ return cls._create_measurement_resolver(field_name)
990
+ return cls._create_normal_resolver(field_name)
946
991
 
947
992
  @classmethod
948
- def _getOrCreatePageType(
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 _buildIdentificationArguments(
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._mapFieldToGrapheneBaseType(input_field.type)
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 _addQueriesToSchema(
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._createFilterOptions(generalManagerClass)
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._sortByOptions(generalManagerClass)
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._getOrCreatePageType(
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._createListResolver(_all_items, generalManagerClass)
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._buildIdentificationArguments(generalManagerClass)
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.getGraphQLProperties()
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.getGraphQLProperties().keys())
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
- if name in available_properties:
1189
- property_names.add(name)
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 _addSubscriptionField(
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._buildIdentificationArguments(generalManagerClass)
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 createWriteFields(cls, interface_cls: InterfaceBase) -> dict[str, Any]:
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.getAttributeTypes().items():
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._mapFieldToGrapheneBaseType(typ)
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 generateCreateMutationClass(
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._handleGraphQLError(error) from error
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.createWriteFields(
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 generateUpdateMutationClass(
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._handleGraphQLError(MissingManagerIdentifierError())
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._handleGraphQLError(error) from error
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.createWriteFields(
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 generateDeleteMutationClass(
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._handleGraphQLError(MissingManagerIdentifierError())
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._handleGraphQLError(error) from error
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.createWriteFields(
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 _handleGraphQLError(error: Exception) -> GraphQLError:
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
- str(error),
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
- str(error),
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
- str(error),
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)