GeneralManager 0.17.0__py3-none-any.whl → 0.18.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of GeneralManager might be problematic. Click here for more details.
- general_manager/__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/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.18.0.dist-info}/METADATA +2 -1
- generalmanager-0.18.0.dist-info/RECORD +77 -0
- {generalmanager-0.17.0.dist-info → generalmanager-0.18.0.dist-info}/licenses/LICENSE +1 -1
- generalmanager-0.17.0.dist-info/RECORD +0 -77
- {generalmanager-0.17.0.dist-info → generalmanager-0.18.0.dist-info}/WHEEL +0 -0
- {generalmanager-0.17.0.dist-info → generalmanager-0.18.0.dist-info}/top_level.txt +0 -0
general_manager/api/property.py
CHANGED
|
@@ -1,13 +1,28 @@
|
|
|
1
1
|
"""GraphQL-aware property descriptor used by GeneralManager classes."""
|
|
2
2
|
|
|
3
|
-
from typing import Any, Callable, get_type_hints, overload, TypeVar
|
|
4
3
|
import sys
|
|
4
|
+
from typing import Any, Callable, TypeVar, get_type_hints, overload
|
|
5
5
|
|
|
6
6
|
T = TypeVar("T", bound=Callable[..., Any])
|
|
7
7
|
|
|
8
8
|
|
|
9
|
+
class GraphQLPropertyReturnAnnotationError(TypeError):
|
|
10
|
+
"""Raised when a GraphQLProperty is defined without a return type annotation."""
|
|
11
|
+
|
|
12
|
+
def __init__(self) -> None:
|
|
13
|
+
"""
|
|
14
|
+
Indicates a GraphQLProperty-decorated function is missing a return type annotation.
|
|
15
|
+
|
|
16
|
+
This exception is raised to signal that a property resolver intended for use with GraphQLProperty must have an explicit return type hint. The exception message is: "GraphQLProperty requires a return type hint for the property function."
|
|
17
|
+
"""
|
|
18
|
+
super().__init__(
|
|
19
|
+
"GraphQLProperty requires a return type hint for the property function."
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
9
23
|
class GraphQLProperty(property):
|
|
10
24
|
"""Descriptor that exposes a property with GraphQL metadata and type hints."""
|
|
25
|
+
|
|
11
26
|
sortable: bool
|
|
12
27
|
filterable: bool
|
|
13
28
|
query_annotation: Any | None
|
|
@@ -22,14 +37,17 @@ class GraphQLProperty(property):
|
|
|
22
37
|
query_annotation: Any | None = None,
|
|
23
38
|
) -> None:
|
|
24
39
|
"""
|
|
25
|
-
|
|
40
|
+
Initialize the GraphQLProperty descriptor with GraphQL-specific metadata.
|
|
26
41
|
|
|
27
42
|
Parameters:
|
|
28
|
-
fget (Callable):
|
|
29
|
-
doc (str | None): Optional documentation string.
|
|
30
|
-
sortable (bool): Whether the property
|
|
31
|
-
filterable (bool): Whether the property
|
|
32
|
-
query_annotation (Any | None): Optional annotation
|
|
43
|
+
fget (Callable[..., Any]): The resolver function to wrap; its unwrapped form must include a return type annotation.
|
|
44
|
+
doc (str | None): Optional documentation string exposed on the descriptor.
|
|
45
|
+
sortable (bool): Whether the property should be considered for sorting.
|
|
46
|
+
filterable (bool): Whether the property should be considered for filtering.
|
|
47
|
+
query_annotation (Any | None): Optional annotation to apply when querying/queryset construction.
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
GraphQLPropertyReturnAnnotationError: If the underlying resolver function does not declare a return type annotation.
|
|
33
51
|
"""
|
|
34
52
|
super().__init__(fget, doc=doc)
|
|
35
53
|
self.is_graphql_resolver = True
|
|
@@ -46,17 +64,25 @@ class GraphQLProperty(property):
|
|
|
46
64
|
) # falls decorator Annotations durchreicht
|
|
47
65
|
ann = getattr(orig, "__annotations__", {}) or {}
|
|
48
66
|
if "return" not in ann:
|
|
49
|
-
raise
|
|
50
|
-
"GraphQLProperty requires a return type hint for the property function."
|
|
51
|
-
)
|
|
67
|
+
raise GraphQLPropertyReturnAnnotationError()
|
|
52
68
|
|
|
53
69
|
def __set_name__(self, owner: type, name: str) -> None:
|
|
54
|
-
"""
|
|
70
|
+
"""
|
|
71
|
+
Record the owner class and attribute name for the descriptor to support later introspection.
|
|
72
|
+
|
|
73
|
+
Parameters:
|
|
74
|
+
owner (type): The class that owns this descriptor.
|
|
75
|
+
name (str): The attribute name under which this descriptor is assigned.
|
|
76
|
+
"""
|
|
55
77
|
self._owner = owner
|
|
56
78
|
self._name = name
|
|
57
79
|
|
|
58
80
|
def _try_resolve_type_hint(self) -> None:
|
|
59
|
-
"""
|
|
81
|
+
"""
|
|
82
|
+
Resolve and cache the wrapped resolver's return type hint.
|
|
83
|
+
|
|
84
|
+
When successful, stores the resolved return annotation on self._graphql_type_hint; if resolution fails or cannot be determined, sets self._graphql_type_hint to None.
|
|
85
|
+
"""
|
|
60
86
|
if self._graphql_type_hint is not None:
|
|
61
87
|
return
|
|
62
88
|
|
|
@@ -71,7 +97,7 @@ class GraphQLProperty(property):
|
|
|
71
97
|
|
|
72
98
|
hints = get_type_hints(self.fget, globalns=globalns, localns=localns)
|
|
73
99
|
self._graphql_type_hint = hints.get("return", None)
|
|
74
|
-
except
|
|
100
|
+
except (AttributeError, KeyError, NameError, TypeError, ValueError):
|
|
75
101
|
self._graphql_type_hint = None
|
|
76
102
|
|
|
77
103
|
@property
|
general_manager/apps.py
CHANGED
|
@@ -13,12 +13,35 @@ from general_manager.manager.meta import GeneralManagerMeta
|
|
|
13
13
|
from general_manager.manager.input import Input
|
|
14
14
|
from general_manager.api.property import graphQlProperty
|
|
15
15
|
from general_manager.api.graphql import GraphQL
|
|
16
|
-
from typing import TYPE_CHECKING, Any, Type, cast
|
|
16
|
+
from typing import TYPE_CHECKING, Any, Callable, Type, cast
|
|
17
17
|
from django.core.checks import register
|
|
18
18
|
import logging
|
|
19
19
|
from django.core.management.base import BaseCommand
|
|
20
20
|
|
|
21
21
|
|
|
22
|
+
class MissingRootUrlconfError(RuntimeError):
|
|
23
|
+
"""Raised when Django settings do not define ROOT_URLCONF."""
|
|
24
|
+
|
|
25
|
+
def __init__(self) -> None:
|
|
26
|
+
"""
|
|
27
|
+
Initialize the MissingRootUrlconfError with a default message indicating ROOT_URLCONF is missing from Django settings.
|
|
28
|
+
"""
|
|
29
|
+
super().__init__("ROOT_URLCONF not found in settings.")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class InvalidPermissionClassError(TypeError):
|
|
33
|
+
"""Raised when a GeneralManager Permission attribute is not a BasePermission subclass."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, permission_name: str) -> None:
|
|
36
|
+
"""
|
|
37
|
+
Create an InvalidPermissionClassError indicating a permission is not a subclass of BasePermission.
|
|
38
|
+
|
|
39
|
+
Parameters:
|
|
40
|
+
permission_name (str): The name of the permission (typically the class or attribute name) that failed validation. The exception message will be "`{permission_name} must be a subclass of BasePermission.`"
|
|
41
|
+
"""
|
|
42
|
+
super().__init__(f"{permission_name} must be a subclass of BasePermission.")
|
|
43
|
+
|
|
44
|
+
|
|
22
45
|
if TYPE_CHECKING:
|
|
23
46
|
from general_manager.interface.readOnlyInterface import ReadOnlyInterface
|
|
24
47
|
|
|
@@ -48,24 +71,45 @@ class GeneralmanagerConfig(AppConfig):
|
|
|
48
71
|
read_only_classes: list[Type[GeneralManager]],
|
|
49
72
|
) -> None:
|
|
50
73
|
"""
|
|
51
|
-
|
|
74
|
+
Configure synchronization and register schema checks for read-only GeneralManager classes.
|
|
52
75
|
|
|
53
|
-
|
|
76
|
+
Parameters:
|
|
77
|
+
read_only_classes (list[Type[GeneralManager]]): GeneralManager subclasses whose Interface implements a ReadOnlyInterface; each class will have its read-only data synchronized before management commands and a Django system check registered to verify the Interface schema is up to date.
|
|
54
78
|
"""
|
|
55
79
|
GeneralmanagerConfig.patchReadOnlyInterfaceSync(read_only_classes)
|
|
56
80
|
from general_manager.interface.readOnlyInterface import ReadOnlyInterface
|
|
57
81
|
|
|
58
82
|
logger.debug("starting to register ReadOnlyInterface schema warnings...")
|
|
83
|
+
|
|
84
|
+
def _build_schema_check(
|
|
85
|
+
manager_cls: Type[GeneralManager], model: Any
|
|
86
|
+
) -> Callable[..., list[Any]]:
|
|
87
|
+
"""
|
|
88
|
+
Builds a Django system check callable that verifies the read-only interface schema for a manager against a model is current.
|
|
89
|
+
|
|
90
|
+
Parameters:
|
|
91
|
+
manager_cls (Type[GeneralManager]): The GeneralManager class whose ReadOnlyInterface schema will be validated.
|
|
92
|
+
model (Any): The model (or model-like descriptor) to check the schema against.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Callable[..., list[Any]]: A callable suitable for Django's system checks framework that returns a list of check messages.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
def schema_check(*_: Any, **__: Any) -> list[Any]:
|
|
99
|
+
return ReadOnlyInterface.ensureSchemaIsUpToDate(manager_cls, model)
|
|
100
|
+
|
|
101
|
+
return schema_check
|
|
102
|
+
|
|
59
103
|
for general_manager_class in read_only_classes:
|
|
60
104
|
read_only_interface = cast(
|
|
61
105
|
Type[ReadOnlyInterface], general_manager_class.Interface
|
|
62
106
|
)
|
|
63
107
|
|
|
64
|
-
register(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
108
|
+
register("general_manager")(
|
|
109
|
+
_build_schema_check(
|
|
110
|
+
general_manager_class,
|
|
111
|
+
read_only_interface._model,
|
|
112
|
+
)
|
|
69
113
|
)
|
|
70
114
|
|
|
71
115
|
@staticmethod
|
|
@@ -73,9 +117,21 @@ class GeneralmanagerConfig(AppConfig):
|
|
|
73
117
|
general_manager_classes: list[Type[GeneralManager]],
|
|
74
118
|
) -> None:
|
|
75
119
|
"""
|
|
76
|
-
|
|
120
|
+
Ensure the provided GeneralManager classes' ReadOnlyInterfaces synchronize their data before any Django management command is executed.
|
|
121
|
+
|
|
122
|
+
For each class in `general_manager_classes`, calls the class's `Interface.syncData()` to keep read-only data consistent. Skips synchronization when running the autoreload subprocess of `runserver`.
|
|
123
|
+
Parameters:
|
|
124
|
+
general_manager_classes (list[Type[GeneralManager]]): GeneralManager subclasses whose `Interface` implements `syncData`.
|
|
125
|
+
"""
|
|
126
|
+
"""
|
|
127
|
+
Wrap BaseCommand.run_from_argv to call `syncData()` on registered ReadOnlyInterfaces before executing the original command.
|
|
77
128
|
|
|
78
|
-
|
|
129
|
+
Skips synchronization when the command is `runserver` and the process is the autoreload subprocess.
|
|
130
|
+
Parameters:
|
|
131
|
+
self (BaseCommand): The management command instance.
|
|
132
|
+
argv (list[str]): Command-line arguments for the management command.
|
|
133
|
+
Returns:
|
|
134
|
+
The result returned by the original `BaseCommand.run_from_argv` call.
|
|
79
135
|
"""
|
|
80
136
|
from general_manager.interface.readOnlyInterface import ReadOnlyInterface
|
|
81
137
|
|
|
@@ -87,13 +143,13 @@ class GeneralmanagerConfig(AppConfig):
|
|
|
87
143
|
) -> None:
|
|
88
144
|
# Ensure syncData is only called at real run of runserver
|
|
89
145
|
"""
|
|
90
|
-
|
|
146
|
+
Synchronizes all registered ReadOnlyInterface data before running a Django management command, except when running the autoreload subprocess of `runserver`.
|
|
91
147
|
|
|
92
148
|
Parameters:
|
|
93
|
-
argv (list):
|
|
149
|
+
argv (list[str]): The management command `argv`, including the program name and command.
|
|
94
150
|
|
|
95
151
|
Returns:
|
|
96
|
-
The
|
|
152
|
+
The value returned by the original `BaseCommand.run_from_argv` invocation.
|
|
97
153
|
"""
|
|
98
154
|
run_main = os.environ.get("RUN_MAIN") == "true"
|
|
99
155
|
command = argv[1] if len(argv) > 1 else None
|
|
@@ -110,7 +166,7 @@ class GeneralmanagerConfig(AppConfig):
|
|
|
110
166
|
result = original_run_from_argv(self, argv)
|
|
111
167
|
return result
|
|
112
168
|
|
|
113
|
-
|
|
169
|
+
BaseCommand.run_from_argv = run_from_argv_with_sync # type: ignore[assignment]
|
|
114
170
|
|
|
115
171
|
@staticmethod
|
|
116
172
|
def initializeGeneralManagerClasses(
|
|
@@ -118,16 +174,40 @@ class GeneralmanagerConfig(AppConfig):
|
|
|
118
174
|
all_classes: list[Type[GeneralManager]],
|
|
119
175
|
) -> None:
|
|
120
176
|
"""
|
|
121
|
-
|
|
177
|
+
Initialize GeneralManager classes' interface attributes, create attribute-based accessors, wire GraphQL connection properties between related managers, and validate each class's permission configuration.
|
|
122
178
|
|
|
123
|
-
For each class
|
|
179
|
+
For each class in `pending_attribute_initialization` this assigns the class's Interface attributes to its internal `_attributes` and creates property accessors for those attributes. For each class in `all_classes` this scans its Interface `input_fields` for inputs whose type is another GeneralManager subclass and adds a GraphQL property on the connected manager that resolves related objects filtered by the input attribute. Finally, validate and normalize the Permission attribute on every class via GeneralmanagerConfig.checkPermissionClass.
|
|
180
|
+
|
|
181
|
+
Parameters:
|
|
182
|
+
pending_attribute_initialization (list[type[GeneralManager]]): GeneralManager classes whose Interface attributes need to be initialized and whose attribute properties should be created.
|
|
183
|
+
all_classes (list[type[GeneralManager]]): All registered GeneralManager classes to inspect for input-field connections and to validate permissions.
|
|
124
184
|
"""
|
|
125
185
|
logger.debug("Initializing GeneralManager classes...")
|
|
126
186
|
|
|
187
|
+
def _build_connection_resolver(
|
|
188
|
+
attribute_key: str, manager_cls: Type[GeneralManager]
|
|
189
|
+
) -> Callable[[object], Any]:
|
|
190
|
+
"""
|
|
191
|
+
Create a resolver that queries the given GeneralManager class by matching an attribute to a provided value.
|
|
192
|
+
|
|
193
|
+
Parameters:
|
|
194
|
+
attribute_key (str): Name of the attribute to filter on.
|
|
195
|
+
manager_cls (Type[GeneralManager]): GeneralManager subclass to query.
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
Callable[[object], Any]: A callable that accepts a value and returns the result of filtering `manager_cls` where `attribute_key` equals that value.
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
def resolver(value: object) -> Any:
|
|
202
|
+
return manager_cls.filter(**{attribute_key: value})
|
|
203
|
+
|
|
204
|
+
resolver.__annotations__ = {"return": manager_cls}
|
|
205
|
+
return resolver
|
|
206
|
+
|
|
127
207
|
logger.debug("starting to create attributes for GeneralManager classes...")
|
|
128
208
|
for general_manager_class in pending_attribute_initialization:
|
|
129
209
|
attributes = general_manager_class.Interface.getAttributes()
|
|
130
|
-
|
|
210
|
+
general_manager_class._attributes = attributes
|
|
131
211
|
GeneralManagerMeta.createAtPropertiesForAttributes(
|
|
132
212
|
attributes.keys(), general_manager_class
|
|
133
213
|
)
|
|
@@ -140,15 +220,13 @@ class GeneralmanagerConfig(AppConfig):
|
|
|
140
220
|
attribute.type, GeneralManager
|
|
141
221
|
):
|
|
142
222
|
connected_manager = attribute.type
|
|
143
|
-
|
|
144
|
-
|
|
223
|
+
resolver = _build_connection_resolver(
|
|
224
|
+
attribute_name, general_manager_class
|
|
145
225
|
)
|
|
146
|
-
|
|
147
|
-
func.__annotations__ = {"return": general_manager_class}
|
|
148
226
|
setattr(
|
|
149
227
|
connected_manager,
|
|
150
228
|
f"{general_manager_class.__name__.lower()}_list",
|
|
151
|
-
graphQlProperty(
|
|
229
|
+
graphQlProperty(resolver),
|
|
152
230
|
)
|
|
153
231
|
for general_manager_class in all_classes:
|
|
154
232
|
GeneralmanagerConfig.checkPermissionClass(general_manager_class)
|
|
@@ -158,10 +236,10 @@ class GeneralmanagerConfig(AppConfig):
|
|
|
158
236
|
pending_graphql_interfaces: list[Type[GeneralManager]],
|
|
159
237
|
) -> None:
|
|
160
238
|
"""
|
|
161
|
-
|
|
162
|
-
|
|
239
|
+
Generate GraphQL types, assemble the GraphQL schema for the given manager classes, and expose the GraphQL endpoint in the project's URL configuration.
|
|
240
|
+
|
|
163
241
|
Parameters:
|
|
164
|
-
pending_graphql_interfaces (list[Type[GeneralManager]]): GeneralManager classes
|
|
242
|
+
pending_graphql_interfaces (list[Type[GeneralManager]]): GeneralManager classes for which GraphQL interfaces, mutations, and optional subscriptions should be created and included in the assembled schema.
|
|
165
243
|
"""
|
|
166
244
|
logger.debug("Starting to create GraphQL interfaces and mutations...")
|
|
167
245
|
for general_manager_class in pending_graphql_interfaces:
|
|
@@ -206,19 +284,19 @@ class GeneralmanagerConfig(AppConfig):
|
|
|
206
284
|
@staticmethod
|
|
207
285
|
def addGraphqlUrl(schema: graphene.Schema) -> None:
|
|
208
286
|
"""
|
|
209
|
-
|
|
287
|
+
Add a GraphQL endpoint to the project's URL configuration and ensure the ASGI subscription route is configured.
|
|
210
288
|
|
|
211
289
|
Parameters:
|
|
212
|
-
schema:
|
|
290
|
+
schema (graphene.Schema): GraphQL schema to serve at the configured GRAPHQL_URL.
|
|
213
291
|
|
|
214
292
|
Raises:
|
|
215
|
-
|
|
293
|
+
MissingRootUrlconfError: If ROOT_URLCONF is not defined in Django settings.
|
|
216
294
|
"""
|
|
217
295
|
logging.debug("Adding GraphQL URL to Django settings...")
|
|
218
296
|
root_url_conf_path = getattr(settings, "ROOT_URLCONF", None)
|
|
219
297
|
graph_ql_url = getattr(settings, "GRAPHQL_URL", "graphql")
|
|
220
298
|
if not root_url_conf_path:
|
|
221
|
-
raise
|
|
299
|
+
raise MissingRootUrlconfError()
|
|
222
300
|
urlconf = import_module(root_url_conf_path)
|
|
223
301
|
urlconf.urlpatterns.append(
|
|
224
302
|
path(
|
|
@@ -230,6 +308,13 @@ class GeneralmanagerConfig(AppConfig):
|
|
|
230
308
|
|
|
231
309
|
@staticmethod
|
|
232
310
|
def _ensure_asgi_subscription_route(graphql_url: str) -> None:
|
|
311
|
+
"""
|
|
312
|
+
Ensure GraphQL websocket subscription route is integrated into the project's ASGI application when ASGI is configured.
|
|
313
|
+
|
|
314
|
+
If settings.ASGI_APPLICATION is absent or invalid, the function logs and returns without making changes. When the ASGI application module can be imported, delegates to GeneralmanagerConfig._finalize_asgi_module to add or wire WebSocket routes for GraphQL subscriptions at the given URL. If the ASGI module cannot be imported during Django's application population phase, the function arranges a deferred finalization so the ASGI module will be finalized once it is loaded; other import failures are logged and cause a no-op return.
|
|
315
|
+
Parameters:
|
|
316
|
+
graphql_url (str): URL path at which the GraphQL websocket subscription endpoint should be exposed (e.g., "/graphql/").
|
|
317
|
+
"""
|
|
233
318
|
asgi_path = getattr(settings, "ASGI_APPLICATION", None)
|
|
234
319
|
if not asgi_path:
|
|
235
320
|
logger.debug("ASGI_APPLICATION not configured; skipping websocket setup.")
|
|
@@ -249,7 +334,10 @@ class GeneralmanagerConfig(AppConfig):
|
|
|
249
334
|
except RuntimeError as exc:
|
|
250
335
|
if "populate() isn't reentrant" not in str(exc):
|
|
251
336
|
logger.warning(
|
|
252
|
-
"Unable to import ASGI module '%s': %s",
|
|
337
|
+
"Unable to import ASGI module '%s': %s",
|
|
338
|
+
module_path,
|
|
339
|
+
exc,
|
|
340
|
+
exc_info=True,
|
|
253
341
|
)
|
|
254
342
|
return
|
|
255
343
|
|
|
@@ -262,10 +350,26 @@ class GeneralmanagerConfig(AppConfig):
|
|
|
262
350
|
return
|
|
263
351
|
|
|
264
352
|
def finalize(module: Any) -> None:
|
|
265
|
-
|
|
353
|
+
"""
|
|
354
|
+
Finalize integration of GraphQL subscription routing into the given ASGI module using the surrounding attribute name and GraphQL URL.
|
|
355
|
+
|
|
356
|
+
Ensure the ASGI module exposes websocket_urlpatterns and add a websocket route for the GraphQL subscription endpoint if missing; modify or wrap the module's application mapping or ASGI application as needed to include an authenticated websocket route for subscriptions.
|
|
357
|
+
|
|
358
|
+
Parameters:
|
|
359
|
+
module (Any): The imported ASGI module object to be modified in-place to support GraphQL websocket subscriptions.
|
|
360
|
+
"""
|
|
361
|
+
GeneralmanagerConfig._finalize_asgi_module(
|
|
362
|
+
module, attr_name, graphql_url
|
|
363
|
+
)
|
|
266
364
|
|
|
267
365
|
class _Loader(importlib.abc.Loader):
|
|
268
366
|
def __init__(self, original_loader: importlib.abc.Loader) -> None:
|
|
367
|
+
"""
|
|
368
|
+
Initialize the wrapper with the given original importlib loader.
|
|
369
|
+
|
|
370
|
+
Parameters:
|
|
371
|
+
original_loader (importlib.abc.Loader): The underlying loader that this wrapper will delegate to.
|
|
372
|
+
"""
|
|
269
373
|
self._original_loader = original_loader
|
|
270
374
|
|
|
271
375
|
def create_module(self, spec): # type: ignore[override]
|
|
@@ -284,18 +388,33 @@ class GeneralmanagerConfig(AppConfig):
|
|
|
284
388
|
self._processed = False
|
|
285
389
|
|
|
286
390
|
def find_spec(self, fullname, path, target=None): # type: ignore[override]
|
|
391
|
+
"""
|
|
392
|
+
Return a ModuleSpec for the wrapped loader the first time the specified module is requested.
|
|
393
|
+
|
|
394
|
+
If `fullname` matches the loader's target module name and this loader has not yet produced a spec, mark the loader as processed, create a new spec using the wrapped loader, copy `submodule_search_locations` from the original `spec` if present, and return the new spec; otherwise return `None`.
|
|
395
|
+
|
|
396
|
+
Parameters:
|
|
397
|
+
fullname (str): Fully-qualified name of the module being imported; matched against the loader's target module name.
|
|
398
|
+
path (Sequence[str] | None): Import path for package search (not used by this finder).
|
|
399
|
+
target (ModuleSpec | None): Optional existing spec suggested by the import machinery (ignored).
|
|
400
|
+
|
|
401
|
+
Returns:
|
|
402
|
+
ModuleSpec | None: The created ModuleSpec when `fullname` matches and has not been processed, `None` otherwise.
|
|
403
|
+
"""
|
|
287
404
|
if fullname != module_path or self._processed:
|
|
288
405
|
return None
|
|
289
406
|
self._processed = True
|
|
290
407
|
new_spec = util.spec_from_loader(fullname, wrapped_loader)
|
|
291
408
|
if new_spec is not None:
|
|
292
|
-
new_spec.submodule_search_locations =
|
|
409
|
+
new_spec.submodule_search_locations = (
|
|
410
|
+
spec.submodule_search_locations
|
|
411
|
+
)
|
|
293
412
|
return new_spec
|
|
294
413
|
|
|
295
414
|
finder = _Finder()
|
|
296
415
|
sys.meta_path.insert(0, finder)
|
|
297
416
|
return
|
|
298
|
-
except
|
|
417
|
+
except ImportError as exc: # pragma: no cover - defensive
|
|
299
418
|
logger.warning(
|
|
300
419
|
"Unable to import ASGI module '%s': %s", module_path, exc, exc_info=True
|
|
301
420
|
)
|
|
@@ -304,14 +423,29 @@ class GeneralmanagerConfig(AppConfig):
|
|
|
304
423
|
GeneralmanagerConfig._finalize_asgi_module(asgi_module, attr_name, graphql_url)
|
|
305
424
|
|
|
306
425
|
@staticmethod
|
|
307
|
-
def _finalize_asgi_module(
|
|
426
|
+
def _finalize_asgi_module(
|
|
427
|
+
asgi_module: Any, attr_name: str, graphql_url: str
|
|
428
|
+
) -> None:
|
|
429
|
+
"""
|
|
430
|
+
Ensure the ASGI module exposes a websocket route for GraphQL subscriptions and integrate it into the ASGI application.
|
|
431
|
+
|
|
432
|
+
If Channels and the GraphQL subscription consumer are available, this function appends a websocket URL pattern for the GraphQL subscriptions to asgi_module.websocket_urlpatterns (creating the list if needed) and then wires that websocket route into the module's ASGI application. If the module exposes an application mapping (application.application_mapping is a dict) the websocket entry is added there; otherwise the existing application is wrapped in a ProtocolTypeRouter that routes websocket traffic through Channels' AuthMiddlewareStack. If optional dependencies are missing, or websocket_urlpatterns cannot be appended, the function returns without modifying the ASGI module.
|
|
433
|
+
|
|
434
|
+
Parameters:
|
|
435
|
+
asgi_module (module): The imported ASGI module object referenced by ASGI_APPLICATION in settings.
|
|
436
|
+
attr_name (str): Attribute name on asgi_module that holds the ASGI application (e.g., "application").
|
|
437
|
+
graphql_url (str): URL path (relative, may include leading/trailing slashes) at which the GraphQL HTTP endpoint is mounted; used to build the websocket route for subscriptions.
|
|
438
|
+
"""
|
|
308
439
|
try:
|
|
309
440
|
from channels.auth import AuthMiddlewareStack # type: ignore[import-untyped]
|
|
310
441
|
from channels.routing import ProtocolTypeRouter, URLRouter # type: ignore[import-untyped]
|
|
311
442
|
from general_manager.api.graphql_subscription_consumer import (
|
|
312
443
|
GraphQLSubscriptionConsumer,
|
|
313
444
|
)
|
|
314
|
-
except
|
|
445
|
+
except (
|
|
446
|
+
ImportError,
|
|
447
|
+
RuntimeError,
|
|
448
|
+
) as exc: # pragma: no cover - optional dependency
|
|
315
449
|
logger.debug(
|
|
316
450
|
"Channels or GraphQL subscription consumer unavailable (%s); skipping websocket setup.",
|
|
317
451
|
exc,
|
|
@@ -321,7 +455,7 @@ class GeneralmanagerConfig(AppConfig):
|
|
|
321
455
|
websocket_patterns = getattr(asgi_module, "websocket_urlpatterns", None)
|
|
322
456
|
if websocket_patterns is None:
|
|
323
457
|
websocket_patterns = []
|
|
324
|
-
|
|
458
|
+
asgi_module.websocket_urlpatterns = websocket_patterns
|
|
325
459
|
|
|
326
460
|
if not hasattr(websocket_patterns, "append"):
|
|
327
461
|
logger.warning(
|
|
@@ -338,17 +472,16 @@ class GeneralmanagerConfig(AppConfig):
|
|
|
338
472
|
for route in websocket_patterns
|
|
339
473
|
)
|
|
340
474
|
if not route_exists:
|
|
341
|
-
websocket_route = re_path(pattern, GraphQLSubscriptionConsumer.as_asgi())
|
|
342
|
-
|
|
475
|
+
websocket_route = re_path(pattern, GraphQLSubscriptionConsumer.as_asgi()) # type: ignore[arg-type]
|
|
476
|
+
websocket_route._general_manager_graphql_ws = True
|
|
343
477
|
websocket_patterns.append(websocket_route)
|
|
344
478
|
|
|
345
479
|
application = getattr(asgi_module, attr_name, None)
|
|
346
480
|
if application is None:
|
|
347
481
|
return
|
|
348
482
|
|
|
349
|
-
if (
|
|
350
|
-
|
|
351
|
-
and isinstance(application.application_mapping, dict)
|
|
483
|
+
if hasattr(application, "application_mapping") and isinstance(
|
|
484
|
+
application.application_mapping, dict
|
|
352
485
|
):
|
|
353
486
|
application.application_mapping["websocket"] = AuthMiddlewareStack(
|
|
354
487
|
URLRouter(list(websocket_patterns))
|
|
@@ -367,8 +500,15 @@ class GeneralmanagerConfig(AppConfig):
|
|
|
367
500
|
@staticmethod
|
|
368
501
|
def checkPermissionClass(general_manager_class: Type[GeneralManager]) -> None:
|
|
369
502
|
"""
|
|
370
|
-
|
|
371
|
-
|
|
503
|
+
Validate and normalize a GeneralManager class's Permission attribute.
|
|
504
|
+
|
|
505
|
+
If the class defines a Permission attribute, ensure it is a subclass of BasePermission and leave it assigned; if it is not a subclass, raise InvalidPermissionClassError. If the class does not define Permission, assign ManagerBasedPermission as the default.
|
|
506
|
+
|
|
507
|
+
Parameters:
|
|
508
|
+
general_manager_class (Type[GeneralManager]): GeneralManager subclass whose Permission attribute will be validated or initialized.
|
|
509
|
+
|
|
510
|
+
Raises:
|
|
511
|
+
InvalidPermissionClassError: If the existing Permission attribute is not a subclass of BasePermission.
|
|
372
512
|
"""
|
|
373
513
|
from general_manager.permission.basePermission import BasePermission
|
|
374
514
|
from general_manager.permission.managerBasedPermission import (
|
|
@@ -377,10 +517,11 @@ class GeneralmanagerConfig(AppConfig):
|
|
|
377
517
|
|
|
378
518
|
if hasattr(general_manager_class, "Permission"):
|
|
379
519
|
permission = general_manager_class.Permission
|
|
380
|
-
if not
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
)
|
|
520
|
+
if not (
|
|
521
|
+
isinstance(permission, type) and issubclass(permission, BasePermission)
|
|
522
|
+
):
|
|
523
|
+
permission_name = getattr(permission, "__name__", repr(permission))
|
|
524
|
+
raise InvalidPermissionClassError(permission_name)
|
|
384
525
|
general_manager_class.Permission = permission
|
|
385
526
|
else:
|
|
386
527
|
general_manager_class.Permission = ManagerBasedPermission
|
|
@@ -12,10 +12,19 @@ __all__ = list(BUCKET_EXPORTS)
|
|
|
12
12
|
_MODULE_MAP = BUCKET_EXPORTS
|
|
13
13
|
|
|
14
14
|
if TYPE_CHECKING:
|
|
15
|
-
from general_manager._types.bucket import * # noqa:
|
|
15
|
+
from general_manager._types.bucket import * # noqa: F403
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
def __getattr__(name: str) -> Any:
|
|
19
|
+
"""
|
|
20
|
+
Dynamically resolve and return a named bucket export from this module's public API.
|
|
21
|
+
|
|
22
|
+
Parameters:
|
|
23
|
+
name (str): The attribute name to resolve from the module's exports.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Any: The resolved export object for `name`.
|
|
27
|
+
"""
|
|
19
28
|
return resolve_export(
|
|
20
29
|
name,
|
|
21
30
|
module_all=__all__,
|