GeneralManager 0.16.1__py3-none-any.whl → 0.17.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.

@@ -0,0 +1,429 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import contextlib
5
+ from types import SimpleNamespace
6
+ from typing import Any, cast
7
+ from channels.generic.websocket import AsyncJsonWebsocketConsumer # type: ignore[import-untyped]
8
+ from graphql import (
9
+ ExecutionResult,
10
+ GraphQLError,
11
+ GraphQLSchema,
12
+ parse,
13
+ subscribe,
14
+ )
15
+
16
+ from general_manager.api.graphql import GraphQL
17
+
18
+
19
+ class GraphQLSubscriptionConsumer(AsyncJsonWebsocketConsumer):
20
+ """
21
+ Websocket consumer implementing the ``graphql-transport-ws`` protocol for GraphQL subscriptions.
22
+
23
+ The consumer streams results produced by the dynamically generated GeneralManager GraphQL schema so
24
+ clients such as GraphiQL can subscribe to live updates.
25
+ """
26
+
27
+ connection_acknowledged: bool
28
+ connection_params: dict[str, Any]
29
+
30
+ async def connect(self) -> None:
31
+ """
32
+ Initialize connection state and accept the WebSocket, preferring the "graphql-transport-ws" subprotocol when offered.
33
+
34
+ Sets up initial flags and containers used for subscription management (connection_acknowledged, connection_params, active_subscriptions) and accepts the WebSocket connection with the selected subprotocol.
35
+ """
36
+ self.connection_acknowledged = False
37
+ self.connection_params = {}
38
+ self.active_subscriptions: dict[str, asyncio.Task[None]] = {}
39
+ subprotocols = self.scope.get("subprotocols", [])
40
+ selected_subprotocol = (
41
+ "graphql-transport-ws" if "graphql-transport-ws" in subprotocols else None
42
+ )
43
+ await self.accept(subprotocol=selected_subprotocol)
44
+
45
+ async def disconnect(self, code: int) -> None:
46
+ """
47
+ Perform cleanup when the WebSocket disconnects by cancelling and awaiting any active subscription tasks and clearing the subscription registry.
48
+
49
+ Parameters:
50
+ code (int): The WebSocket close code received from the connection.
51
+ """
52
+ tasks = list(self.active_subscriptions.values())
53
+ for task in tasks:
54
+ task.cancel()
55
+ for task in tasks:
56
+ with contextlib.suppress(asyncio.CancelledError):
57
+ await task
58
+ self.active_subscriptions.clear()
59
+
60
+ async def receive_json(self, content: dict[str, Any], **_: Any) -> None:
61
+ """
62
+ Dispatch incoming graphql-transport-ws protocol messages to the appropriate handler.
63
+
64
+ Parameters:
65
+ content (dict[str, Any]): The received JSON message; expected to include a "type" key
66
+ whose value is one of "connection_init", "ping", "subscribe", or "complete".
67
+ Messages with an unrecognized or missing "type" cause the connection to be closed
68
+ with code 4400.
69
+ """
70
+ message_type = content.get("type")
71
+ if message_type == "connection_init":
72
+ await self._handle_connection_init(content)
73
+ elif message_type == "ping":
74
+ await self._handle_ping(content)
75
+ elif message_type == "subscribe":
76
+ await self._handle_subscribe(content)
77
+ elif message_type == "complete":
78
+ await self._handle_complete(content)
79
+ else:
80
+ await self.close(code=4400)
81
+
82
+ async def _handle_connection_init(self, content: dict[str, Any]) -> None:
83
+ """
84
+ Handle a client's "connection_init" message and send a protocol acknowledgment.
85
+
86
+ If the connection has already been acknowledged, closes the WebSocket with code 4429.
87
+ If the incoming message contains a "payload" that is a dict, stores it on
88
+ self.connection_params; otherwise clears connection_params. Marks the connection
89
+ as acknowledged and sends a "connection_ack" protocol message.
90
+
91
+ Parameters:
92
+ content (dict[str, Any]): The received WebSocket message for "connection_init".
93
+ """
94
+ if self.connection_acknowledged:
95
+ await self.close(code=4429)
96
+ return
97
+ payload = content.get("payload")
98
+ if isinstance(payload, dict):
99
+ self.connection_params = payload
100
+ else:
101
+ self.connection_params = {}
102
+ self.connection_acknowledged = True
103
+ await self._send_protocol_message({"type": "connection_ack"})
104
+
105
+ async def _handle_ping(self, content: dict[str, Any]) -> None:
106
+ """
107
+ Responds to an incoming ping by sending a pong protocol message.
108
+
109
+ If the incoming `content` contains a `"payload"` key, its value is included in the sent pong message under the same key.
110
+
111
+ Parameters:
112
+ content (dict[str, Any]): The received message object; may include an optional `"payload"` to echo.
113
+ """
114
+ payload = content.get("payload")
115
+ response: dict[str, Any] = {"type": "pong"}
116
+ if payload is not None:
117
+ response["payload"] = payload
118
+ await self._send_protocol_message(response)
119
+
120
+ async def _handle_subscribe(self, content: dict[str, Any]) -> None:
121
+ """
122
+ Handle an incoming GraphQL "subscribe" message and start or send the corresponding subscription results.
123
+
124
+ Validates the connection state and the message shape, checks that a subscription-capable schema is available, parses and executes the GraphQL operation, and either streams resulting events to the client or sends a single execution result. On protocol or execution errors the method sends appropriate "error" and "complete" protocol messages. If an active subscription exists for the same operation id it is stopped before the new subscription is started.
125
+
126
+ Parameters:
127
+ content (dict[str, Any]): The incoming protocol message. Expected keys:
128
+ - "id" (str): The operation identifier.
129
+ - "payload" (dict): Operation payload containing:
130
+ - "query" (str): GraphQL query string (required).
131
+ - "variables" (dict, optional): Operation variables.
132
+ - "operationName" (str, optional): Named operation to execute.
133
+ """
134
+ if not self.connection_acknowledged:
135
+ await self.close(code=4401)
136
+ return
137
+
138
+ operation_id = content.get("id")
139
+ payload = content.get("payload", {})
140
+ if not isinstance(operation_id, str) or not isinstance(payload, dict):
141
+ await self.close(code=4403)
142
+ return
143
+
144
+ schema = GraphQL.get_schema()
145
+ if schema is None or self._schema_has_no_subscription(schema.graphql_schema):
146
+ await self._send_protocol_message(
147
+ {
148
+ "type": "error",
149
+ "id": operation_id,
150
+ "payload": [
151
+ {"message": "GraphQL subscriptions are not configured."}
152
+ ],
153
+ }
154
+ )
155
+ await self._send_protocol_message(
156
+ {"type": "complete", "id": operation_id}
157
+ )
158
+ return
159
+
160
+ query = payload.get("query")
161
+ if not isinstance(query, str):
162
+ await self._send_protocol_message(
163
+ {
164
+ "type": "error",
165
+ "id": operation_id,
166
+ "payload": [{"message": "A GraphQL query string is required."}],
167
+ }
168
+ )
169
+ await self._send_protocol_message(
170
+ {"type": "complete", "id": operation_id}
171
+ )
172
+ return
173
+
174
+ variables = payload.get("variables")
175
+ if variables is not None and not isinstance(variables, dict):
176
+ await self._send_protocol_message(
177
+ {
178
+ "type": "error",
179
+ "id": operation_id,
180
+ "payload": [{"message": "Variables must be provided as an object."}],
181
+ }
182
+ )
183
+ await self._send_protocol_message(
184
+ {"type": "complete", "id": operation_id}
185
+ )
186
+ return
187
+
188
+ operation_name = payload.get("operationName")
189
+ if operation_name is not None and not isinstance(operation_name, str):
190
+ await self._send_protocol_message(
191
+ {
192
+ "type": "error",
193
+ "id": operation_id,
194
+ "payload": [
195
+ {"message": "The operation name must be a string when provided."}
196
+ ],
197
+ }
198
+ )
199
+ await self._send_protocol_message(
200
+ {"type": "complete", "id": operation_id}
201
+ )
202
+ return
203
+
204
+ try:
205
+ document = parse(query)
206
+ except GraphQLError as error:
207
+ await self._send_protocol_message(
208
+ {
209
+ "type": "error",
210
+ "id": operation_id,
211
+ "payload": [error.formatted],
212
+ }
213
+ )
214
+ await self._send_protocol_message(
215
+ {"type": "complete", "id": operation_id}
216
+ )
217
+ return
218
+
219
+ context = self._build_context()
220
+
221
+ try:
222
+ subscription = await subscribe(
223
+ schema.graphql_schema,
224
+ document,
225
+ variable_values=variables,
226
+ operation_name=operation_name,
227
+ context_value=context,
228
+ )
229
+ except GraphQLError as error:
230
+ await self._send_protocol_message(
231
+ {
232
+ "type": "error",
233
+ "id": operation_id,
234
+ "payload": [error.formatted],
235
+ }
236
+ )
237
+ await self._send_protocol_message(
238
+ {"type": "complete", "id": operation_id}
239
+ )
240
+ return
241
+ except Exception as error: # pragma: no cover - defensive safeguard
242
+ await self._send_protocol_message(
243
+ {
244
+ "type": "error",
245
+ "id": operation_id,
246
+ "payload": [{"message": str(error)}],
247
+ }
248
+ )
249
+ await self._send_protocol_message(
250
+ {"type": "complete", "id": operation_id}
251
+ )
252
+ return
253
+
254
+ if isinstance(subscription, ExecutionResult):
255
+ await self._send_execution_result(operation_id, subscription)
256
+ await self._send_protocol_message(
257
+ {"type": "complete", "id": operation_id}
258
+ )
259
+ return
260
+
261
+ if operation_id in self.active_subscriptions:
262
+ await self._stop_subscription(operation_id)
263
+
264
+ self.active_subscriptions[operation_id] = asyncio.create_task(
265
+ self._stream_subscription(operation_id, subscription)
266
+ )
267
+
268
+ async def _handle_complete(self, content: dict[str, Any]) -> None:
269
+ """
270
+ Handle an incoming "complete" protocol message by stopping the subscription for the specified operation.
271
+
272
+ If the message payload contains an "id" field that is a string, the corresponding active subscription task is cancelled and cleaned up; otherwise the message is ignored.
273
+
274
+ Parameters:
275
+ content (dict[str, Any]): The received protocol message payload. Expected to contain an "id" key with the operation identifier.
276
+ """
277
+ operation_id = content.get("id")
278
+ if isinstance(operation_id, str):
279
+ await self._stop_subscription(operation_id)
280
+
281
+ async def _stream_subscription(
282
+ self, operation_id: str, async_iterator: Any
283
+ ) -> None:
284
+ """
285
+ Stream results from an async iterator to the client for a subscription operation.
286
+
287
+ Iterates the provided async_iterator and sends each yielded execution result to the client for the given operation_id. If an unexpected exception occurs while iterating, sends an error payload for the operation. In all cases, attempts to close the iterator, sends a completion message for the operation_id, and removes the operation from active_subscriptions.
288
+
289
+ Parameters:
290
+ operation_id (str): The subscription operation identifier to use in protocol messages.
291
+ async_iterator (Any): An asynchronous iterator that yields execution result objects to be sent to the client.
292
+
293
+ Raises:
294
+ asyncio.CancelledError: Propagated if the surrounding subscription task is cancelled.
295
+ """
296
+ try:
297
+ async for result in async_iterator:
298
+ await self._send_execution_result(operation_id, result)
299
+ except asyncio.CancelledError:
300
+ raise
301
+ except Exception as error: # pragma: no cover - defensive safeguard
302
+ await self._send_protocol_message(
303
+ {
304
+ "type": "error",
305
+ "id": operation_id,
306
+ "payload": [{"message": str(error)}],
307
+ }
308
+ )
309
+ finally:
310
+ await self._close_iterator(async_iterator)
311
+ await self._send_protocol_message(
312
+ {"type": "complete", "id": operation_id}
313
+ )
314
+ self.active_subscriptions.pop(operation_id, None)
315
+
316
+ async def _stop_subscription(self, operation_id: str) -> None:
317
+ """
318
+ Cancel and await the active subscription task for the given operation id, if one exists.
319
+
320
+ If a task is found for operation_id it is cancelled and awaited; CancelledError raised during awaiting is suppressed. If no task exists this is a no-op.
321
+ """
322
+ task = self.active_subscriptions.pop(operation_id, None)
323
+ if task is None:
324
+ return
325
+ task.cancel()
326
+ with contextlib.suppress(asyncio.CancelledError):
327
+ await task
328
+
329
+ async def _send_execution_result(
330
+ self, operation_id: str, result: ExecutionResult
331
+ ) -> None:
332
+ """
333
+ Send a GraphQL execution result to the client as a "next" protocol message.
334
+
335
+ The message payload includes a "data" field when result.data is present and an "errors" field when result.errors is non-empty; errors are converted to serializable dictionaries.
336
+
337
+ Parameters:
338
+ operation_id (str): The operation identifier to include as the message `id`.
339
+ result (ExecutionResult): The GraphQL execution result to serialize and send.
340
+ """
341
+ payload: dict[str, Any] = {}
342
+ if result.data is not None:
343
+ payload["data"] = result.data
344
+ if result.errors:
345
+ payload["errors"] = [self._format_error(error) for error in result.errors]
346
+ await self._send_protocol_message(
347
+ {"type": "next", "id": operation_id, "payload": payload}
348
+ )
349
+
350
+ async def _send_protocol_message(self, message: dict[str, Any]) -> None:
351
+ """
352
+ Send a JSON-serializable GraphQL transport protocol message over the WebSocket.
353
+
354
+ Parameters:
355
+ message (dict[str, Any]): The protocol message to send. If the connection is already closed, the message is discarded silently.
356
+ """
357
+ try:
358
+ await self.send_json(message)
359
+ except RuntimeError:
360
+ # The connection has already been closed. There is nothing else to send.
361
+ pass
362
+
363
+ def _build_context(self) -> Any:
364
+ """
365
+ Builds a request context object for GraphQL execution containing the current user, decoded headers, scope, and connection parameters.
366
+
367
+ Returns:
368
+ context (SimpleNamespace): An object with attributes:
369
+ - `user`: the value of `scope["user"]` (may be None).
370
+ - `headers`: a dict mapping header names to decoded string values.
371
+ - `scope`: the consumer's `scope`.
372
+ - `connection_params`: the connection parameters provided during `connection_init`.
373
+ """
374
+ user = self.scope.get("user")
375
+ raw_headers = self.scope.get("headers") or []
376
+ headers = {
377
+ (key.decode("latin1") if isinstance(key, (bytes, bytearray)) else key): (
378
+ value.decode("latin1") if isinstance(value, (bytes, bytearray)) else value
379
+ )
380
+ for key, value in raw_headers
381
+ }
382
+ return SimpleNamespace(
383
+ user=user,
384
+ headers=headers,
385
+ scope=self.scope,
386
+ connection_params=self.connection_params,
387
+ )
388
+
389
+ @staticmethod
390
+ def _schema_has_no_subscription(schema: GraphQLSchema) -> bool:
391
+ """
392
+ Check whether the provided GraphQL schema defines no subscription root type.
393
+
394
+ Parameters:
395
+ schema (GraphQLSchema): The schema to inspect.
396
+
397
+ Returns:
398
+ bool: `True` if the schema has no subscription type, `False` otherwise.
399
+ """
400
+ return schema.subscription_type is None
401
+
402
+ @staticmethod
403
+ def _format_error(error: Exception) -> dict[str, Any]:
404
+ """
405
+ Format an exception into a GraphQL-compatible error dictionary.
406
+
407
+ Parameters:
408
+ error (Exception): The exception to format; may be a GraphQLError.
409
+
410
+ Returns:
411
+ dict[str, Any]: If `error` is a `GraphQLError`, returns its `formatted` representation.
412
+ Otherwise returns a dictionary with a single `"message"` key containing the exception's string.
413
+ """
414
+ if isinstance(error, GraphQLError):
415
+ return cast(dict[str, Any], error.formatted)
416
+ return {"message": str(error)}
417
+
418
+ @staticmethod
419
+ async def _close_iterator(async_iterator: Any) -> None:
420
+ """
421
+ Close an asynchronous iterator if it exposes an `aclose` coroutine.
422
+
423
+ Parameters:
424
+ async_iterator (Any): An asynchronous iterator; if it has an `aclose` coroutine method, that coroutine will be awaited to close the iterator.
425
+ """
426
+ close = getattr(async_iterator, "aclose", None)
427
+ if close is None:
428
+ return
429
+ await close()
general_manager/apps.py CHANGED
@@ -3,15 +3,17 @@ from django.apps import AppConfig
3
3
  import graphene # type: ignore[import]
4
4
  import os
5
5
  from django.conf import settings
6
- from django.urls import path
6
+ from django.urls import path, re_path
7
7
  from graphene_django.views import GraphQLView # type: ignore[import]
8
- from importlib import import_module
8
+ from importlib import import_module, util
9
+ import importlib.abc
10
+ import sys
9
11
  from general_manager.manager.generalManager import GeneralManager
10
12
  from general_manager.manager.meta import GeneralManagerMeta
11
13
  from general_manager.manager.input import Input
12
14
  from general_manager.api.property import graphQlProperty
13
15
  from general_manager.api.graphql import GraphQL
14
- from typing import TYPE_CHECKING, Type, cast
16
+ from typing import TYPE_CHECKING, Any, Type, cast
15
17
  from django.core.checks import register
16
18
  import logging
17
19
  from django.core.management.base import BaseCommand
@@ -156,7 +158,10 @@ class GeneralmanagerConfig(AppConfig):
156
158
  pending_graphql_interfaces: list[Type[GeneralManager]],
157
159
  ) -> None:
158
160
  """
159
- Creates GraphQL interfaces and mutations for the provided general manager classes, builds the GraphQL schema, and registers the GraphQL endpoint in the Django URL configuration.
161
+ Create GraphQL interfaces and mutations for the given manager classes, build the GraphQL schema, and add the GraphQL endpoint to the URL configuration.
162
+
163
+ Parameters:
164
+ pending_graphql_interfaces (list[Type[GeneralManager]]): GeneralManager classes that require GraphQL interface and mutation generation.
160
165
  """
161
166
  logger.debug("Starting to create GraphQL interfaces and mutations...")
162
167
  for general_manager_class in pending_graphql_interfaces:
@@ -176,13 +181,26 @@ class GeneralmanagerConfig(AppConfig):
176
181
  },
177
182
  )
178
183
  GraphQL._mutation_class = mutation_class
179
- schema = graphene.Schema(
180
- query=GraphQL._query_class,
181
- mutation=mutation_class,
182
- )
183
184
  else:
184
185
  GraphQL._mutation_class = None
185
- schema = graphene.Schema(query=GraphQL._query_class)
186
+
187
+ if GraphQL._subscription_fields:
188
+ subscription_class = type(
189
+ "Subscription",
190
+ (graphene.ObjectType,),
191
+ GraphQL._subscription_fields,
192
+ )
193
+ GraphQL._subscription_class = subscription_class
194
+ else:
195
+ GraphQL._subscription_class = None
196
+
197
+ schema_kwargs: dict[str, Any] = {"query": GraphQL._query_class}
198
+ if GraphQL._mutation_class is not None:
199
+ schema_kwargs["mutation"] = GraphQL._mutation_class
200
+ if GraphQL._subscription_class is not None:
201
+ schema_kwargs["subscription"] = GraphQL._subscription_class
202
+ schema = graphene.Schema(**schema_kwargs)
203
+ GraphQL._schema = schema
186
204
  GeneralmanagerConfig.addGraphqlUrl(schema)
187
205
 
188
206
  @staticmethod
@@ -208,6 +226,143 @@ class GeneralmanagerConfig(AppConfig):
208
226
  GraphQLView.as_view(graphiql=True, schema=schema),
209
227
  )
210
228
  )
229
+ GeneralmanagerConfig._ensure_asgi_subscription_route(graph_ql_url)
230
+
231
+ @staticmethod
232
+ def _ensure_asgi_subscription_route(graphql_url: str) -> None:
233
+ asgi_path = getattr(settings, "ASGI_APPLICATION", None)
234
+ if not asgi_path:
235
+ logger.debug("ASGI_APPLICATION not configured; skipping websocket setup.")
236
+ return
237
+
238
+ try:
239
+ module_path, attr_name = asgi_path.rsplit(".", 1)
240
+ except ValueError:
241
+ logger.warning(
242
+ "ASGI_APPLICATION '%s' is not a valid module path; skipping websocket setup.",
243
+ asgi_path,
244
+ )
245
+ return
246
+
247
+ try:
248
+ asgi_module = import_module(module_path)
249
+ except RuntimeError as exc:
250
+ if "populate() isn't reentrant" not in str(exc):
251
+ logger.warning(
252
+ "Unable to import ASGI module '%s': %s", module_path, exc, exc_info=True
253
+ )
254
+ return
255
+
256
+ spec = util.find_spec(module_path)
257
+ if spec is None or spec.loader is None:
258
+ logger.warning(
259
+ "Could not locate loader for ASGI module '%s'; skipping websocket setup.",
260
+ module_path,
261
+ )
262
+ return
263
+
264
+ def finalize(module: Any) -> None:
265
+ GeneralmanagerConfig._finalize_asgi_module(module, attr_name, graphql_url)
266
+
267
+ class _Loader(importlib.abc.Loader):
268
+ def __init__(self, original_loader: importlib.abc.Loader) -> None:
269
+ self._original_loader = original_loader
270
+
271
+ def create_module(self, spec): # type: ignore[override]
272
+ if hasattr(self._original_loader, "create_module"):
273
+ return self._original_loader.create_module(spec) # type: ignore[attr-defined]
274
+ return None
275
+
276
+ def exec_module(self, module): # type: ignore[override]
277
+ self._original_loader.exec_module(module)
278
+ finalize(module)
279
+
280
+ wrapped_loader = _Loader(spec.loader)
281
+
282
+ class _Finder(importlib.abc.MetaPathFinder):
283
+ def __init__(self) -> None:
284
+ self._processed = False
285
+
286
+ def find_spec(self, fullname, path, target=None): # type: ignore[override]
287
+ if fullname != module_path or self._processed:
288
+ return None
289
+ self._processed = True
290
+ new_spec = util.spec_from_loader(fullname, wrapped_loader)
291
+ if new_spec is not None:
292
+ new_spec.submodule_search_locations = spec.submodule_search_locations
293
+ return new_spec
294
+
295
+ finder = _Finder()
296
+ sys.meta_path.insert(0, finder)
297
+ return
298
+ except Exception as exc: # pragma: no cover - defensive
299
+ logger.warning(
300
+ "Unable to import ASGI module '%s': %s", module_path, exc, exc_info=True
301
+ )
302
+ return
303
+
304
+ GeneralmanagerConfig._finalize_asgi_module(asgi_module, attr_name, graphql_url)
305
+
306
+ @staticmethod
307
+ def _finalize_asgi_module(asgi_module: Any, attr_name: str, graphql_url: str) -> None:
308
+ try:
309
+ from channels.auth import AuthMiddlewareStack # type: ignore[import-untyped]
310
+ from channels.routing import ProtocolTypeRouter, URLRouter # type: ignore[import-untyped]
311
+ from general_manager.api.graphql_subscription_consumer import (
312
+ GraphQLSubscriptionConsumer,
313
+ )
314
+ except Exception as exc: # pragma: no cover - optional dependency
315
+ logger.debug(
316
+ "Channels or GraphQL subscription consumer unavailable (%s); skipping websocket setup.",
317
+ exc,
318
+ )
319
+ return
320
+
321
+ websocket_patterns = getattr(asgi_module, "websocket_urlpatterns", None)
322
+ if websocket_patterns is None:
323
+ websocket_patterns = []
324
+ setattr(asgi_module, "websocket_urlpatterns", websocket_patterns)
325
+
326
+ if not hasattr(websocket_patterns, "append"):
327
+ logger.warning(
328
+ "websocket_urlpatterns in '%s' does not support appending; skipping websocket setup.",
329
+ asgi_module.__name__,
330
+ )
331
+ return
332
+
333
+ normalized = graphql_url.strip("/")
334
+ pattern = rf"^{normalized}/?$" if normalized else r"^$"
335
+
336
+ route_exists = any(
337
+ getattr(route, "_general_manager_graphql_ws", False)
338
+ for route in websocket_patterns
339
+ )
340
+ if not route_exists:
341
+ websocket_route = re_path(pattern, GraphQLSubscriptionConsumer.as_asgi()) # type: ignore[arg-type]
342
+ setattr(websocket_route, "_general_manager_graphql_ws", True)
343
+ websocket_patterns.append(websocket_route)
344
+
345
+ application = getattr(asgi_module, attr_name, None)
346
+ if application is None:
347
+ return
348
+
349
+ if (
350
+ hasattr(application, "application_mapping")
351
+ and isinstance(application.application_mapping, dict)
352
+ ):
353
+ application.application_mapping["websocket"] = AuthMiddlewareStack(
354
+ URLRouter(list(websocket_patterns))
355
+ )
356
+ else:
357
+ wrapped_application = ProtocolTypeRouter(
358
+ {
359
+ "http": application,
360
+ "websocket": AuthMiddlewareStack(
361
+ URLRouter(list(websocket_patterns))
362
+ ),
363
+ }
364
+ )
365
+ setattr(asgi_module, attr_name, wrapped_application)
211
366
 
212
367
  @staticmethod
213
368
  def checkPermissionClass(general_manager_class: Type[GeneralManager]) -> None: