GeneralManager 0.17.0__py3-none-any.whl → 0.19.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 (68) hide show
  1. general_manager/__init__.py +11 -1
  2. general_manager/_types/api.py +0 -1
  3. general_manager/_types/bucket.py +0 -1
  4. general_manager/_types/cache.py +0 -1
  5. general_manager/_types/factory.py +0 -1
  6. general_manager/_types/general_manager.py +0 -1
  7. general_manager/_types/interface.py +0 -1
  8. general_manager/_types/manager.py +0 -1
  9. general_manager/_types/measurement.py +0 -1
  10. general_manager/_types/permission.py +0 -1
  11. general_manager/_types/rule.py +0 -1
  12. general_manager/_types/utils.py +0 -1
  13. general_manager/api/__init__.py +13 -1
  14. general_manager/api/graphql.py +356 -221
  15. general_manager/api/graphql_subscription_consumer.py +81 -78
  16. general_manager/api/mutation.py +85 -23
  17. general_manager/api/property.py +39 -13
  18. general_manager/apps.py +188 -47
  19. general_manager/bucket/__init__.py +10 -1
  20. general_manager/bucket/calculationBucket.py +155 -53
  21. general_manager/bucket/databaseBucket.py +157 -45
  22. general_manager/bucket/groupBucket.py +133 -44
  23. general_manager/cache/__init__.py +10 -1
  24. general_manager/cache/cacheDecorator.py +3 -0
  25. general_manager/cache/dependencyIndex.py +143 -45
  26. general_manager/cache/signals.py +9 -2
  27. general_manager/factory/__init__.py +10 -1
  28. general_manager/factory/autoFactory.py +55 -13
  29. general_manager/factory/factories.py +110 -40
  30. general_manager/factory/factoryMethods.py +122 -34
  31. general_manager/interface/__init__.py +10 -1
  32. general_manager/interface/baseInterface.py +129 -36
  33. general_manager/interface/calculationInterface.py +35 -18
  34. general_manager/interface/databaseBasedInterface.py +71 -45
  35. general_manager/interface/databaseInterface.py +96 -38
  36. general_manager/interface/models.py +5 -5
  37. general_manager/interface/readOnlyInterface.py +94 -20
  38. general_manager/manager/__init__.py +10 -1
  39. general_manager/manager/generalManager.py +25 -16
  40. general_manager/manager/groupManager.py +20 -6
  41. general_manager/manager/meta.py +84 -16
  42. general_manager/measurement/__init__.py +10 -1
  43. general_manager/measurement/measurement.py +289 -95
  44. general_manager/measurement/measurementField.py +42 -31
  45. general_manager/permission/__init__.py +10 -1
  46. general_manager/permission/basePermission.py +120 -38
  47. general_manager/permission/managerBasedPermission.py +72 -21
  48. general_manager/permission/mutationPermission.py +14 -9
  49. general_manager/permission/permissionChecks.py +14 -12
  50. general_manager/permission/permissionDataManager.py +24 -11
  51. general_manager/permission/utils.py +34 -6
  52. general_manager/public_api_registry.py +36 -10
  53. general_manager/rule/__init__.py +10 -1
  54. general_manager/rule/handler.py +133 -44
  55. general_manager/rule/rule.py +178 -39
  56. general_manager/utils/__init__.py +10 -1
  57. general_manager/utils/argsToKwargs.py +34 -9
  58. general_manager/utils/filterParser.py +22 -7
  59. general_manager/utils/formatString.py +1 -0
  60. general_manager/utils/pathMapping.py +23 -15
  61. general_manager/utils/public_api.py +33 -2
  62. general_manager/utils/testing.py +31 -33
  63. {generalmanager-0.17.0.dist-info → generalmanager-0.19.0.dist-info}/METADATA +3 -1
  64. generalmanager-0.19.0.dist-info/RECORD +77 -0
  65. {generalmanager-0.17.0.dist-info → generalmanager-0.19.0.dist-info}/licenses/LICENSE +1 -1
  66. generalmanager-0.17.0.dist-info/RECORD +0 -77
  67. {generalmanager-0.17.0.dist-info → generalmanager-0.19.0.dist-info}/WHEEL +0 -0
  68. {generalmanager-0.17.0.dist-info → generalmanager-0.19.0.dist-info}/top_level.txt +0 -0
@@ -16,6 +16,7 @@ from typing import (
16
16
  Any,
17
17
  AsyncIterator,
18
18
  Callable,
19
+ ClassVar,
19
20
  Generator,
20
21
  Iterable,
21
22
  TYPE_CHECKING,
@@ -28,7 +29,12 @@ from typing import (
28
29
 
29
30
  import graphene # type: ignore[import]
30
31
  from graphql.language import ast
31
- from graphql.language.ast import FieldNode, FragmentSpreadNode, InlineFragmentNode, SelectionSetNode
32
+ from graphql.language.ast import (
33
+ FieldNode,
34
+ FragmentSpreadNode,
35
+ InlineFragmentNode,
36
+ SelectionSetNode,
37
+ )
32
38
  from asgiref.sync import async_to_sync
33
39
  from channels.layers import BaseChannelLayer, get_channel_layer
34
40
 
@@ -58,6 +64,87 @@ class SubscriptionEvent:
58
64
  action: str
59
65
 
60
66
 
67
+ class InvalidMeasurementValueError(TypeError):
68
+ """Raised when a scalar receives a value that is not a Measurement instance."""
69
+
70
+ def __init__(self, value: object) -> None:
71
+ """
72
+ Initialize the error raised when a scalar value is not a Measurement instance.
73
+
74
+ Parameters:
75
+ value (object): The value that failed validation; its type name is included in the exception message.
76
+ """
77
+ super().__init__(f"Expected Measurement, got {type(value).__name__}.")
78
+
79
+
80
+ class MissingChannelLayerError(RuntimeError):
81
+ """Raised when GraphQL subscriptions run without a configured channel layer."""
82
+
83
+ def __init__(self) -> None:
84
+ """
85
+ Indicates that Django Channels channel layer is not configured for GraphQL subscriptions.
86
+
87
+ Raised when subscription functionality requires a configured channel layer (e.g., CHANNEL_LAYERS) but none is present.
88
+ """
89
+ super().__init__(
90
+ "No channel layer configured. Configure CHANNEL_LAYERS to enable GraphQL subscriptions."
91
+ )
92
+
93
+
94
+ class UnsupportedGraphQLFieldTypeError(TypeError):
95
+ """Raised when attempting to map an unsupported Python type to GraphQL."""
96
+
97
+ def __init__(self, field_type: type) -> None:
98
+ """
99
+ Exception raised when a Python `dict` type is encountered while mapping a field to GraphQL, which is not supported.
100
+
101
+ Parameters:
102
+ field_type (type): The offending Python type that was provided for the field. The exception message includes this type's `__name__`.
103
+ """
104
+ super().__init__(
105
+ f"GraphQL does not support dict fields (received {field_type.__name__})."
106
+ )
107
+
108
+
109
+ class InvalidGeneralManagerClassError(TypeError):
110
+ """Raised when a non-GeneralManager subclass is used in the GraphQL registry."""
111
+
112
+ def __init__(self, received_class: type) -> None:
113
+ """
114
+ Initialize the exception indicating the provided class is not a GeneralManager subclass.
115
+
116
+ Parameters:
117
+ received_class (type): The class that was supplied but is not a subclass of GeneralManager.
118
+ """
119
+ super().__init__(
120
+ f"{received_class.__name__} must be a subclass of GeneralManager to create a GraphQL interface."
121
+ )
122
+
123
+
124
+ class MissingManagerIdentifierError(ValueError):
125
+ """Raised when a GraphQL mutation is missing the required manager identifier."""
126
+
127
+ def __init__(self) -> None:
128
+ """
129
+ Initialize the exception indicating a required manager identifier is missing.
130
+
131
+ This exception instance carries the default message "id is required.".
132
+ """
133
+ super().__init__("id is required.")
134
+
135
+
136
+ HANDLED_MANAGER_ERRORS: tuple[type[Exception], ...] = (
137
+ PermissionError,
138
+ ValidationError,
139
+ ValueError,
140
+ LookupError,
141
+ TypeError,
142
+ AttributeError,
143
+ GraphQLError,
144
+ RuntimeError,
145
+ )
146
+
147
+
61
148
  class MeasurementType(graphene.ObjectType):
62
149
  value = graphene.Float()
63
150
  unit = graphene.String()
@@ -70,8 +157,20 @@ class MeasurementScalar(graphene.Scalar):
70
157
 
71
158
  @staticmethod
72
159
  def serialize(value: Measurement) -> str:
160
+ """
161
+ Convert a Measurement to its string representation.
162
+
163
+ Parameters:
164
+ value (Measurement): Measurement to serialize.
165
+
166
+ Returns:
167
+ str: String representation of the given Measurement.
168
+
169
+ Raises:
170
+ InvalidMeasurementValueError: If `value` is not a Measurement instance.
171
+ """
73
172
  if not isinstance(value, Measurement):
74
- raise TypeError(f"Expected Measurement, got {type(value)}")
173
+ raise InvalidMeasurementValueError(value)
75
174
  return str(value)
76
175
 
77
176
  @staticmethod
@@ -98,11 +197,11 @@ def getReadPermissionFilter(
98
197
  ) -> list[tuple[dict[str, Any], dict[str, Any]]]:
99
198
  """
100
199
  Produce a list of permission-derived filter and exclude mappings for queries against a manager class.
101
-
200
+
102
201
  Parameters:
103
202
  generalManagerClass (Type[GeneralManager]): Manager class to derive permission filters for.
104
203
  info (GraphQLResolveInfo): GraphQL resolver info whose context provides the current user.
105
-
204
+
106
205
  Returns:
107
206
  list[tuple[dict[str, Any], dict[str, Any]]]: A list of `(filter, exclude)` tuples where each `filter` and `exclude` is a mapping of query constraints produced by the manager's Permission class.
108
207
  """
@@ -124,45 +223,43 @@ def getReadPermissionFilter(
124
223
  class GraphQL:
125
224
  """Static helper that builds GraphQL types, queries, and mutations for managers."""
126
225
 
127
- _query_class: type[graphene.ObjectType] | None = None
128
- _mutation_class: type[graphene.ObjectType] | None = None
129
- _subscription_class: type[graphene.ObjectType] | None = None
130
- _mutations: dict[str, Any] = {}
131
- _query_fields: dict[str, Any] = {}
132
- _subscription_fields: dict[str, Any] = {}
133
- _page_type_registry: dict[str, type[graphene.ObjectType]] = {}
134
- _subscription_payload_registry: dict[str, type[graphene.ObjectType]] = {}
135
- graphql_type_registry: dict[str, type] = {}
136
- graphql_filter_type_registry: dict[str, type] = {}
137
- manager_registry: dict[str, type[GeneralManager]] = {}
138
- _schema: graphene.Schema | None = None
226
+ _query_class: ClassVar[type[graphene.ObjectType] | None] = None
227
+ _mutation_class: ClassVar[type[graphene.ObjectType] | None] = None
228
+ _subscription_class: ClassVar[type[graphene.ObjectType] | None] = None
229
+ _mutations: ClassVar[dict[str, Any]] = {}
230
+ _query_fields: ClassVar[dict[str, Any]] = {}
231
+ _subscription_fields: ClassVar[dict[str, Any]] = {}
232
+ _page_type_registry: ClassVar[dict[str, type[graphene.ObjectType]]] = {}
233
+ _subscription_payload_registry: ClassVar[dict[str, type[graphene.ObjectType]]] = {}
234
+ graphql_type_registry: ClassVar[dict[str, type]] = {}
235
+ graphql_filter_type_registry: ClassVar[dict[str, type]] = {}
236
+ manager_registry: ClassVar[dict[str, type[GeneralManager]]] = {}
237
+ _schema: ClassVar[graphene.Schema | None] = None
139
238
 
140
239
  @staticmethod
141
240
  def _get_channel_layer(strict: bool = False) -> BaseChannelLayer | None:
142
241
  """
143
- Return the configured channel layer used for GraphQL subscriptions.
144
-
242
+ Retrieve the configured channel layer for GraphQL subscriptions.
243
+
145
244
  Parameters:
146
- strict (bool): If True, raise an error when no channel layer is configured.
147
-
245
+ strict (bool): When True, raise MissingChannelLayerError if no channel layer is configured.
246
+
148
247
  Returns:
149
- BaseChannelLayer | None: The channel layer instance if configured, otherwise `None`.
150
-
248
+ BaseChannelLayer | None: The configured channel layer instance if available, otherwise None.
249
+
151
250
  Raises:
152
- RuntimeError: If `strict` is True and no channel layer is configured.
251
+ MissingChannelLayerError: If `strict` is True and no channel layer is configured.
153
252
  """
154
253
  layer = cast(BaseChannelLayer | None, get_channel_layer())
155
254
  if layer is None and strict:
156
- raise RuntimeError(
157
- "No channel layer configured. Configure CHANNEL_LAYERS to enable GraphQL subscriptions."
158
- )
255
+ raise MissingChannelLayerError()
159
256
  return layer
160
257
 
161
258
  @classmethod
162
259
  def get_schema(cls) -> graphene.Schema | None:
163
260
  """
164
261
  Get the currently configured Graphene schema for the GraphQL registry.
165
-
262
+
166
263
  Returns:
167
264
  The active `graphene.Schema` instance, or `None` if no schema has been created.
168
265
  """
@@ -173,14 +270,14 @@ class GraphQL:
173
270
  manager_class: type[GeneralManager], identification: dict[str, Any]
174
271
  ) -> str:
175
272
  """
176
- Build a consistent channel group name for subscriptions tied to a manager and its identification.
177
-
273
+ Builds a deterministic channel group name for subscription events for a specific manager instance.
274
+
178
275
  Parameters:
179
- manager_class (type[GeneralManager]): Manager class used to namespace the group.
180
- identification (dict[str, Any]): Mapping of identifying fields for a manager instance; will be JSON-normalized and included in the group name.
181
-
276
+ manager_class (type[GeneralManager]): GeneralManager subclass used to namespace the group.
277
+ identification (dict[str, Any]): Identifying fields for the manager instance; the mapping is JSON-normalized (sorted keys) before being incorporated.
278
+
182
279
  Returns:
183
- group_name (str): A deterministic string usable as a channel group identifier for subscription events.
280
+ group_name (str): A deterministic channel group identifier derived from the manager class and normalized identification.
184
281
  """
185
282
  normalized = json.dumps(identification, sort_keys=True, default=str)
186
283
  digest = hashlib.sha256(
@@ -198,17 +295,19 @@ class GraphQL:
198
295
  ) -> None:
199
296
  """
200
297
  Listen to a channel layer for "gm.subscription.event" messages and enqueue their `action` values.
201
-
298
+
202
299
  Continuously receives messages from the given channel_name on the channel_layer, and when a message of type "gm.subscription.event" contains an `action`, that action string is put into the provided asyncio queue. Ignores messages of other types. The loop exits silently when the task is cancelled.
203
-
300
+
204
301
  Parameters:
205
- channel_layer (BaseChannelLayer): Channel layer to receive messages from.
206
- channel_name (str): Name of the channel to listen on.
207
- queue (asyncio.Queue[str]): Async queue to which received action strings will be enqueued.
302
+ channel_layer (BaseChannelLayer): Channel layer to receive messages from.
303
+ channel_name (str): Name of the channel to listen on.
304
+ queue (asyncio.Queue[str]): Async queue to which received action strings will be enqueued.
208
305
  """
209
306
  try:
210
307
  while True:
211
- message = cast(dict[str, Any], await channel_layer.receive(channel_name))
308
+ message = cast(
309
+ dict[str, Any], await channel_layer.receive(channel_name)
310
+ )
212
311
  if message.get("type") != "gm.subscription.event":
213
312
  continue
214
313
  action = cast(str | None, message.get("action"))
@@ -220,12 +319,12 @@ class GraphQL:
220
319
  @classmethod
221
320
  def createGraphqlMutation(cls, generalManagerClass: type[GeneralManager]) -> None:
222
321
  """
223
- Register create, update, and delete GraphQL mutations for a manager class when its Interface overrides the default CRUD methods.
224
-
225
- For each of `create`, `update`, and `deactivate` that is implemented on the manager's `Interface` (i.e., differs from InterfaceBase), this method generates a corresponding mutation class and stores it on the class-level mutation registry (`cls._mutations`) under the names `create<ManagerName>`, `update<ManagerName>`, and `delete<ManagerName>`. The generated mutations return a `success` flag and the created/updated/deactivated object field keyed by the manager class name.
226
-
322
+ Register GraphQL mutation classes for a GeneralManager based on its Interface.
323
+
324
+ If the manager's Interface overrides the default `create`, `update`, or `deactivate` methods, corresponding mutation classes are generated and stored in the class-level mutation registry under the names `create<ManagerName>`, `update<ManagerName>`, and `delete<ManagerName>`. Each generated mutation exposes a `success` flag and a field named for the manager class that returns the created/updated/deactivated object.
325
+
227
326
  Parameters:
228
- generalManagerClass (type[GeneralManager]): Manager class whose `Interface` drives which mutations are generated and registered.
327
+ generalManagerClass (type[GeneralManager]): Manager class whose `Interface` determines which mutations are generated and registered.
229
328
  """
230
329
 
231
330
  interface_cls: InterfaceBase | None = getattr(
@@ -262,9 +361,9 @@ class GraphQL:
262
361
  def createGraphqlInterface(cls, generalManagerClass: Type[GeneralManager]) -> None:
263
362
  """
264
363
  Create and register a Graphene ObjectType for a GeneralManager class and expose its queries and subscription.
265
-
364
+
266
365
  Builds a Graphene type by mapping the manager's Interface attributes and GraphQLProperties to Graphene fields and resolvers, registers the resulting type and manager in the GraphQL registries, and adds corresponding query and subscription fields to the schema.
267
-
366
+
268
367
  Parameters:
269
368
  generalManagerClass (Type[GeneralManager]): The manager class whose Interface and GraphQLProperties are used to generate Graphene fields and resolvers.
270
369
  """
@@ -337,7 +436,7 @@ class GraphQL:
337
436
  ) -> type[graphene.Enum] | None:
338
437
  """
339
438
  Builds an enum of sortable field names for the given manager class.
340
-
439
+
341
440
  Returns:
342
441
  An Enum type whose members are the sortable field names for the manager, or `None` if no sortable fields exist.
343
442
  """
@@ -376,7 +475,9 @@ class GraphQL:
376
475
  )
377
476
 
378
477
  @staticmethod
379
- def _getFilterOptions(attribute_type: type, attribute_name: str) -> Generator[
478
+ def _getFilterOptions(
479
+ attribute_type: type, attribute_name: str
480
+ ) -> Generator[
380
481
  tuple[
381
482
  str, type[graphene.ObjectType] | MeasurementScalar | graphene.List | None
382
483
  ],
@@ -384,14 +485,18 @@ class GraphQL:
384
485
  None,
385
486
  ]:
386
487
  """
387
- Yield filter field names and Graphene types for a given attribute.
488
+ Produce filter field names and corresponding Graphene input types for a given attribute.
388
489
 
389
490
  Parameters:
390
- attribute_type (type): Python type declared for the attribute.
391
- attribute_name (str): Name of the attribute.
491
+ attribute_type (type): The Python type declared for the attribute.
492
+ attribute_name (str): The attribute's name used as the base for generated filter field names.
392
493
 
393
494
  Yields:
394
- tuple[str, Graphene type | None]: Filter name and corresponding Graphene type.
495
+ tuple[str, type | MeasurementScalar | graphene.List | None]:
496
+ A pair where the first element is a filter field name (e.g., "age", "price__gte",
497
+ "name__icontains") and the second element is the Graphene input type for that filter
498
+ or `None` when no Graphene input type should be exposed (for example, nested
499
+ GeneralManager references).
395
500
  """
396
501
  number_options = ["exact", "gt", "gte", "lt", "lte"]
397
502
  string_options = [
@@ -410,13 +515,19 @@ class GraphQL:
410
515
  for option in number_options:
411
516
  yield f"{attribute_name}__{option}", MeasurementScalar()
412
517
  else:
413
- yield attribute_name, GraphQL._mapFieldToGrapheneRead(
414
- attribute_type, attribute_name
518
+ yield (
519
+ attribute_name,
520
+ GraphQL._mapFieldToGrapheneRead(attribute_type, attribute_name),
415
521
  )
416
522
  if issubclass(attribute_type, (int, float, Decimal, date, datetime)):
417
523
  for option in number_options:
418
- yield f"{attribute_name}__{option}", (
419
- GraphQL._mapFieldToGrapheneRead(attribute_type, attribute_name)
524
+ yield (
525
+ f"{attribute_name}__{option}",
526
+ (
527
+ GraphQL._mapFieldToGrapheneRead(
528
+ attribute_type, attribute_name
529
+ )
530
+ ),
420
531
  )
421
532
  elif issubclass(attribute_type, str):
422
533
  base_type = GraphQL._mapFieldToGrapheneBaseType(attribute_type)
@@ -424,10 +535,13 @@ class GraphQL:
424
535
  if option == "in":
425
536
  yield f"{attribute_name}__in", graphene.List(base_type)
426
537
  else:
427
- yield f"{attribute_name}__{option}", (
428
- GraphQL._mapFieldToGrapheneRead(
429
- attribute_type, attribute_name
430
- )
538
+ yield (
539
+ f"{attribute_name}__{option}",
540
+ (
541
+ GraphQL._mapFieldToGrapheneRead(
542
+ attribute_type, attribute_name
543
+ )
544
+ ),
431
545
  )
432
546
 
433
547
  @staticmethod
@@ -436,12 +550,12 @@ class GraphQL:
436
550
  ) -> type[graphene.InputObjectType] | None:
437
551
  """
438
552
  Create a Graphene InputObjectType that exposes available filter fields for a GeneralManager subclass.
439
-
553
+
440
554
  Builds filter fields from the manager's Interface attributes and any GraphQLProperty marked filterable. Returns None when no filterable fields are found.
441
-
555
+
442
556
  Parameters:
443
557
  field_type (Type[GeneralManager]): Manager class whose Interface and GraphQLProperties determine the filter fields.
444
-
558
+
445
559
  Returns:
446
560
  type[graphene.InputObjectType] | None: Generated InputObjectType class for filters, or `None` if no filter fields are applicable.
447
561
  """
@@ -502,7 +616,7 @@ class GraphQL:
502
616
  return graphene.Field(MeasurementType, target_unit=graphene.String())
503
617
  elif issubclass(field_type, GeneralManager):
504
618
  if field_name.endswith("_list"):
505
- attributes = {
619
+ attributes: dict[str, Any] = {
506
620
  "reverse": graphene.Boolean(),
507
621
  "page": graphene.Int(),
508
622
  "page_size": graphene.Int(),
@@ -532,29 +646,37 @@ class GraphQL:
532
646
  @staticmethod
533
647
  def _mapFieldToGrapheneBaseType(field_type: type) -> Type[Any]:
534
648
  """
535
- Map a Python type to the corresponding Graphene scalar/class.
649
+ Map a Python interface type to the corresponding Graphene scalar or custom scalar.
536
650
 
537
651
  Parameters:
538
- field_type (type): Python type declared on the interface.
652
+ field_type (type): Python type from the interface to map.
539
653
 
540
654
  Returns:
541
- Type[Any]: Graphene scalar or type implementing the field.
655
+ Type[Any]: The Graphene scalar or custom scalar type used to represent the input type (e.g., graphene.String, graphene.Int, MeasurementScalar).
656
+
657
+ Raises:
658
+ UnsupportedGraphQLFieldTypeError: If `field_type` is `dict`, which is not supported for GraphQL mapping.
542
659
  """
543
- if issubclass(field_type, dict):
544
- raise TypeError("GraphQL does not support dict fields")
545
- if issubclass(field_type, str):
660
+ base_type = (
661
+ get_origin(field_type) or field_type
662
+ ) # Handle typing generics safely.
663
+ if not isinstance(base_type, type):
546
664
  return graphene.String
547
- elif issubclass(field_type, bool):
665
+ if issubclass(base_type, dict):
666
+ raise UnsupportedGraphQLFieldTypeError(field_type)
667
+ if issubclass(base_type, str):
668
+ return graphene.String
669
+ elif issubclass(base_type, bool):
548
670
  return graphene.Boolean
549
- elif issubclass(field_type, int):
671
+ elif issubclass(base_type, int):
550
672
  return graphene.Int
551
- elif issubclass(field_type, (float, Decimal)):
673
+ elif issubclass(base_type, (float, Decimal)):
552
674
  return graphene.Float
553
- elif issubclass(field_type, datetime):
675
+ elif issubclass(base_type, datetime):
554
676
  return graphene.DateTime
555
- elif issubclass(field_type, date):
677
+ elif issubclass(base_type, date):
556
678
  return graphene.Date
557
- elif issubclass(field_type, Measurement):
679
+ elif issubclass(base_type, Measurement):
558
680
  return MeasurementScalar
559
681
  else:
560
682
  return graphene.String
@@ -562,20 +684,22 @@ class GraphQL:
562
684
  @staticmethod
563
685
  def _parseInput(input_val: dict[str, Any] | str | None) -> dict[str, Any]:
564
686
  """
565
- Normalise filter/exclude input into a dictionary.
687
+ Normalize a filter or exclude input into a dictionary.
688
+
689
+ Accepts a dict, a JSON-encoded string, or None. If given None or an invalid JSON string, returns an empty dict.
566
690
 
567
691
  Parameters:
568
- input_val (dict[str, Any] | str | None): Raw filter/exclude value.
692
+ input_val: Filter or exclude input provided as a dict, a JSON-encoded string, or None.
569
693
 
570
694
  Returns:
571
- dict[str, Any]: Parsed dictionary suitable for queryset filtering.
695
+ dict: Mapping of filter keys to values; empty dict if input is None or invalid JSON.
572
696
  """
573
697
  if input_val is None:
574
698
  return {}
575
699
  if isinstance(input_val, str):
576
700
  try:
577
701
  return json.loads(input_val)
578
- except Exception:
702
+ except (json.JSONDecodeError, ValueError):
579
703
  return {}
580
704
  return input_val
581
705
 
@@ -697,6 +821,8 @@ class GraphQL:
697
821
  A dictionary containing the paginated items under "items" and pagination metadata under "pageInfo".
698
822
  """
699
823
  base_queryset = base_getter(self)
824
+ if base_queryset is None:
825
+ base_queryset = fallback_manager_class.all()
700
826
  # use _manager_class from the attribute if available, otherwise fallback
701
827
  manager_class = getattr(
702
828
  base_queryset, "_manager_class", fallback_manager_class
@@ -826,16 +952,16 @@ class GraphQL:
826
952
  ) -> type[graphene.ObjectType]:
827
953
  """
828
954
  Provide or retrieve a GraphQL ObjectType that represents a paginated page for the given item type.
829
-
955
+
830
956
  Creates and caches a GraphQL ObjectType with two fields:
831
957
  - `items`: a required list of the provided item type.
832
958
  - `pageInfo`: a required PageInfo object containing pagination metadata.
833
-
959
+
834
960
  Parameters:
835
961
  page_type_name (str): The name to use for the generated GraphQL ObjectType.
836
962
  item_type (type[graphene.ObjectType] | Callable[[], type[graphene.ObjectType]]):
837
963
  The Graphene ObjectType used for items, or a zero-argument callable that returns it (to support forward references).
838
-
964
+
839
965
  Returns:
840
966
  type[graphene.ObjectType]: A Graphene ObjectType with `items` and `pageInfo` fields.
841
967
  """
@@ -855,16 +981,21 @@ class GraphQL:
855
981
  cls, generalManagerClass: Type[GeneralManager]
856
982
  ) -> dict[str, Any]:
857
983
  """
858
- Builds GraphQL arguments required to uniquely identify an instance of the given manager class.
859
-
984
+ Build the GraphQL arguments required to uniquely identify an instance of the given manager class.
985
+
986
+ For each input field defined on the manager's Interface: use "<name>_id" as a required ID argument for fields that reference another GeneralManager, use "id" as a required ID argument when present, and map other fields to their corresponding Graphene base type marked required.
987
+
860
988
  Parameters:
861
- generalManagerClass (Type[GeneralManager]): Manager class whose Interface.input_fields are used to derive identification arguments.
862
-
989
+ generalManagerClass: GeneralManager subclass whose Interface.input_fields are used to derive identification arguments.
990
+
863
991
  Returns:
864
- dict[str, Any]: Mapping from argument name to a Graphene argument/field. Fields for related manager inputs use "<name>_id" as an ID, the "id" input uses an ID, and other inputs are mapped to appropriate Graphene read fields with `required=True`.
992
+ dict[str, Any]: Mapping of argument name to a Graphene Argument suitable for identifying a single manager instance.
865
993
  """
866
- identification_fields: dict[str, graphene.Argument] = {}
867
- for input_field_name, input_field in generalManagerClass.Interface.input_fields.items():
994
+ identification_fields: dict[str, Any] = {}
995
+ for (
996
+ input_field_name,
997
+ input_field,
998
+ ) in generalManagerClass.Interface.input_fields.items():
868
999
  if issubclass(input_field.type, GeneralManager):
869
1000
  key = f"{input_field_name}_id"
870
1001
  identification_fields[key] = graphene.Argument(
@@ -887,25 +1018,23 @@ class GraphQL:
887
1018
  ) -> None:
888
1019
  """
889
1020
  Registers list and detail GraphQL query fields for the given manager type into the class query registry.
890
-
1021
+
891
1022
  Parameters:
892
1023
  graphene_type (type): The Graphene ObjectType that represents the manager's GraphQL type.
893
1024
  generalManagerClass (Type[GeneralManager]): The GeneralManager subclass to expose via queries.
894
-
1025
+
895
1026
  Raises:
896
1027
  TypeError: If `generalManagerClass` is not a subclass of GeneralManager.
897
1028
  """
898
1029
  if not issubclass(generalManagerClass, GeneralManager):
899
- raise TypeError(
900
- "generalManagerClass must be a subclass of GeneralManager to create a GraphQL interface"
901
- )
1030
+ raise InvalidGeneralManagerClassError(generalManagerClass)
902
1031
 
903
1032
  if not hasattr(cls, "_query_fields"):
904
1033
  cls._query_fields = cast(dict[str, Any], {})
905
1034
 
906
1035
  # resolver and field for the list query
907
1036
  list_field_name = f"{generalManagerClass.__name__.lower()}_list"
908
- attributes = {
1037
+ attributes: dict[str, Any] = {
909
1038
  "reverse": graphene.Boolean(),
910
1039
  "page": graphene.Int(),
911
1040
  "page_size": graphene.Int(),
@@ -924,9 +1053,16 @@ class GraphQL:
924
1053
  )
925
1054
  list_field = graphene.Field(page_type, **attributes)
926
1055
 
927
- list_resolver = cls._createListResolver(
928
- lambda self: generalManagerClass.all(), generalManagerClass
929
- )
1056
+ def _all_items(_: Any) -> Any:
1057
+ """
1058
+ Return all instances for the associated GeneralManager class.
1059
+
1060
+ Returns:
1061
+ All instances for the associated GeneralManager class, typically provided as a Bucket/QuerySet-like iterable.
1062
+ """
1063
+ return generalManagerClass.all()
1064
+
1065
+ list_resolver = cls._createListResolver(_all_items, generalManagerClass)
930
1066
  cls._query_fields[list_field_name] = list_field
931
1067
  cls._query_fields[f"resolve_{list_field_name}"] = list_resolver
932
1068
 
@@ -939,13 +1075,13 @@ class GraphQL:
939
1075
  self: GeneralManager, info: GraphQLResolveInfo, **identification: dict
940
1076
  ) -> GeneralManager:
941
1077
  """
942
- Resolve a single manager instance from provided identification arguments.
943
-
1078
+ Instantiate and return a GeneralManager for the provided identification arguments.
1079
+
944
1080
  Parameters:
945
- identification (dict): Mapping of identification argument names to their values; used as keyword arguments when instantiating the manager.
946
-
1081
+ identification (dict): Mapping of identification argument names to values passed to the manager constructor.
1082
+
947
1083
  Returns:
948
- GeneralManager: The instantiated manager corresponding to the given identification.
1084
+ GeneralManager: The manager instance identified by the provided arguments.
949
1085
  """
950
1086
  return generalManagerClass(**identification)
951
1087
 
@@ -958,9 +1094,9 @@ class GraphQL:
958
1094
  ) -> None:
959
1095
  """
960
1096
  Eagerly evaluate GraphQLProperty attributes on a manager instance to capture dependency metadata.
961
-
1097
+
962
1098
  When GraphQLProperty descriptors are accessed they may record dependency information on the instance; this function forces those properties to be read so their dependency tracking is populated. If `property_names` is provided, only those property names that exist on the Interface are evaluated; otherwise all GraphQLProperties from the Interface are evaluated.
963
-
1099
+
964
1100
  Parameters:
965
1101
  instance (GeneralManager): The manager instance whose GraphQLProperty attributes should be evaluated.
966
1102
  property_names (Iterable[str] | None): Optional iterable of property names to evaluate. Names not present in the Interface's GraphQLProperties are ignored.
@@ -981,15 +1117,15 @@ class GraphQL:
981
1117
  cls, dependency_records: Iterable[Dependency]
982
1118
  ) -> list[tuple[type[GeneralManager], dict[str, Any]]]:
983
1119
  """
984
- Convert DependencyTracker records into tuples of (manager class, identification dict).
985
-
986
- Filters input records to those whose operation is "identification", whose manager name maps to a registered GeneralManager class, and whose identifier can be parsed to a dict; returns one tuple per successfully resolved record.
987
-
1120
+ Convert dependency tracker records into resolved (manager class, identification dict) pairs.
1121
+
1122
+ Parses records whose operation is "identification", looks up the corresponding GeneralManager class by name in the registry, and parses the identifier into a dict. Records that do not meet these criteria are skipped.
1123
+
988
1124
  Parameters:
989
1125
  dependency_records (Iterable[Dependency]): Iterable of dependency records as (manager_name, operation, identifier).
990
-
1126
+
991
1127
  Returns:
992
- list[tuple[type[GeneralManager], dict[str, Any]]]: A list of (manager_class, identification_dict) tuples for each resolved record.
1128
+ list[tuple[type[GeneralManager], dict[str, Any]]]: List of (manager_class, identification_dict) tuples for each successfully resolved record.
993
1129
  """
994
1130
  resolved: list[tuple[type[GeneralManager], dict[str, Any]]] = []
995
1131
  for manager_name, operation, identifier in dependency_records:
@@ -1014,14 +1150,14 @@ class GraphQL:
1014
1150
  manager_class: type[GeneralManager],
1015
1151
  ) -> set[str]:
1016
1152
  """
1017
- Identify GraphQLProperty names selected within the subscription payload's `item` field.
1018
-
1153
+ Determine which GraphQLProperty names are selected under the subscription payload's `item` field.
1154
+
1019
1155
  Parameters:
1020
1156
  info (GraphQLResolveInfo): Resolve info containing the parsed selection set and fragments.
1021
- manager_class (type[GeneralManager]): Manager class whose Interface defines available GraphQLProperty names.
1022
-
1157
+ manager_class (type[GeneralManager]): GeneralManager subclass whose Interface defines available GraphQLProperty names.
1158
+
1023
1159
  Returns:
1024
- property_names (set[str]): Set of GraphQLProperty names referenced under the `item` selection in the subscription request; empty if none or if the manager has no Interface.
1160
+ property_names (set[str]): Set of GraphQLProperty names referenced under the `item` selection in the subscription request; empty set if none or if the manager has no Interface.
1025
1161
  """
1026
1162
  interface_cls = getattr(manager_class, "Interface", None)
1027
1163
  if interface_cls is None:
@@ -1035,12 +1171,12 @@ class GraphQL:
1035
1171
  def collect_from_selection(selection_set: SelectionSetNode | None) -> None:
1036
1172
  """
1037
1173
  Recursively collect selected GraphQL property names from a SelectionSetNode into the enclosing `property_names` set.
1038
-
1174
+
1039
1175
  Processes each selection in the provided selection_set:
1040
1176
  - Adds a field's name to `property_names` when the name is present in the surrounding `available_properties` set.
1041
1177
  - For fragment spreads, resolves the fragment via `info.fragments` (from the enclosing scope) and recurses into its selection set.
1042
1178
  - For inline fragments, recurses into the fragment's selection set.
1043
-
1179
+
1044
1180
  Parameters:
1045
1181
  selection_set (SelectionSetNode | None): GraphQL selection set to traverse; no action is taken if None.
1046
1182
  """
@@ -1060,16 +1196,16 @@ class GraphQL:
1060
1196
 
1061
1197
  def inspect_selection_set(selection_set: SelectionSetNode | None) -> None:
1062
1198
  """
1063
- Recursively traverse a GraphQL SelectionSet and delegate the subselection of any field named "item" to collect_from_selection.
1064
-
1199
+ Traverse a GraphQL SelectionSet and collect subselections of any field named "item".
1200
+
1065
1201
  Parameters:
1066
- selection_set (SelectionSetNode | None): The AST selection set to inspect. If None, the function does nothing.
1067
-
1202
+ selection_set (SelectionSetNode | None): The AST selection set to inspect. If None, the function returns without action.
1203
+
1068
1204
  Description:
1069
- Visits FieldNode, FragmentSpreadNode, and InlineFragmentNode entries:
1070
- - For FieldNode named "item", calls collect_from_selection with that field's subselection.
1071
- - For other FieldNode entries and inline fragments, continues recursion into their subselections.
1072
- - For fragment spreads, resolves the fragment from `info.fragments` (if present) and inspects its subselection.
1205
+ - Visits FieldNode, FragmentSpreadNode, and InlineFragmentNode entries.
1206
+ - For a FieldNode named "item", delegates its subselection to collect_from_selection.
1207
+ - For other FieldNode entries and InlineFragmentNode entries, recurses into their subselections.
1208
+ - For FragmentSpreadNode entries, attempts to resolve the referenced fragment (via the surrounding `info.fragments` if available) and inspects that fragment's subselection.
1073
1209
  """
1074
1210
  if selection_set is None:
1075
1211
  return
@@ -1099,16 +1235,16 @@ class GraphQL:
1099
1235
  ) -> list[tuple[type[GeneralManager], dict[str, Any]]]:
1100
1236
  """
1101
1237
  Builds a list of dependency pairs (manager class, identification) for subscription wiring from an instance and optional dependency records.
1102
-
1238
+
1103
1239
  Given a manager class and its instantiated item, returns deduplicated dependency definitions derived from:
1104
1240
  - any Dependency records produced by a dependency tracker, and
1105
1241
  - the manager Interface's input fields that reference other GeneralManager types and are populated on the instance.
1106
-
1242
+
1107
1243
  Parameters:
1108
1244
  manager_class (type[GeneralManager]): The manager type whose subscription dependencies are being resolved.
1109
1245
  instance (GeneralManager): The instantiated manager item whose inputs and identification are inspected.
1110
1246
  dependency_records (Iterable[Dependency] | None): Optional dependency-tracker records to include.
1111
-
1247
+
1112
1248
  Returns:
1113
1249
  list[tuple[type[GeneralManager], dict[str, Any]]]: A list of (dependent_manager_class, identification) pairs.
1114
1250
  Each identification is a dict of identification fields. The list excludes the (manager_class, instance.identification) pair and contains no duplicates.
@@ -1116,9 +1252,10 @@ class GraphQL:
1116
1252
  dependencies: list[tuple[type[GeneralManager], dict[str, Any]]] = []
1117
1253
  seen: set[tuple[str, str]] = set()
1118
1254
  if dependency_records:
1119
- for dependency_class, dependency_identification in cls._dependencies_from_tracker(
1120
- dependency_records
1121
- ):
1255
+ for (
1256
+ dependency_class,
1257
+ dependency_identification,
1258
+ ) in cls._dependencies_from_tracker(dependency_records):
1122
1259
  if (
1123
1260
  dependency_class is manager_class
1124
1261
  and dependency_identification == instance.identification
@@ -1181,13 +1318,13 @@ class GraphQL:
1181
1318
  ) -> tuple[GeneralManager, set[Dependency]]:
1182
1319
  """
1183
1320
  Create a GeneralManager instance for the given identification and optionally prime its GraphQL properties to capture dependency records.
1184
-
1321
+
1185
1322
  Parameters:
1186
1323
  manager_class (type[GeneralManager]): Manager class to instantiate.
1187
1324
  identification (dict[str, Any]): Mapping of identification field names to values used to construct the instance.
1188
1325
  collect_dependencies (bool): If True, prime GraphQL properties while tracking and return the captured Dependency records.
1189
1326
  property_names (Iterable[str] | None): Specific GraphQLProperty names to prime; if None, all relevant properties are primed.
1190
-
1327
+
1191
1328
  Returns:
1192
1329
  tuple[GeneralManager, set[Dependency]]: The instantiated manager and a set of captured Dependency objects (empty if collect_dependencies is False).
1193
1330
  """
@@ -1202,20 +1339,22 @@ class GraphQL:
1202
1339
 
1203
1340
  @classmethod
1204
1341
  def _addSubscriptionField(
1205
- cls, graphene_type: type[graphene.ObjectType], generalManagerClass: Type[GeneralManager]
1342
+ cls,
1343
+ graphene_type: type[graphene.ObjectType],
1344
+ generalManagerClass: Type[GeneralManager],
1206
1345
  ) -> None:
1207
1346
  """
1208
1347
  Register a GraphQL subscription field that publishes change events for the given manager type.
1209
-
1348
+
1210
1349
  Creates (or reuses) a SubscriptionEvent payload GraphQL type and adds three entries to the class subscription registry:
1211
1350
  - a field exposing the subscription with identification arguments,
1212
1351
  - an async subscribe function that yields an initial "snapshot" event and subsequent change events for the identified instance and its dependencies,
1213
1352
  - and a resolve function that returns the delivered payload.
1214
-
1353
+
1215
1354
  Parameters:
1216
1355
  graphene_type (type[graphene.ObjectType]): GraphQL ObjectType representing the manager's item type used as the payload `item` field.
1217
1356
  generalManagerClass (Type[GeneralManager]): The GeneralManager subclass whose changes the subscription will publish.
1218
-
1357
+
1219
1358
  Notes:
1220
1359
  - The subscribe function requires an available channel layer and subscribes the caller to channel groups derived from the instance identification and its resolved dependencies.
1221
1360
  - The subscribe coroutine yields SubscriptionEvent objects with fields `item` (the current instance or None if it cannot be instantiated) and `action` (a string such as `"snapshot"` or other change actions).
@@ -1237,9 +1376,9 @@ class GraphQL:
1237
1376
  "action": graphene.String(required=True),
1238
1377
  },
1239
1378
  )
1240
- cls._subscription_payload_registry[
1241
- generalManagerClass.__name__
1242
- ] = payload_type
1379
+ cls._subscription_payload_registry[generalManagerClass.__name__] = (
1380
+ payload_type
1381
+ )
1243
1382
 
1244
1383
  identification_args = cls._buildIdentificationArguments(generalManagerClass)
1245
1384
  subscription_field = graphene.Field(payload_type, **identification_args)
@@ -1250,15 +1389,15 @@ class GraphQL:
1250
1389
  **identification: Any,
1251
1390
  ) -> AsyncIterator[SubscriptionEvent]:
1252
1391
  """
1253
- Open a subscription stream for a specific manager instance identified by the provided arguments.
1254
-
1255
- The iterator first yields a `SubscriptionEvent` with action `"snapshot"` containing the current item, then yields subsequent `SubscriptionEvent`s for each received action. If an item cannot be re-instantiated while producing an update event, the yielded `SubscriptionEvent.item` will be `None`. The subscription registers the caller on the manager's channel groups and automatically unsubscribes and cancels background listeners when the iterator is closed or cancelled.
1256
-
1392
+ Stream subscription events for a specific manager instance identified by the provided arguments.
1393
+
1394
+ Yields an initial `SubscriptionEvent` with `action` set to `"snapshot"` containing the current manager instance, then yields `SubscriptionEvent`s for each subsequent action. For update events, `item` will be the re-instantiated manager instance or `None` if instantiation fails. The subscriber is registered on the manager's channel groups (including dependent managers' groups) and the channel subscriptions and background listener are cleaned up when the iterator is closed or cancelled.
1395
+
1257
1396
  Parameters:
1258
1397
  identification: Identification fields required to locate the manager instance (maps to the manager's identification signature).
1259
-
1398
+
1260
1399
  Returns:
1261
- AsyncIterator[SubscriptionEvent]: An asynchronous iterator that produces subscription events (initial snapshot then updates).
1400
+ AsyncIterator[SubscriptionEvent]: An asynchronous iterator that first yields a snapshot event and then yields update events; each event's `item` is the manager instance or `None` if it could not be instantiated.
1262
1401
  """
1263
1402
  identification_copy = deepcopy(identification)
1264
1403
  property_names = cls._subscription_property_names(
@@ -1276,7 +1415,9 @@ class GraphQL:
1276
1415
  raise GraphQLError(str(exc)) from exc
1277
1416
 
1278
1417
  try:
1279
- channel_layer = cast(BaseChannelLayer, cls._get_channel_layer(strict=True))
1418
+ channel_layer = cast(
1419
+ BaseChannelLayer, cls._get_channel_layer(strict=True)
1420
+ )
1280
1421
  except RuntimeError as exc:
1281
1422
  raise GraphQLError(str(exc)) from exc
1282
1423
  channel_name = cast(str, await channel_layer.new_channel())
@@ -1307,13 +1448,13 @@ class GraphQL:
1307
1448
 
1308
1449
  async def event_stream() -> AsyncIterator[SubscriptionEvent]:
1309
1450
  """
1310
- Asynchronously yields subscription events for a manager instance: an initial "snapshot" event followed by events for subsequent actions.
1311
-
1312
- Yields:
1313
- SubscriptionEvent: First yields a `SubscriptionEvent` with `action` set to "snapshot" and `item` containing the current manager instance (or `None` if instantiation failed). Afterwards yields `SubscriptionEvent` values for each action received, where `action` is the action string and `item` is the (re-)instantiated manager or `None` if instantiation failed.
1314
-
1451
+ Yield subscription events for a manager instance, starting with an initial snapshot followed by subsequent updates.
1452
+
1453
+ Returns:
1454
+ AsyncIterator[SubscriptionEvent]: An asynchronous iterator that first yields a `SubscriptionEvent` with `action` set to `"snapshot"` and `item` containing the current manager instance (or `None` if instantiation failed). Subsequent yields provide `SubscriptionEvent` values for each received action, where `action` is the action string and `item` is the (re-)instantiated manager or `None` if instantiation failed.
1455
+
1315
1456
  Notes:
1316
- On exit the function cancels the background listener task and discards the channel subscriptions for all groups associated with the subscription.
1457
+ When the iterator is closed or exits, the background listener task is cancelled and the subscription's channel group memberships are discarded.
1317
1458
  """
1318
1459
  try:
1319
1460
  yield SubscriptionEvent(item=instance, action="snapshot")
@@ -1326,7 +1467,7 @@ class GraphQL:
1326
1467
  identification_copy,
1327
1468
  property_names=property_names,
1328
1469
  )
1329
- except Exception:
1470
+ except HANDLED_MANAGER_ERRORS:
1330
1471
  item = None
1331
1472
  yield SubscriptionEvent(item=item, action=action)
1332
1473
  finally:
@@ -1345,11 +1486,11 @@ class GraphQL:
1345
1486
  ) -> SubscriptionEvent:
1346
1487
  """
1347
1488
  Passes a subscription payload through unchanged.
1348
-
1489
+
1349
1490
  Parameters:
1350
1491
  payload (SubscriptionEvent): The subscription event payload to deliver to the client.
1351
1492
  info (GraphQLResolveInfo): GraphQL resolver info (unused).
1352
-
1493
+
1353
1494
  Returns:
1354
1495
  SubscriptionEvent: The same payload instance provided as input.
1355
1496
  """
@@ -1362,16 +1503,13 @@ class GraphQL:
1362
1503
  @classmethod
1363
1504
  def createWriteFields(cls, interface_cls: InterfaceBase) -> dict[str, Any]:
1364
1505
  """
1365
- Generate Graphene input fields for writable attributes defined by an Interface.
1366
-
1367
- Skips system fields ("changed_by", "created_at", "updated_at") and derived attributes.
1368
- For relation attributes referencing another GeneralManager, produces ID or list-of-ID fields.
1369
- Each generated field is annotated with an `editable` attribute reflecting the interface metadata.
1370
- Always includes an optional "history_comment" String field marked editable.
1371
-
1506
+ Create Graphene input fields for writable attributes defined by an Interface.
1507
+
1508
+ Skips system fields ("changed_by", "created_at", "updated_at") and attributes marked as derived. For attributes whose type is a GeneralManager, produces an ID field or a list of ID fields for names ending with "_list". Each generated field is annotated with an `editable` attribute reflecting the interface metadata. Always includes an optional `history_comment` string field marked editable.
1509
+
1372
1510
  Parameters:
1373
- interface_cls (InterfaceBase): Interface whose attribute metadata (types, required, default, editable, derived) is used to build input fields.
1374
-
1511
+ interface_cls (InterfaceBase): Interface providing attribute metadata (type, required, default, editable, derived) used to build the input fields.
1512
+
1375
1513
  Returns:
1376
1514
  dict[str, Any]: Mapping from attribute name to a Graphene input field instance.
1377
1515
  """
@@ -1387,6 +1525,7 @@ class GraphQL:
1387
1525
  req = info["is_required"]
1388
1526
  default = info["default"]
1389
1527
 
1528
+ fld: Any
1390
1529
  if issubclass(typ, GeneralManager):
1391
1530
  if name.endswith("_list"):
1392
1531
  fld = graphene.List(
@@ -1407,12 +1546,13 @@ class GraphQL:
1407
1546
  )
1408
1547
 
1409
1548
  # mark for generate* code to know what is editable
1410
- setattr(fld, "editable", info["is_editable"])
1549
+ cast(Any, fld).editable = info["is_editable"]
1411
1550
  fields[name] = fld
1412
1551
 
1413
1552
  # history_comment is always optional without a default value
1414
- fields["history_comment"] = graphene.String()
1415
- setattr(fields["history_comment"], "editable", True)
1553
+ history_field = graphene.String()
1554
+ cast(Any, history_field).editable = True
1555
+ fields["history_comment"] = history_field
1416
1556
 
1417
1557
  return fields
1418
1558
 
@@ -1423,12 +1563,14 @@ class GraphQL:
1423
1563
  default_return_values: dict[str, Any],
1424
1564
  ) -> type[graphene.Mutation] | None:
1425
1565
  """
1426
- Dynamically generates a Graphene mutation class for creating an instance of a specified GeneralManager subclass.
1566
+ Generate a Graphene Mutation class that creates instances of the given GeneralManager subclass.
1427
1567
 
1428
- The generated mutation class uses the manager's interface to define input arguments, filters out fields with `NOT_PROVIDED` values, and invokes the manager's `create` method with the provided data and the current user's ID. On success, it returns a dictionary with a success flag and the created instance; on failure, it raises a GraphQL error. Returns `None` if the manager class does not define an interface.
1568
+ Parameters:
1569
+ generalManagerClass (type[GeneralManager]): The GeneralManager subclass to expose a create mutation for.
1570
+ default_return_values (dict[str, Any]): Base mutation return fields to include on the generated class.
1429
1571
 
1430
1572
  Returns:
1431
- The generated Graphene mutation class, or `None` if the manager class does not define an interface.
1573
+ type[graphene.Mutation] | None: A Mutation class named `Create<ManagerName>` that implements creation for the manager (exposes appropriate Arguments and a `mutate` implementation), or `None` if the manager class does not define an `Interface`.
1432
1574
  """
1433
1575
  interface_cls: InterfaceBase | None = getattr(
1434
1576
  generalManagerClass, "Interface", None
@@ -1442,9 +1584,14 @@ class GraphQL:
1442
1584
  **kwargs: Any,
1443
1585
  ) -> dict:
1444
1586
  """
1445
- Creates a new instance of the manager class using the provided arguments.
1587
+ Create a new instance of the manager using the provided input fields.
1446
1588
 
1447
- Filters out any fields set to `NOT_PROVIDED` before invoking the creation method. Returns a dictionary with a success flag and the created instance keyed by the manager class name. If creation fails, raises a GraphQL error and returns a dictionary with `success` set to `False`.
1589
+ Parameters:
1590
+ info (GraphQLResolveInfo): GraphQL resolve info whose context user ID is used as the creator_id.
1591
+ kwargs (Any): Input fields for the new instance; fields equal to `NOT_PROVIDED` are ignored.
1592
+
1593
+ Returns:
1594
+ result (dict): A dictionary containing `success` (`True` if creation succeeded, `False` otherwise). On success the dictionary also includes the created instance under a key matching the manager class name.
1448
1595
  """
1449
1596
  try:
1450
1597
  kwargs = {
@@ -1455,11 +1602,8 @@ class GraphQL:
1455
1602
  instance = generalManagerClass.create(
1456
1603
  **kwargs, creator_id=info.context.user.id
1457
1604
  )
1458
- except Exception as e:
1459
- GraphQL._handleGraphQLError(e)
1460
- return {
1461
- "success": False,
1462
- }
1605
+ except HANDLED_MANAGER_ERRORS as error:
1606
+ raise GraphQL._handleGraphQLError(error) from error
1463
1607
 
1464
1608
  return {
1465
1609
  "success": True,
@@ -1513,27 +1657,24 @@ class GraphQL:
1513
1657
  **kwargs: Any,
1514
1658
  ) -> dict:
1515
1659
  """
1516
- Updates an instance of a GeneralManager subclass with the specified field values.
1660
+ Update a GeneralManager instance identified by its `id` with the provided field values.
1517
1661
 
1518
1662
  Parameters:
1519
- info (GraphQLResolveInfo): The GraphQL resolver context, including user and request data.
1520
- **kwargs: Field values to update, including the required 'id' of the instance.
1663
+ info (GraphQLResolveInfo): GraphQL resolver context (contains the requesting user and request data).
1664
+ **kwargs: Field values to update. Must include `id` to identify the target instance; other keys are treated as fields to update.
1521
1665
 
1522
1666
  Returns:
1523
- dict: A dictionary with 'success' (bool) and the updated instance keyed by its class name.
1667
+ dict: Contains `success` (`True` if the update succeeded, `False` otherwise`) and, on success, the updated instance under a key matching the manager class name.
1524
1668
  """
1669
+ manager_id = kwargs.pop("id", None)
1670
+ if manager_id is None:
1671
+ raise GraphQL._handleGraphQLError(MissingManagerIdentifierError())
1525
1672
  try:
1526
- manager_id = kwargs.pop("id", None)
1527
- if manager_id is None:
1528
- raise ValueError("id is required")
1529
1673
  instance = generalManagerClass(id=manager_id).update(
1530
1674
  creator_id=info.context.user.id, **kwargs
1531
1675
  )
1532
- except Exception as e:
1533
- GraphQL._handleGraphQLError(e)
1534
- return {
1535
- "success": False,
1536
- }
1676
+ except HANDLED_MANAGER_ERRORS as error:
1677
+ raise GraphQL._handleGraphQLError(error) from error
1537
1678
 
1538
1679
  return {
1539
1680
  "success": True,
@@ -1590,23 +1731,20 @@ class GraphQL:
1590
1731
  **kwargs: Any,
1591
1732
  ) -> dict:
1592
1733
  """
1593
- Deactivates an instance of a GeneralManager subclass and returns the operation result.
1734
+ Deactivates the identified GeneralManager instance.
1594
1735
 
1595
1736
  Returns:
1596
- dict: A dictionary with a "success" boolean and the deactivated instance keyed by its class name.
1737
+ dict: `success` key with `True` if deactivation succeeded, `False` otherwise. On success, includes an additional key equal to the manager class name containing the deactivated instance.
1597
1738
  """
1739
+ manager_id = kwargs.pop("id", None)
1740
+ if manager_id is None:
1741
+ raise GraphQL._handleGraphQLError(MissingManagerIdentifierError())
1598
1742
  try:
1599
- manager_id = kwargs.pop("id", None)
1600
- if manager_id is None:
1601
- raise ValueError("id is required")
1602
1743
  instance = generalManagerClass(id=manager_id).deactivate(
1603
1744
  creator_id=info.context.user.id
1604
1745
  )
1605
- except Exception as e:
1606
- GraphQL._handleGraphQLError(e)
1607
- return {
1608
- "success": False,
1609
- }
1746
+ except HANDLED_MANAGER_ERRORS as error:
1747
+ raise GraphQL._handleGraphQLError(error) from error
1610
1748
 
1611
1749
  return {
1612
1750
  "success": True,
@@ -1635,37 +1773,37 @@ class GraphQL:
1635
1773
  )
1636
1774
 
1637
1775
  @staticmethod
1638
- def _handleGraphQLError(error: Exception) -> None:
1776
+ def _handleGraphQLError(error: Exception) -> GraphQLError:
1639
1777
  """
1640
1778
  Convert an exception into a GraphQL error with an appropriate extensions['code'].
1641
-
1779
+
1642
1780
  Maps:
1643
1781
  PermissionError -> "PERMISSION_DENIED"
1644
1782
  ValueError, ValidationError, TypeError -> "BAD_USER_INPUT"
1645
1783
  other exceptions -> "INTERNAL_SERVER_ERROR"
1646
-
1784
+
1647
1785
  Parameters:
1648
1786
  error (Exception): The original exception to convert.
1649
-
1650
- Raises:
1787
+
1788
+ Returns:
1651
1789
  GraphQLError: GraphQL error containing the original message and an `extensions['code']` indicating the error category.
1652
1790
  """
1653
1791
  if isinstance(error, PermissionError):
1654
- raise GraphQLError(
1792
+ return GraphQLError(
1655
1793
  str(error),
1656
1794
  extensions={
1657
1795
  "code": "PERMISSION_DENIED",
1658
1796
  },
1659
1797
  )
1660
1798
  elif isinstance(error, (ValueError, ValidationError, TypeError)):
1661
- raise GraphQLError(
1799
+ return GraphQLError(
1662
1800
  str(error),
1663
1801
  extensions={
1664
1802
  "code": "BAD_USER_INPUT",
1665
1803
  },
1666
1804
  )
1667
1805
  else:
1668
- raise GraphQLError(
1806
+ return GraphQLError(
1669
1807
  str(error),
1670
1808
  extensions={
1671
1809
  "code": "INTERNAL_SERVER_ERROR",
@@ -1681,17 +1819,14 @@ class GraphQL:
1681
1819
  **_: Any,
1682
1820
  ) -> None:
1683
1821
  """
1684
- Send a subscription event to the channel group for a changed GeneralManager instance.
1685
-
1686
- If `instance` is a GeneralManager of a registered manager type and a channel layer is available,
1687
- publish a "gm.subscription.event" message containing the provided `action` to the group identified
1688
- by the manager class and the instance's identification. If `instance` is missing, the manager is
1689
- not registered, or no channel layer is available, the function returns without side effects.
1690
-
1822
+ Send a "gm.subscription.event" message to the channel group corresponding to a changed GeneralManager instance.
1823
+
1824
+ If the provided instance is a registered GeneralManager and a channel layer is configured, publish a message containing the given action to the channel group derived from the manager class and the instance's identification. If the instance is None, the manager type is not registered, or no channel layer is available, the function returns without side effects.
1825
+
1691
1826
  Parameters:
1692
- sender: The signal sender; either a GeneralManager subclass or an instance.
1693
- instance: The GeneralManager instance that changed.
1694
- action: A string describing the change action (e.g., "created", "updated", "deleted").
1827
+ sender (type[GeneralManager] | GeneralManager): The signal sender; either a GeneralManager subclass or an instance.
1828
+ instance (GeneralManager | None): The GeneralManager instance that changed.
1829
+ action (str): A string describing the change action (e.g., "created", "updated", "deleted").
1695
1830
  """
1696
1831
  if instance is None or not isinstance(instance, GeneralManager):
1697
1832
  return
@@ -1718,4 +1853,4 @@ class GraphQL:
1718
1853
  )
1719
1854
 
1720
1855
 
1721
- post_data_change.connect(GraphQL._handle_data_change, weak=False)
1856
+ post_data_change.connect(GraphQL._handle_data_change, weak=False)