GeneralManager 0.14.1__py3-none-any.whl → 0.15.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.
Files changed (62) hide show
  1. general_manager/__init__.py +49 -0
  2. general_manager/api/__init__.py +36 -0
  3. general_manager/api/graphql.py +92 -43
  4. general_manager/api/mutation.py +35 -10
  5. general_manager/api/property.py +26 -3
  6. general_manager/apps.py +23 -16
  7. general_manager/bucket/__init__.py +32 -0
  8. general_manager/bucket/baseBucket.py +76 -64
  9. general_manager/bucket/calculationBucket.py +188 -108
  10. general_manager/bucket/databaseBucket.py +130 -49
  11. general_manager/bucket/groupBucket.py +113 -60
  12. general_manager/cache/__init__.py +38 -0
  13. general_manager/cache/cacheDecorator.py +29 -17
  14. general_manager/cache/cacheTracker.py +34 -15
  15. general_manager/cache/dependencyIndex.py +117 -33
  16. general_manager/cache/modelDependencyCollector.py +17 -8
  17. general_manager/cache/signals.py +17 -6
  18. general_manager/factory/__init__.py +34 -5
  19. general_manager/factory/autoFactory.py +57 -60
  20. general_manager/factory/factories.py +39 -14
  21. general_manager/factory/factoryMethods.py +38 -1
  22. general_manager/interface/__init__.py +36 -0
  23. general_manager/interface/baseInterface.py +71 -27
  24. general_manager/interface/calculationInterface.py +18 -10
  25. general_manager/interface/databaseBasedInterface.py +102 -71
  26. general_manager/interface/databaseInterface.py +66 -20
  27. general_manager/interface/models.py +10 -4
  28. general_manager/interface/readOnlyInterface.py +44 -30
  29. general_manager/manager/__init__.py +36 -3
  30. general_manager/manager/generalManager.py +73 -47
  31. general_manager/manager/groupManager.py +72 -17
  32. general_manager/manager/input.py +23 -15
  33. general_manager/manager/meta.py +53 -53
  34. general_manager/measurement/__init__.py +37 -2
  35. general_manager/measurement/measurement.py +135 -58
  36. general_manager/measurement/measurementField.py +161 -61
  37. general_manager/permission/__init__.py +32 -1
  38. general_manager/permission/basePermission.py +29 -12
  39. general_manager/permission/managerBasedPermission.py +32 -26
  40. general_manager/permission/mutationPermission.py +32 -3
  41. general_manager/permission/permissionChecks.py +9 -1
  42. general_manager/permission/permissionDataManager.py +49 -15
  43. general_manager/permission/utils.py +14 -3
  44. general_manager/rule/__init__.py +27 -1
  45. general_manager/rule/handler.py +90 -5
  46. general_manager/rule/rule.py +40 -27
  47. general_manager/utils/__init__.py +44 -2
  48. general_manager/utils/argsToKwargs.py +17 -9
  49. general_manager/utils/filterParser.py +29 -30
  50. general_manager/utils/formatString.py +2 -0
  51. general_manager/utils/jsonEncoder.py +14 -1
  52. general_manager/utils/makeCacheKey.py +18 -12
  53. general_manager/utils/noneToZero.py +8 -6
  54. general_manager/utils/pathMapping.py +92 -29
  55. general_manager/utils/public_api.py +49 -0
  56. general_manager/utils/testing.py +135 -69
  57. {generalmanager-0.14.1.dist-info → generalmanager-0.15.0.dist-info}/METADATA +10 -2
  58. generalmanager-0.15.0.dist-info/RECORD +62 -0
  59. generalmanager-0.14.1.dist-info/RECORD +0 -58
  60. {generalmanager-0.14.1.dist-info → generalmanager-0.15.0.dist-info}/WHEEL +0 -0
  61. {generalmanager-0.14.1.dist-info → generalmanager-0.15.0.dist-info}/licenses/LICENSE +0 -0
  62. {generalmanager-0.14.1.dist-info → generalmanager-0.15.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,49 @@
1
+ """Convenience access to GeneralManager core components."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from general_manager.utils.public_api import build_module_dir, resolve_export
8
+
9
+ __all__ = [
10
+ "GraphQL",
11
+ "GeneralManager",
12
+ "GeneralManagerMeta",
13
+ "Input",
14
+ "graphQlProperty",
15
+ "graphQlMutation",
16
+ "Bucket",
17
+ "DatabaseBucket",
18
+ "CalculationBucket",
19
+ "GroupBucket",
20
+ ]
21
+
22
+ _MODULE_MAP = {
23
+ "GraphQL": ("general_manager.api.graphql", "GraphQL"),
24
+ "graphQlProperty": ("general_manager.api.property", "graphQlProperty"),
25
+ "graphQlMutation": ("general_manager.api.mutation", "graphQlMutation"),
26
+ "GeneralManager": ("general_manager.manager.generalManager", "GeneralManager"),
27
+ "GeneralManagerMeta": ("general_manager.manager.meta", "GeneralManagerMeta"),
28
+ "Input": ("general_manager.manager.input", "Input"),
29
+ "Bucket": ("general_manager.bucket.baseBucket", "Bucket"),
30
+ "DatabaseBucket": ("general_manager.bucket.databaseBucket", "DatabaseBucket"),
31
+ "CalculationBucket": (
32
+ "general_manager.bucket.calculationBucket",
33
+ "CalculationBucket",
34
+ ),
35
+ "GroupBucket": ("general_manager.bucket.groupBucket", "GroupBucket"),
36
+ }
37
+
38
+
39
+ def __getattr__(name: str) -> Any:
40
+ return resolve_export(
41
+ name,
42
+ module_all=__all__,
43
+ module_map=_MODULE_MAP,
44
+ module_globals=globals(),
45
+ )
46
+
47
+
48
+ def __dir__() -> list[str]:
49
+ return build_module_dir(module_all=__all__, module_globals=globals())
@@ -0,0 +1,36 @@
1
+ """GraphQL helpers for GeneralManager."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from general_manager.utils.public_api import build_module_dir, resolve_export
8
+
9
+ __all__ = [
10
+ "GraphQL",
11
+ "MeasurementType",
12
+ "MeasurementScalar",
13
+ "graphQlProperty",
14
+ "graphQlMutation",
15
+ ]
16
+
17
+ _MODULE_MAP = {
18
+ "GraphQL": ("general_manager.api.graphql", "GraphQL"),
19
+ "MeasurementType": ("general_manager.api.graphql", "MeasurementType"),
20
+ "MeasurementScalar": ("general_manager.api.graphql", "MeasurementScalar"),
21
+ "graphQlProperty": ("general_manager.api.property", "graphQlProperty"),
22
+ "graphQlMutation": ("general_manager.api.mutation", "graphQlMutation"),
23
+ }
24
+
25
+
26
+ def __getattr__(name: str) -> Any:
27
+ return resolve_export(
28
+ name,
29
+ module_all=__all__,
30
+ module_map=_MODULE_MAP,
31
+ module_globals=globals(),
32
+ )
33
+
34
+
35
+ def __dir__() -> list[str]:
36
+ return build_module_dir(module_all=__all__, module_globals=globals())
@@ -1,5 +1,7 @@
1
+ """GraphQL schema utilities for exposing GeneralManager models via Graphene."""
2
+
1
3
  from __future__ import annotations
2
- import graphene
4
+ import graphene # type: ignore[import]
3
5
  from typing import (
4
6
  Any,
5
7
  Callable,
@@ -73,9 +75,14 @@ def getReadPermissionFilter(
73
75
  info: GraphQLResolveInfo,
74
76
  ) -> list[tuple[dict[str, Any], dict[str, Any]]]:
75
77
  """
76
- Returns permission-based filter and exclude constraints for querying instances of a manager class.
78
+ Return permission-derived filter and exclude pairs for the given manager class.
79
+
80
+ Parameters:
81
+ generalManagerClass (GeneralManagerMeta): Manager class being queried.
82
+ info (GraphQLResolveInfo): GraphQL resolver info containing the request user.
77
83
 
78
- For the given manager class and user context, retrieves a list of (filter, exclude) dictionary pairs that represent the read access restrictions to be applied to queries. Returns an empty list if no permission class is defined.
84
+ Returns:
85
+ list[tuple[dict[str, Any], dict[str, Any]]]: List of ``(filter, exclude)`` mappings.
79
86
  """
80
87
  filters = []
81
88
  PermissionClass: type[BasePermission] | None = getattr(
@@ -93,10 +100,7 @@ def getReadPermissionFilter(
93
100
 
94
101
 
95
102
  class GraphQL:
96
- """
97
- Baut die GraphQL-Oberfläche auf und erstellt Resolver-Funktionen
98
- dynamisch für die angegebene GeneralManager-Klasse.
99
- """
103
+ """Static helper that builds GraphQL types, queries, and mutations for managers."""
100
104
 
101
105
  _query_class: type[graphene.ObjectType] | None = None
102
106
  _mutation_class: type[graphene.ObjectType] | None = None
@@ -109,16 +113,17 @@ class GraphQL:
109
113
  @classmethod
110
114
  def createGraphqlMutation(cls, generalManagerClass: type[GeneralManager]) -> None:
111
115
  """
112
- Generates and registers GraphQL mutation classes for create, update, and delete operations on the specified manager class if its interface provides custom implementations.
116
+ Register create, update, and delete mutations for ``generalManagerClass``.
113
117
 
114
- For each supported mutation, a corresponding GraphQL mutation class is created and added to the mutation registry, enabling dynamic mutation support in the schema.
118
+ Parameters:
119
+ generalManagerClass (type[GeneralManager]): Manager class whose interface drives mutation generation.
115
120
  """
116
121
 
117
122
  interface_cls: InterfaceBase | None = getattr(
118
123
  generalManagerClass, "Interface", None
119
124
  )
120
125
  if not interface_cls:
121
- return
126
+ return None
122
127
 
123
128
  default_return_values = {
124
129
  "success": graphene.Boolean(),
@@ -147,15 +152,16 @@ class GraphQL:
147
152
  @classmethod
148
153
  def createGraphqlInterface(cls, generalManagerClass: GeneralManagerMeta) -> None:
149
154
  """
150
- Creates and registers a GraphQL ObjectType for a GeneralManager subclass.
155
+ Build and register a Graphene ``ObjectType`` for the supplied manager class.
151
156
 
152
- Introspects the manager's interface and GraphQLProperty fields, maps them to Graphene fields with appropriate resolvers, registers the resulting type in the internal registry, and adds corresponding query fields to the schema.
157
+ Parameters:
158
+ generalManagerClass (GeneralManagerMeta): Manager class whose attributes drive field generation.
153
159
  """
154
160
  interface_cls: InterfaceBase | None = getattr(
155
161
  generalManagerClass, "Interface", None
156
162
  )
157
163
  if not interface_cls:
158
- return
164
+ return None
159
165
 
160
166
  graphene_type_name = f"{generalManagerClass.__name__}Type"
161
167
  fields: dict[str, Any] = {}
@@ -217,10 +223,13 @@ class GraphQL:
217
223
  generalManagerClass: GeneralManagerMeta,
218
224
  ) -> type[graphene.Enum] | None:
219
225
  """
220
- Creates a Graphene Enum type representing sortable fields for a given GeneralManager class.
226
+ Build an enum of sortable fields for the provided manager class.
227
+
228
+ Parameters:
229
+ generalManagerClass (GeneralManagerMeta): Manager class being inspected.
221
230
 
222
231
  Returns:
223
- A Graphene Enum type with options for each sortable attribute, including separate entries for the value and unit of Measurement fields. Returns None if there are no sortable fields.
232
+ type[graphene.Enum] | None: Enum of sortable fields, or ``None`` when no options exist.
224
233
  """
225
234
  sort_options = []
226
235
  for (
@@ -264,6 +273,16 @@ class GraphQL:
264
273
  None,
265
274
  None,
266
275
  ]:
276
+ """
277
+ Yield filter field names and Graphene types for a given attribute.
278
+
279
+ Parameters:
280
+ attribute_type (type): Python type declared for the attribute.
281
+ attribute_name (str): Name of the attribute.
282
+
283
+ Yields:
284
+ tuple[str, Graphene type | None]: Filter name and corresponding Graphene type.
285
+ """
267
286
  number_options = ["exact", "gt", "gte", "lt", "lte"]
268
287
  string_options = [
269
288
  "exact",
@@ -306,22 +325,20 @@ class GraphQL:
306
325
  field_type: GeneralManagerMeta,
307
326
  ) -> type[graphene.InputObjectType] | None:
308
327
  """
309
- Dynamically generates a Graphene InputObjectType for filtering fields of a GeneralManager subclass.
310
-
311
- Creates filter fields for each attribute based on its type, supporting numeric and string filter operations, and specialized handling for Measurement attributes. Returns the generated InputObjectType, or None if no applicable filter fields exist.
328
+ Create a Graphene ``InputObjectType`` for filters on ``field_type``.
312
329
 
313
330
  Parameters:
314
- field_type (GeneralManagerMeta): The manager class whose attributes are used to build filter fields.
331
+ field_type (GeneralManagerMeta): Manager class whose attributes drive filter generation.
315
332
 
316
333
  Returns:
317
- type[graphene.InputObjectType] | None: The generated filter input type, or None if no filter fields are applicable.
334
+ type[graphene.InputObjectType] | None: Input type containing filter fields, or ``None`` if not applicable.
318
335
  """
319
336
 
320
337
  graphene_filter_type_name = f"{field_type.__name__}FilterType"
321
338
  if graphene_filter_type_name in GraphQL.graphql_filter_type_registry:
322
339
  return GraphQL.graphql_filter_type_registry[graphene_filter_type_name]
323
340
 
324
- filter_fields = {}
341
+ filter_fields: dict[str, Any] = {}
325
342
  for attr_name, attr_info in field_type.Interface.getAttributeTypes().items():
326
343
  attr_type = attr_info["type"]
327
344
  filter_fields = {
@@ -360,9 +377,14 @@ class GraphQL:
360
377
  @staticmethod
361
378
  def _mapFieldToGrapheneRead(field_type: type, field_name: str) -> Any:
362
379
  """
363
- Maps a Python field type and name to the appropriate Graphene field for GraphQL schema generation.
380
+ Map a field type and name to the appropriate Graphene field for reads.
364
381
 
365
- For `Measurement` fields, returns a Graphene field with an optional `target_unit` argument. For `GeneralManager` subclasses, returns a paginated field with filtering, exclusion, sorting, pagination, and grouping arguments if the field name ends with `_list`; otherwise, returns a single object field. For all other types, returns the corresponding Graphene scalar field.
382
+ Parameters:
383
+ field_type (type): Python type declared on the interface.
384
+ field_name (str): Attribute name being exposed.
385
+
386
+ Returns:
387
+ Any: Graphene field or type configured for the attribute.
366
388
  """
367
389
  if issubclass(field_type, Measurement):
368
390
  return graphene.Field(MeasurementType, target_unit=graphene.String())
@@ -398,7 +420,13 @@ class GraphQL:
398
420
  @staticmethod
399
421
  def _mapFieldToGrapheneBaseType(field_type: type) -> Type[Any]:
400
422
  """
401
- Ordnet einen Python-Typ einem entsprechenden Graphene-Feld zu.
423
+ Map a Python type to the corresponding Graphene scalar/class.
424
+
425
+ Parameters:
426
+ field_type (type): Python type declared on the interface.
427
+
428
+ Returns:
429
+ Type[Any]: Graphene scalar or type implementing the field.
402
430
  """
403
431
  if issubclass(field_type, dict):
404
432
  raise TypeError("GraphQL does not support dict fields")
@@ -422,7 +450,13 @@ class GraphQL:
422
450
  @staticmethod
423
451
  def _parseInput(input_val: dict[str, Any] | str | None) -> dict[str, Any]:
424
452
  """
425
- Wandelt einen als JSON-String oder Dict gelieferten Filter/Exclude-Parameter in ein Dict um.
453
+ Normalise filter/exclude input into a dictionary.
454
+
455
+ Parameters:
456
+ input_val (dict[str, Any] | str | None): Raw filter/exclude value.
457
+
458
+ Returns:
459
+ dict[str, Any]: Parsed dictionary suitable for queryset filtering.
426
460
  """
427
461
  if input_val is None:
428
462
  return {}
@@ -474,10 +508,15 @@ class GraphQL:
474
508
  info: GraphQLResolveInfo,
475
509
  ) -> Bucket:
476
510
  """
477
- Applies permission-based filters to a queryset according to the permission interface of the given manager class.
511
+ Apply permission-based filters to ``queryset`` for the current user.
512
+
513
+ Parameters:
514
+ queryset (Bucket): Queryset being filtered.
515
+ general_manager_class (type[GeneralManager]): Manager class providing permissions.
516
+ info (GraphQLResolveInfo): Resolver info containing the request user.
478
517
 
479
518
  Returns:
480
- A queryset containing only the items allowed by the user's read permissions. If no permission filters are defined, returns the original queryset unchanged.
519
+ Bucket: Queryset constrained by read permissions.
481
520
  """
482
521
  permission_filters = getReadPermissionFilter(general_manager_class, info)
483
522
  if not permission_filters:
@@ -494,9 +533,7 @@ class GraphQL:
494
533
  def _checkReadPermission(
495
534
  instance: GeneralManager, info: GraphQLResolveInfo, field_name: str
496
535
  ) -> bool:
497
- """
498
- Überprüft, ob der Benutzer Lesezugriff auf das jeweilige Feld hat.
499
- """
536
+ """Return True if the user may read ``field_name`` on ``instance``."""
500
537
  PermissionClass: type[BasePermission] | None = getattr(
501
538
  instance, "Permission", None
502
539
  )
@@ -511,9 +548,14 @@ class GraphQL:
511
548
  base_getter: Callable[[Any], Any], fallback_manager_class: type[GeneralManager]
512
549
  ) -> Callable[..., Any]:
513
550
  """
514
- Creates a resolver for GraphQL list fields that returns paginated, filtered, sorted, and optionally grouped results with permission checks.
551
+ Build a resolver for list fields applying filters, permissions, and paging.
515
552
 
516
- The generated resolver applies permission-based filtering, user-specified filters and exclusions, sorting, grouping, and pagination to the list field. It returns a dictionary containing the resulting items and pagination metadata.
553
+ Parameters:
554
+ base_getter (Callable[[Any], Any]): Callable returning the base queryset.
555
+ fallback_manager_class (type[GeneralManager]): Manager used when ``base_getter`` returns ``None``.
556
+
557
+ Returns:
558
+ Callable[..., Any]: Resolver function compatible with Graphene.
517
559
  """
518
560
 
519
561
  def resolver(
@@ -691,9 +733,11 @@ class GraphQL:
691
733
  cls, graphene_type: type, generalManagerClass: GeneralManagerMeta
692
734
  ) -> None:
693
735
  """
694
- Adds paginated list and single-item query fields for a GeneralManager subclass to the GraphQL schema.
736
+ Register list and detail query fields for ``generalManagerClass``.
695
737
 
696
- The list query field enables filtering, exclusion, sorting, pagination, and grouping, returning a paginated result with metadata. The single-item query field retrieves an instance by its identification fields. Both queries are registered with their respective resolvers.
738
+ Parameters:
739
+ graphene_type (type): Graphene ``ObjectType`` representing the manager.
740
+ generalManagerClass (GeneralManagerMeta): Manager class being exposed.
697
741
  """
698
742
  if not issubclass(generalManagerClass, GeneralManager):
699
743
  raise TypeError(
@@ -701,7 +745,7 @@ class GraphQL:
701
745
  )
702
746
 
703
747
  if not hasattr(cls, "_query_fields"):
704
- cls._query_fields: dict[str, Any] = {}
748
+ cls._query_fields = cast(dict[str, Any], {})
705
749
 
706
750
  # resolver and field for the list query
707
751
  list_field_name = f"{generalManagerClass.__name__.lower()}_list"
@@ -761,12 +805,13 @@ class GraphQL:
761
805
  @classmethod
762
806
  def createWriteFields(cls, interface_cls: InterfaceBase) -> dict[str, Any]:
763
807
  """
764
- Generates a dictionary of Graphene input fields for mutations based on the attributes of the provided interface class.
808
+ Generate Graphene input fields for writable interface attributes.
765
809
 
766
- Skips system-managed and derived attributes. For attributes referencing `GeneralManager` subclasses, uses an ID or list of IDs as appropriate. Other types are mapped to their corresponding Graphene scalar types. Each field is annotated with an `editable` attribute. Always includes an optional `history_comment` field marked as editable.
810
+ Parameters:
811
+ interface_cls (InterfaceBase): Interface whose attributes drive the input field map.
767
812
 
768
813
  Returns:
769
- dict[str, Any]: Mapping of attribute names to Graphene input fields for mutation arguments.
814
+ dict[str, Any]: Mapping of attribute names to Graphene field definitions.
770
815
  """
771
816
  fields: dict[str, Any] = {}
772
817
 
@@ -827,7 +872,7 @@ class GraphQL:
827
872
  generalManagerClass, "Interface", None
828
873
  )
829
874
  if not interface_cls:
830
- return
875
+ return None
831
876
 
832
877
  def create_mutation(
833
878
  self,
@@ -898,7 +943,7 @@ class GraphQL:
898
943
  generalManagerClass, "Interface", None
899
944
  )
900
945
  if not interface_cls:
901
- return
946
+ return None
902
947
 
903
948
  def update_mutation(
904
949
  self,
@@ -975,7 +1020,7 @@ class GraphQL:
975
1020
  generalManagerClass, "Interface", None
976
1021
  )
977
1022
  if not interface_cls:
978
- return
1023
+ return None
979
1024
 
980
1025
  def delete_mutation(
981
1026
  self,
@@ -1030,9 +1075,13 @@ class GraphQL:
1030
1075
  @staticmethod
1031
1076
  def _handleGraphQLError(error: Exception) -> None:
1032
1077
  """
1033
- Raises a GraphQLError with a specific error code based on the exception type.
1078
+ Raise a ``GraphQLError`` with a code based on the exception type.
1079
+
1080
+ Parameters:
1081
+ error (Exception): Exception raised during mutation execution.
1034
1082
 
1035
- PermissionError results in "PERMISSION_DENIED", ValueError or ValidationError in "BAD_USER_INPUT", and all other exceptions in "INTERNAL_SERVER_ERROR".
1083
+ Raises:
1084
+ GraphQLError: Error with an appropriate ``extensions['code']`` value.
1036
1085
  """
1037
1086
  if isinstance(error, PermissionError):
1038
1087
  raise GraphQLError(
@@ -1,15 +1,21 @@
1
+ """Decorator utilities for building GraphQL mutations from manager functions."""
2
+
1
3
  import inspect
2
4
  from typing import (
3
- get_type_hints,
5
+ Callable,
4
6
  Optional,
7
+ TypeVar,
5
8
  Union,
6
9
  List,
7
10
  Tuple,
8
11
  get_origin,
9
12
  get_args,
10
13
  Type,
14
+ get_type_hints,
15
+ cast,
11
16
  )
12
- import graphene
17
+ import graphene # type: ignore[import]
18
+ from graphql import GraphQLResolveInfo
13
19
 
14
20
  from general_manager.api.graphql import GraphQL
15
21
  from general_manager.manager.generalManager import GeneralManager
@@ -19,7 +25,13 @@ from typing import TypeAliasType
19
25
  from general_manager.permission.mutationPermission import MutationPermission
20
26
 
21
27
 
22
- def graphQlMutation(_func=None, permission: Optional[Type[MutationPermission]] = None):
28
+ FuncT = TypeVar("FuncT", bound=Callable[..., object])
29
+
30
+
31
+ def graphQlMutation(
32
+ _func: FuncT | type[MutationPermission] | None = None,
33
+ permission: Optional[Type[MutationPermission]] = None,
34
+ ) -> FuncT | Callable[[FuncT], FuncT]:
23
35
  """
24
36
  Decorator that converts a function into a GraphQL mutation class for use with Graphene, automatically generating argument and output fields from the function's signature and type annotations.
25
37
 
@@ -39,11 +51,15 @@ def graphQlMutation(_func=None, permission: Optional[Type[MutationPermission]] =
39
51
  permission = _func
40
52
  _func = None
41
53
 
42
- def decorator(fn):
54
+ def decorator(fn: FuncT) -> FuncT:
43
55
  """
44
- Decorator that transforms a function into a GraphQL mutation class compatible with Graphene.
56
+ Transform ``fn`` into a Graphene-compatible mutation class.
57
+
58
+ Parameters:
59
+ fn (Callable[..., Any]): Resolver implementing the mutation behaviour.
45
60
 
46
- Analyzes the decorated function's signature and type hints to dynamically generate a mutation class with appropriate argument and output fields. Handles permission checks if a permission class is provided, manages mutation execution, and registers the mutation for use in the GraphQL API. On success, returns output fields and a `success` flag; on error, returns only `success=False`.
61
+ Returns:
62
+ Callable[..., Any]: Original function after registration.
47
63
  """
48
64
  sig = inspect.signature(fn)
49
65
  hints = get_type_hints(fn)
@@ -128,12 +144,21 @@ def graphQlMutation(_func=None, permission: Optional[Type[MutationPermission]] =
128
144
  )
129
145
 
130
146
  # Define mutate method
131
- def _mutate(root, info, **kwargs):
147
+ def _mutate(
148
+ root: object,
149
+ info: GraphQLResolveInfo,
150
+ **kwargs: object,
151
+ ) -> graphene.Mutation:
132
152
  """
133
- Handles the execution of a GraphQL mutation, including permission checks, result unpacking, and error handling.
153
+ Execute the mutation resolver, enforcing permissions and formatting output.
154
+
155
+ Parameters:
156
+ root: Graphene root object (unused).
157
+ info: GraphQL execution info passed by Graphene.
158
+ **kwargs: Mutation arguments provided by the client.
134
159
 
135
160
  Returns:
136
- An instance of the mutation class with output fields populated from the mutation result and a success status.
161
+ mutation_class: Instance populated with resolver results and a success flag.
137
162
  """
138
163
  if permission:
139
164
  permission.check(kwargs, info.context.user)
@@ -176,5 +201,5 @@ def graphQlMutation(_func=None, permission: Optional[Type[MutationPermission]] =
176
201
  return fn
177
202
 
178
203
  if _func is not None and inspect.isfunction(_func):
179
- return decorator(_func)
204
+ return decorator(cast(FuncT, _func))
180
205
  return decorator
@@ -1,3 +1,5 @@
1
+ """GraphQL-aware property descriptor used by GeneralManager classes."""
2
+
1
3
  from typing import Any, Callable, get_type_hints, overload, TypeVar
2
4
  import sys
3
5
 
@@ -5,6 +7,7 @@ T = TypeVar("T", bound=Callable[..., Any])
5
7
 
6
8
 
7
9
  class GraphQLProperty(property):
10
+ """Descriptor that exposes a property with GraphQL metadata and type hints."""
8
11
  sortable: bool
9
12
  filterable: bool
10
13
  query_annotation: Any | None
@@ -18,6 +21,16 @@ class GraphQLProperty(property):
18
21
  filterable: bool = False,
19
22
  query_annotation: Any | None = None,
20
23
  ) -> None:
24
+ """
25
+ Initialise the descriptor with GraphQL-specific configuration.
26
+
27
+ Parameters:
28
+ fget (Callable): Underlying resolver function.
29
+ doc (str | None): Optional documentation string.
30
+ sortable (bool): Whether the property participates in sorting.
31
+ filterable (bool): Whether the property participates in filtering.
32
+ query_annotation (Any | None): Optional annotation applied to querysets.
33
+ """
21
34
  super().__init__(fget, doc=doc)
22
35
  self.is_graphql_resolver = True
23
36
  self._owner: type | None = None
@@ -38,10 +51,12 @@ class GraphQLProperty(property):
38
51
  )
39
52
 
40
53
  def __set_name__(self, owner: type, name: str) -> None:
54
+ """Store the owning class and attribute name for later introspection."""
41
55
  self._owner = owner
42
56
  self._name = name
43
57
 
44
58
  def _try_resolve_type_hint(self) -> None:
59
+ """Resolve the return type hint of the wrapped resolver, if available."""
45
60
  if self._graphql_type_hint is not None:
46
61
  return
47
62
 
@@ -61,6 +76,7 @@ class GraphQLProperty(property):
61
76
 
62
77
  @property
63
78
  def graphql_type_hint(self) -> Any | None:
79
+ """Return the cached GraphQL type hint resolved from annotations."""
64
80
  if self._graphql_type_hint is None:
65
81
  self._try_resolve_type_hint()
66
82
  return self._graphql_type_hint
@@ -86,10 +102,17 @@ def graphQlProperty(
86
102
  ) -> GraphQLProperty | Callable[[T], GraphQLProperty]:
87
103
  from general_manager.cache.cacheDecorator import cached
88
104
 
89
- """Decorator to create a :class:`GraphQLProperty`.
105
+ """
106
+ Decorate a resolver to return a cached ``GraphQLProperty`` descriptor.
107
+
108
+ Parameters:
109
+ func (Callable[..., Any] | None): Resolver function when used without arguments.
110
+ sortable (bool): Whether the property can participate in sorting.
111
+ filterable (bool): Whether the property can be used in filtering.
112
+ query_annotation (Any | None): Optional queryset annotation callable or expression.
90
113
 
91
- It can be used without arguments or with optional configuration for
92
- filtering, sorting and queryset annotation.
114
+ Returns:
115
+ GraphQLProperty | Callable[[Callable[..., Any]], GraphQLProperty]: Decorated property or decorator factory.
93
116
  """
94
117
 
95
118
  def wrapper(f: Callable[..., Any]) -> GraphQLProperty: