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.
- general_manager/__init__.py +11 -1
- general_manager/_types/api.py +0 -1
- general_manager/_types/bucket.py +0 -1
- general_manager/_types/cache.py +0 -1
- general_manager/_types/factory.py +0 -1
- general_manager/_types/general_manager.py +0 -1
- general_manager/_types/interface.py +0 -1
- general_manager/_types/manager.py +0 -1
- general_manager/_types/measurement.py +0 -1
- general_manager/_types/permission.py +0 -1
- general_manager/_types/rule.py +0 -1
- general_manager/_types/utils.py +0 -1
- general_manager/api/__init__.py +13 -1
- general_manager/api/graphql.py +356 -221
- general_manager/api/graphql_subscription_consumer.py +81 -78
- general_manager/api/mutation.py +85 -23
- general_manager/api/property.py +39 -13
- general_manager/apps.py +188 -47
- general_manager/bucket/__init__.py +10 -1
- general_manager/bucket/calculationBucket.py +155 -53
- general_manager/bucket/databaseBucket.py +157 -45
- general_manager/bucket/groupBucket.py +133 -44
- general_manager/cache/__init__.py +10 -1
- general_manager/cache/cacheDecorator.py +3 -0
- general_manager/cache/dependencyIndex.py +143 -45
- general_manager/cache/signals.py +9 -2
- general_manager/factory/__init__.py +10 -1
- general_manager/factory/autoFactory.py +55 -13
- general_manager/factory/factories.py +110 -40
- general_manager/factory/factoryMethods.py +122 -34
- general_manager/interface/__init__.py +10 -1
- general_manager/interface/baseInterface.py +129 -36
- general_manager/interface/calculationInterface.py +35 -18
- general_manager/interface/databaseBasedInterface.py +71 -45
- general_manager/interface/databaseInterface.py +96 -38
- general_manager/interface/models.py +5 -5
- general_manager/interface/readOnlyInterface.py +94 -20
- general_manager/manager/__init__.py +10 -1
- general_manager/manager/generalManager.py +25 -16
- general_manager/manager/groupManager.py +20 -6
- general_manager/manager/meta.py +84 -16
- general_manager/measurement/__init__.py +10 -1
- general_manager/measurement/measurement.py +289 -95
- general_manager/measurement/measurementField.py +42 -31
- general_manager/permission/__init__.py +10 -1
- general_manager/permission/basePermission.py +120 -38
- general_manager/permission/managerBasedPermission.py +72 -21
- general_manager/permission/mutationPermission.py +14 -9
- general_manager/permission/permissionChecks.py +14 -12
- general_manager/permission/permissionDataManager.py +24 -11
- general_manager/permission/utils.py +34 -6
- general_manager/public_api_registry.py +36 -10
- general_manager/rule/__init__.py +10 -1
- general_manager/rule/handler.py +133 -44
- general_manager/rule/rule.py +178 -39
- general_manager/utils/__init__.py +10 -1
- general_manager/utils/argsToKwargs.py +34 -9
- general_manager/utils/filterParser.py +22 -7
- general_manager/utils/formatString.py +1 -0
- general_manager/utils/pathMapping.py +23 -15
- general_manager/utils/public_api.py +33 -2
- general_manager/utils/testing.py +31 -33
- {generalmanager-0.17.0.dist-info → generalmanager-0.19.0.dist-info}/METADATA +3 -1
- generalmanager-0.19.0.dist-info/RECORD +77 -0
- {generalmanager-0.17.0.dist-info → generalmanager-0.19.0.dist-info}/licenses/LICENSE +1 -1
- generalmanager-0.17.0.dist-info/RECORD +0 -77
- {generalmanager-0.17.0.dist-info → generalmanager-0.19.0.dist-info}/WHEEL +0 -0
- {generalmanager-0.17.0.dist-info → generalmanager-0.19.0.dist-info}/top_level.txt +0 -0
general_manager/api/graphql.py
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
144
|
-
|
|
242
|
+
Retrieve the configured channel layer for GraphQL subscriptions.
|
|
243
|
+
|
|
145
244
|
Parameters:
|
|
146
|
-
|
|
147
|
-
|
|
245
|
+
strict (bool): When True, raise MissingChannelLayerError if no channel layer is configured.
|
|
246
|
+
|
|
148
247
|
Returns:
|
|
149
|
-
|
|
150
|
-
|
|
248
|
+
BaseChannelLayer | None: The configured channel layer instance if available, otherwise None.
|
|
249
|
+
|
|
151
250
|
Raises:
|
|
152
|
-
|
|
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
|
|
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
|
-
|
|
177
|
-
|
|
273
|
+
Builds a deterministic channel group name for subscription events for a specific manager instance.
|
|
274
|
+
|
|
178
275
|
Parameters:
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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(
|
|
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
|
|
224
|
-
|
|
225
|
-
|
|
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`
|
|
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(
|
|
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
|
-
|
|
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):
|
|
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,
|
|
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
|
|
414
|
-
|
|
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
|
|
419
|
-
|
|
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
|
|
428
|
-
|
|
429
|
-
|
|
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
|
|
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
|
|
652
|
+
field_type (type): Python type from the interface to map.
|
|
539
653
|
|
|
540
654
|
Returns:
|
|
541
|
-
Type[Any]: Graphene scalar or type
|
|
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
|
-
|
|
544
|
-
|
|
545
|
-
|
|
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
|
-
|
|
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(
|
|
671
|
+
elif issubclass(base_type, int):
|
|
550
672
|
return graphene.Int
|
|
551
|
-
elif issubclass(
|
|
673
|
+
elif issubclass(base_type, (float, Decimal)):
|
|
552
674
|
return graphene.Float
|
|
553
|
-
elif issubclass(
|
|
675
|
+
elif issubclass(base_type, datetime):
|
|
554
676
|
return graphene.DateTime
|
|
555
|
-
elif issubclass(
|
|
677
|
+
elif issubclass(base_type, date):
|
|
556
678
|
return graphene.Date
|
|
557
|
-
elif issubclass(
|
|
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
|
-
|
|
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
|
|
692
|
+
input_val: Filter or exclude input provided as a dict, a JSON-encoded string, or None.
|
|
569
693
|
|
|
570
694
|
Returns:
|
|
571
|
-
dict
|
|
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
|
|
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
|
-
|
|
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
|
|
862
|
-
|
|
989
|
+
generalManagerClass: GeneralManager subclass whose Interface.input_fields are used to derive identification arguments.
|
|
990
|
+
|
|
863
991
|
Returns:
|
|
864
|
-
dict[str, Any]: Mapping
|
|
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,
|
|
867
|
-
for
|
|
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
|
|
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
|
-
|
|
928
|
-
|
|
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
|
-
|
|
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
|
|
946
|
-
|
|
1081
|
+
identification (dict): Mapping of identification argument names to values passed to the manager constructor.
|
|
1082
|
+
|
|
947
1083
|
Returns:
|
|
948
|
-
GeneralManager: The
|
|
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
|
|
985
|
-
|
|
986
|
-
|
|
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]]]:
|
|
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
|
-
|
|
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]):
|
|
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
|
-
|
|
1064
|
-
|
|
1199
|
+
Traverse a GraphQL SelectionSet and collect subselections of any field named "item".
|
|
1200
|
+
|
|
1065
1201
|
Parameters:
|
|
1066
|
-
|
|
1067
|
-
|
|
1202
|
+
selection_set (SelectionSetNode | None): The AST selection set to inspect. If None, the function returns without action.
|
|
1203
|
+
|
|
1068
1204
|
Description:
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
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
|
|
1120
|
-
|
|
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,
|
|
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
|
-
|
|
1242
|
-
|
|
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
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
SubscriptionEvent:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1366
|
-
|
|
1367
|
-
Skips system fields ("changed_by", "created_at", "updated_at") and derived attributes.
|
|
1368
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1415
|
-
|
|
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
|
-
|
|
1566
|
+
Generate a Graphene Mutation class that creates instances of the given GeneralManager subclass.
|
|
1427
1567
|
|
|
1428
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1587
|
+
Create a new instance of the manager using the provided input fields.
|
|
1446
1588
|
|
|
1447
|
-
|
|
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
|
|
1459
|
-
GraphQL._handleGraphQLError(
|
|
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
|
-
|
|
1660
|
+
Update a GeneralManager instance identified by its `id` with the provided field values.
|
|
1517
1661
|
|
|
1518
1662
|
Parameters:
|
|
1519
|
-
info (GraphQLResolveInfo):
|
|
1520
|
-
**kwargs: Field values to update
|
|
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:
|
|
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
|
|
1533
|
-
GraphQL._handleGraphQLError(
|
|
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
|
|
1734
|
+
Deactivates the identified GeneralManager instance.
|
|
1594
1735
|
|
|
1595
1736
|
Returns:
|
|
1596
|
-
dict:
|
|
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
|
|
1606
|
-
GraphQL._handleGraphQLError(
|
|
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) ->
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1799
|
+
return GraphQLError(
|
|
1662
1800
|
str(error),
|
|
1663
1801
|
extensions={
|
|
1664
1802
|
"code": "BAD_USER_INPUT",
|
|
1665
1803
|
},
|
|
1666
1804
|
)
|
|
1667
1805
|
else:
|
|
1668
|
-
|
|
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
|
|
1685
|
-
|
|
1686
|
-
If
|
|
1687
|
-
|
|
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)
|