django-socket 0.2.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.
@@ -0,0 +1,262 @@
1
+ /*!
2
+ * django_socket - cliente con reconexion automatica.
3
+ *
4
+ * const sock = djangoSocket("/chat/general/");
5
+ * sock.on("mensaje", (datos) => pintar(datos.texto));
6
+ * sock.send({type: "mensaje", texto: "hola"});
7
+ *
8
+ * Lo que aporta frente a `new WebSocket(...)` a pelo:
9
+ * - reconecta con backoff exponencial y jitter
10
+ * - NO reconecta cuando el servidor cierra a proposito (4401 falta login,
11
+ * 4404 ruta inexistente...): ahi reintentar es un bucle infinito
12
+ * - encola lo que envies mientras esta caido y lo suelta al volver
13
+ * - JSON en los dos sentidos, y enrutado por `type` como el Events de Python
14
+ * - deja de reintentar si el navegador esta sin red (y opcionalmente
15
+ * si la pestaña esta oculta: {pauseWhenHidden: true})
16
+ */
17
+ (function (global) {
18
+ "use strict";
19
+
20
+ // Cierres que significan "no lo vuelvas a intentar".
21
+ // 1000 el servidor cerro limpiamente 1008 violacion de politica
22
+ // 4000-4999 decisiones de tu aplicacion (login, ruta, datos invalidos)
23
+ function esDefinitivo(code) {
24
+ return code === 1000 || code === 1008 || (code >= 4000 && code <= 4999);
25
+ }
26
+
27
+ function urlAbsoluta(ruta) {
28
+ if (/^wss?:\/\//.test(ruta)) return ruta;
29
+ var proto = location.protocol === "https:" ? "wss:" : "ws:";
30
+ return proto + "//" + location.host + (ruta[0] === "/" ? ruta : "/" + ruta);
31
+ }
32
+
33
+ function djangoSocket(ruta, opciones) {
34
+ var o = Object.assign(
35
+ {
36
+ key: "type", // el campo que enruta, igual que Events(key=)
37
+ reconnect: true,
38
+ minDelay: 500, // primer reintento, en ms
39
+ maxDelay: 15000, // tope del backoff
40
+ maxRetries: Infinity,
41
+ queue: true, // guardar los envios mientras no hay conexion
42
+ maxQueue: 100,
43
+ pauseWhenHidden: false, // ver la nota en esperarAEstarListo()
44
+ protocols: undefined,
45
+ onOpen: null, // (esReconexion) => {}
46
+ onMessage: null, // (datos, evento) => {} para lo que no case
47
+ onClose: null, // (evento, vaAReintentar) => {}
48
+ onError: null,
49
+ onRetry: null, // (intento, esperaMs) => {}
50
+ shouldReconnect: null, // (evento) => bool para decidirlo tu
51
+ },
52
+ opciones || {}
53
+ );
54
+
55
+ var url = urlAbsoluta(ruta);
56
+ var ws = null;
57
+ var handlers = {};
58
+ var pendientes = [];
59
+ var intentos = 0;
60
+ var temporizador = null;
61
+ var cerradoPorNosotros = false;
62
+ var haAbiertoAlgunaVez = false;
63
+
64
+ var api = {
65
+ get readyState() {
66
+ return ws ? ws.readyState : WebSocket.CLOSED;
67
+ },
68
+ get connected() {
69
+ return !!ws && ws.readyState === WebSocket.OPEN;
70
+ },
71
+ get url() {
72
+ return url;
73
+ },
74
+ get pending() {
75
+ return pendientes.length;
76
+ },
77
+ on: on,
78
+ off: off,
79
+ send: send,
80
+ close: close,
81
+ reconnect: forzarReconexion,
82
+ };
83
+
84
+ // ------------------------------------------------------------- handlers
85
+
86
+ function on(tipo, fn) {
87
+ (handlers[tipo] = handlers[tipo] || []).push(fn);
88
+ return api; // encadenable: .on(..).on(..)
89
+ }
90
+
91
+ function off(tipo, fn) {
92
+ if (!handlers[tipo]) return api;
93
+ if (!fn) delete handlers[tipo];
94
+ else handlers[tipo] = handlers[tipo].filter(function (f) { return f !== fn; });
95
+ return api;
96
+ }
97
+
98
+ function despachar(datos, evento) {
99
+ var tipo = datos && typeof datos === "object" ? datos[o.key] : undefined;
100
+ // "*" es un respaldo, no un espia: solo corre cuando nadie mas cogio el
101
+ // mensaje. Es la misma semantica que el Events de Python, y tenerlas
102
+ // distintas a cada lado del mismo protocolo es pedir un bug.
103
+ var lista = handlers[tipo] && handlers[tipo].length
104
+ ? handlers[tipo]
105
+ : handlers["*"] || [];
106
+ if (lista.length) {
107
+ lista.slice().forEach(function (fn) { fn(datos, evento); });
108
+ } else if (o.onMessage) {
109
+ o.onMessage(datos, evento);
110
+ }
111
+ }
112
+
113
+ // --------------------------------------------------------------- enviar
114
+
115
+ function send(datos) {
116
+ var cuerpo =
117
+ typeof datos === "string" || datos instanceof Blob || datos instanceof ArrayBuffer
118
+ ? datos
119
+ : JSON.stringify(datos);
120
+
121
+ if (api.connected) {
122
+ ws.send(cuerpo);
123
+ return true;
124
+ }
125
+ if (o.queue) {
126
+ if (pendientes.length >= o.maxQueue) pendientes.shift(); // tira el mas viejo
127
+ pendientes.push(cuerpo);
128
+ }
129
+ return false;
130
+ }
131
+
132
+ function vaciarCola() {
133
+ var cola = pendientes;
134
+ pendientes = [];
135
+ cola.forEach(function (cuerpo) { ws.send(cuerpo); });
136
+ }
137
+
138
+ // ---------------------------------------------------------- ciclo de vida
139
+
140
+ function conectar() {
141
+ clearTimeout(temporizador);
142
+ cerradoPorNosotros = false;
143
+ try {
144
+ ws = o.protocols ? new WebSocket(url, o.protocols) : new WebSocket(url);
145
+ } catch (err) {
146
+ if (o.onError) o.onError(err);
147
+ programarReintento({ code: 1006, reason: String(err) });
148
+ return;
149
+ }
150
+
151
+ ws.onopen = function () {
152
+ var esReconexion = haAbiertoAlgunaVez;
153
+ haAbiertoAlgunaVez = true;
154
+ intentos = 0;
155
+ vaciarCola();
156
+ if (o.onOpen) o.onOpen(esReconexion);
157
+ };
158
+
159
+ ws.onmessage = function (evento) {
160
+ var datos = evento.data;
161
+ if (typeof datos === "string") {
162
+ try {
163
+ datos = JSON.parse(datos);
164
+ } catch (e) {
165
+ /* no era JSON: se pasa el texto tal cual */
166
+ }
167
+ }
168
+ despachar(datos, evento);
169
+ };
170
+
171
+ ws.onerror = function (evento) {
172
+ if (o.onError) o.onError(evento);
173
+ };
174
+
175
+ ws.onclose = function (evento) {
176
+ var reintentara = decideReintentar(evento);
177
+ if (o.onClose) o.onClose(evento, reintentara);
178
+ if (reintentara) programarReintento(evento);
179
+ };
180
+ }
181
+
182
+ function decideReintentar(evento) {
183
+ if (cerradoPorNosotros || !o.reconnect) return false;
184
+ if (intentos >= o.maxRetries) return false;
185
+ if (o.shouldReconnect) return !!o.shouldReconnect(evento);
186
+ return !esDefinitivo(evento.code);
187
+ }
188
+
189
+ function programarReintento(evento) {
190
+ // Backoff exponencial con jitter: sin el, N clientes que se caen a la vez
191
+ // vuelven todos en el mismo instante y tumban el servidor otra vez.
192
+ var base = Math.min(o.minDelay * Math.pow(2, intentos), o.maxDelay);
193
+ var espera = Math.round(base * (0.5 + Math.random() * 0.5));
194
+ intentos += 1;
195
+ if (o.onRetry) o.onRetry(intentos, espera);
196
+
197
+ temporizador = setTimeout(function () {
198
+ if (debeEsperar()) {
199
+ esperarAEstarListo();
200
+ return;
201
+ }
202
+ conectar();
203
+ }, espera);
204
+ }
205
+
206
+ function debeEsperar() {
207
+ // Sin red no hay nada que intentar: espera al evento "online".
208
+ if (navigator.onLine === false) return true;
209
+ // Pausar por estar la pestaña oculta NO es el defecto a proposito. Un
210
+ // chat en segundo plano que deja de reconectar en silencio esta roto:
211
+ // vuelves y te has perdido todo sin un solo aviso. Actívalo tu si tu
212
+ // caso tolera quedarse atras: {pauseWhenHidden: true}.
213
+ return o.pauseWhenHidden && document.visibilityState === "hidden";
214
+ }
215
+
216
+ var esperando = false;
217
+
218
+ function esperarAEstarListo() {
219
+ if (esperando) return; // no acumules parejas de listeners
220
+ esperando = true;
221
+
222
+ function despertar() {
223
+ if (!esperando) return; // otro evento nos desperto primero
224
+ esperando = false;
225
+ window.removeEventListener("online", despertar);
226
+ document.removeEventListener("visibilitychange", alVerse);
227
+ intentos = 0; // volver es buena señal: reintenta ya
228
+ conectar();
229
+ }
230
+ function alVerse() {
231
+ if (!debeEsperar()) despertar();
232
+ }
233
+ window.addEventListener("online", despertar);
234
+ document.addEventListener("visibilitychange", alVerse);
235
+ }
236
+
237
+ function forzarReconexion() {
238
+ intentos = 0;
239
+ if (ws && ws.readyState === WebSocket.OPEN) {
240
+ cerradoPorNosotros = false;
241
+ ws.close(1000);
242
+ }
243
+ conectar();
244
+ return api;
245
+ }
246
+
247
+ function close(code, reason) {
248
+ cerradoPorNosotros = true;
249
+ clearTimeout(temporizador);
250
+ if (ws) ws.close(code || 1000, reason || "");
251
+ return api;
252
+ }
253
+
254
+ conectar();
255
+ return api;
256
+ }
257
+
258
+ djangoSocket.esDefinitivo = esDefinitivo;
259
+
260
+ global.djangoSocket = djangoSocket;
261
+ if (typeof module === "object" && module.exports) module.exports = djangoSocket;
262
+ })(typeof window !== "undefined" ? window : this);
File without changes
@@ -0,0 +1,28 @@
1
+ """Etiqueta para incluir el cliente JS.
2
+
3
+ {% load django_socket %}
4
+ {% ws_client %}
5
+
6
+ Deja disponible `djangoSocket(ruta, opciones)` en la pagina.
7
+ """
8
+
9
+ from django import template
10
+ from django.templatetags.static import static
11
+ from django.utils.html import format_html
12
+
13
+ register = template.Library()
14
+
15
+
16
+ @register.simple_tag
17
+ def ws_client(defer: bool = False):
18
+ return format_html(
19
+ '<script src="{}"{}></script>',
20
+ static("django_socket/client.js"),
21
+ " defer" if defer else "",
22
+ )
23
+
24
+
25
+ @register.simple_tag
26
+ def ws_client_url():
27
+ """Solo la URL, por si la metes en tu propio bundle."""
28
+ return static("django_socket/client.js")
@@ -0,0 +1,330 @@
1
+ """Cliente de pruebas: testea tus handlers sin levantar ningun servidor.
2
+
3
+ from django_socket.testing import WebSocketClient
4
+
5
+ async def test_el_chat_reparte():
6
+ async with WebSocketClient("/chat/general/") as a, \\
7
+ WebSocketClient("/chat/general/") as b:
8
+ await b.send_json({"type": "mensaje", "texto": "hola"})
9
+ assert (await a.receive_json())["texto"] == "hola"
10
+
11
+ Habla el protocolo ASGI directamente contra el dispatcher, asi que pasa por el
12
+ mismo camino que una conexion real -- ruta, conversores, validacion de origen,
13
+ sesion, grupos -- pero sin sockets, sin puertos y en milisegundos.
14
+
15
+ Todos los `receive` llevan timeout: un test que espera algo que no llega falla
16
+ en un segundo en vez de colgarse.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import asyncio
22
+ from typing import Any
23
+
24
+ from .websocket import InvalidJSON, Message, WebSocketDisconnect
25
+
26
+ TIMEOUT = 1.0
27
+
28
+
29
+ class TimeoutDelEsperado(AssertionError):
30
+ """No llego nada en el plazo. Es AssertionError para que lea como un fallo de test."""
31
+
32
+
33
+ class WebSocketClient:
34
+ """
35
+ Un cliente websocket de mentira contra el dispatcher de verdad.
36
+
37
+ `path` la ruta, tal cual la escribiria un navegador ("/chat/x/")
38
+ `user` un User: se le crea sesion y `sock.user` lo vera autenticado
39
+ `headers` cabeceras extra (`origin` ya va puesto)
40
+ `cookies` dict de cookies; se combina con la sesion de `user`
41
+ `query` "token=abc&n=3"
42
+ `subprotocols` los que ofreceria el navegador
43
+ """
44
+
45
+ def __init__(
46
+ self,
47
+ path: str,
48
+ *,
49
+ user=None,
50
+ headers: dict | None = None,
51
+ cookies: dict | None = None,
52
+ query: str = "",
53
+ subprotocols=(),
54
+ origin: str = "http://testserver",
55
+ ):
56
+ self.path = path
57
+ self.user = user
58
+ self._headers = dict(headers or {})
59
+ self._cookies = dict(cookies or {})
60
+ self._query = query
61
+ self._subprotocols = list(subprotocols)
62
+ if origin is not None:
63
+ self._headers.setdefault("origin", origin)
64
+
65
+ self._al_servidor: asyncio.Queue = asyncio.Queue()
66
+ self._al_cliente: asyncio.Queue = asyncio.Queue()
67
+ self._buffer: list[dict] = [] # lo que ya sacamos de la cola
68
+ self._tarea: asyncio.Task | None = None
69
+
70
+ self.accepted = False
71
+ self.subprotocol: str | None = None
72
+ self.close_code: int | None = None
73
+ self.close_reason: str = ""
74
+
75
+ @property
76
+ def connected(self) -> bool:
77
+ """
78
+ Si tienes una conexion usable.
79
+
80
+ Distinto de `accepted`, que dice literalmente si el servidor mando un
81
+ `websocket.accept`. Un rechazo con codigo (4404, 4401...) se ve en el
82
+ protocolo como accept + close, porque cerrar sin aceptar dejaria al
83
+ navegador con un 1006 sin motivo. Para "¿me dejo entrar?" usa este.
84
+ """
85
+ return self.accepted and self.close_code is None
86
+
87
+ # ------------------------------------------------------------ ciclo de vida
88
+
89
+ async def connect(self, timeout: float = TIMEOUT) -> "WebSocketClient":
90
+ """
91
+ Abre la conexion y espera la respuesta al handshake.
92
+
93
+ No lanza si el servidor rechaza: mira `connected` y `close_code`, que
94
+ es lo que quieres afirmar cuando pruebas un rechazo.
95
+ """
96
+ if self.user is not None:
97
+ self._cookies.setdefault(
98
+ await _nombre_cookie_sesion(), await _crear_sesion(self.user)
99
+ )
100
+ if self._cookies:
101
+ self._headers["cookie"] = "; ".join(
102
+ f"{k}={v}" for k, v in self._cookies.items()
103
+ )
104
+
105
+ from . import dispatch
106
+
107
+ self._al_servidor.put_nowait({"type": "websocket.connect"})
108
+ self._tarea = asyncio.create_task(
109
+ dispatch.handle_websocket(self._scope(), self._recibir, self._enviar)
110
+ )
111
+
112
+ evento = await self._siguiente(timeout, que="la respuesta al handshake")
113
+ if evento["type"] == "websocket.accept":
114
+ self.accepted = True
115
+ self.subprotocol = evento.get("subprotocol")
116
+ await self._mirar_si_cierra_enseguida()
117
+ else:
118
+ self._anotar_cierre(evento)
119
+ return self
120
+
121
+ async def _mirar_si_cierra_enseguida(self) -> None:
122
+ """
123
+ Deja avanzar al handler y anota el cierre si ya viene de camino, para
124
+ que `connected` diga la verdad nada mas volver de connect().
125
+
126
+ Lo que se saca de la cola se guarda en el buffer, asi que `receive()`
127
+ lo sigue viendo en orden: no se pierde ni un mensaje ni el cierre.
128
+ """
129
+ for _ in range(3):
130
+ await asyncio.sleep(0)
131
+ while not self._al_cliente.empty():
132
+ evento = self._al_cliente.get_nowait()
133
+ self._buffer.append(evento)
134
+ if evento["type"] == "websocket.close":
135
+ self._anotar_cierre(evento)
136
+ return
137
+
138
+ async def disconnect(self, code: int = 1000) -> None:
139
+ """El cliente se va. Espera a que el handler termine su limpieza."""
140
+ if self._tarea is None or self._tarea.done():
141
+ return
142
+ self._al_servidor.put_nowait({"type": "websocket.disconnect", "code": code})
143
+ try:
144
+ await asyncio.wait_for(asyncio.shield(self._tarea), timeout=TIMEOUT)
145
+ except asyncio.TimeoutError:
146
+ self._tarea.cancel()
147
+ self._vaciar_cierres()
148
+
149
+ async def __aenter__(self) -> "WebSocketClient":
150
+ return await self.connect()
151
+
152
+ async def __aexit__(self, *exc) -> None:
153
+ await self.disconnect()
154
+
155
+ # ------------------------------------------------------------------ enviar
156
+
157
+ async def send(self, data: Any) -> None:
158
+ """str -> texto, bytes -> binario, cualquier otra cosa -> JSON."""
159
+ if isinstance(data, str):
160
+ await self.send_text(data)
161
+ elif isinstance(data, (bytes, bytearray)):
162
+ await self.send_bytes(bytes(data))
163
+ else:
164
+ await self.send_json(data)
165
+
166
+ async def send_text(self, text: str) -> None:
167
+ self._al_servidor.put_nowait({"type": "websocket.receive", "text": text})
168
+ await asyncio.sleep(0) # deja correr al handler
169
+
170
+ async def send_bytes(self, data: bytes) -> None:
171
+ self._al_servidor.put_nowait({"type": "websocket.receive", "bytes": data})
172
+ await asyncio.sleep(0)
173
+
174
+ async def send_json(self, data: Any) -> None:
175
+ import json
176
+
177
+ await self.send_text(json.dumps(data, default=str))
178
+
179
+ # ----------------------------------------------------------------- recibir
180
+
181
+ async def receive(self, timeout: float = TIMEOUT) -> Message:
182
+ """
183
+ El siguiente mensaje. Lanza `WebSocketDisconnect` si el servidor cierra
184
+ y `TimeoutDelEsperado` si no llega nada.
185
+ """
186
+ evento = await self._siguiente(timeout, que="un mensaje")
187
+ if evento["type"] == "websocket.close":
188
+ self._anotar_cierre(evento)
189
+ raise WebSocketDisconnect(self.close_code, self.close_reason)
190
+ return Message(text=evento.get("text"), data=evento.get("bytes"))
191
+
192
+ async def receive_text(self, timeout: float = TIMEOUT) -> str:
193
+ msg = await self.receive(timeout)
194
+ if msg.text is None:
195
+ raise AssertionError("Se esperaba texto y llego un frame binario.")
196
+ return msg.text
197
+
198
+ async def receive_bytes(self, timeout: float = TIMEOUT) -> bytes:
199
+ msg = await self.receive(timeout)
200
+ if msg.bytes is None:
201
+ raise AssertionError("Se esperaba binario y llego un frame de texto.")
202
+ return msg.bytes
203
+
204
+ async def receive_json(self, timeout: float = TIMEOUT) -> Any:
205
+ return (await self.receive(timeout)).json()
206
+
207
+ async def receive_all(self, timeout: float = 0.1) -> list[Message]:
208
+ """Todo lo que haya pendiente ahora mismo. Util tras un broadcast."""
209
+ mensajes = []
210
+ while True:
211
+ try:
212
+ mensajes.append(await self.receive(timeout))
213
+ except (TimeoutDelEsperado, WebSocketDisconnect):
214
+ return mensajes
215
+
216
+ async def receive_nothing(self, timeout: float = 0.1) -> bool:
217
+ """True si NO llega nada. Para afirmar que un grupo esta aislado."""
218
+ try:
219
+ await self._siguiente(timeout, que="nada")
220
+ except TimeoutDelEsperado:
221
+ return True
222
+ return False
223
+
224
+ async def wait_closed(self, timeout: float = TIMEOUT) -> int:
225
+ """Espera a que el servidor cierre y devuelve el codigo."""
226
+ if self.close_code is not None:
227
+ return self.close_code
228
+ try:
229
+ while True:
230
+ evento = await self._siguiente(timeout, que="el cierre")
231
+ if evento["type"] == "websocket.close":
232
+ self._anotar_cierre(evento)
233
+ return self.close_code
234
+ except TimeoutDelEsperado:
235
+ raise AssertionError(
236
+ f"El servidor no cerro en {timeout}s (sigue abierto)."
237
+ ) from None
238
+
239
+ # ------------------------------------------------------------------ dentro
240
+
241
+ def _scope(self) -> dict:
242
+ cabeceras = [
243
+ (k.lower().encode("latin-1"), str(v).encode("latin-1"))
244
+ for k, v in self._headers.items()
245
+ if v is not None
246
+ ]
247
+ return {
248
+ "type": "websocket",
249
+ "path": self.path,
250
+ "raw_path": self.path.encode(),
251
+ "query_string": self._query.encode(),
252
+ "headers": cabeceras,
253
+ "subprotocols": self._subprotocols,
254
+ "client": ("127.0.0.1", 54321),
255
+ "server": ("testserver", 80),
256
+ "scheme": "ws",
257
+ }
258
+
259
+ async def _recibir(self) -> dict:
260
+ return await self._al_servidor.get()
261
+
262
+ async def _enviar(self, message: dict) -> None:
263
+ await self._al_cliente.put(message)
264
+
265
+ async def _siguiente(self, timeout: float, que: str) -> dict:
266
+ if self._buffer:
267
+ return self._buffer.pop(0)
268
+ try:
269
+ return await asyncio.wait_for(self._al_cliente.get(), timeout=timeout)
270
+ except asyncio.TimeoutError:
271
+ self._reventar_si_el_handler_murio()
272
+ raise TimeoutDelEsperado(
273
+ f"No llego {que} de {self.path} en {timeout}s."
274
+ ) from None
275
+
276
+ def _reventar_si_el_handler_murio(self) -> None:
277
+ """Si el handler murio de verdad, ese error es mas util que el timeout."""
278
+ if self._tarea is not None and self._tarea.done():
279
+ exc = self._tarea.exception()
280
+ if exc is not None:
281
+ raise exc
282
+
283
+ def _anotar_cierre(self, evento: dict) -> None:
284
+ self.close_code = evento.get("code", 1000)
285
+ self.close_reason = evento.get("reason", "")
286
+
287
+ def _vaciar_cierres(self) -> None:
288
+ while not self._al_cliente.empty():
289
+ self._buffer.append(self._al_cliente.get_nowait())
290
+ for evento in self._buffer:
291
+ if evento["type"] == "websocket.close":
292
+ self._anotar_cierre(evento)
293
+
294
+
295
+ # --------------------------------------------------------------------- sesion
296
+
297
+
298
+ async def _nombre_cookie_sesion() -> str:
299
+ from django.conf import settings
300
+
301
+ return settings.SESSION_COOKIE_NAME
302
+
303
+
304
+ async def _crear_sesion(user) -> str:
305
+ """Deja una sesion autenticada en la BD, como haria un login de verdad."""
306
+ from importlib import import_module
307
+
308
+ from django.conf import settings
309
+ from django.contrib.auth import (
310
+ BACKEND_SESSION_KEY,
311
+ HASH_SESSION_KEY,
312
+ SESSION_KEY,
313
+ )
314
+
315
+ engine = import_module(settings.SESSION_ENGINE)
316
+ sesion = engine.SessionStore()
317
+ sesion[SESSION_KEY] = str(user.pk)
318
+ sesion[BACKEND_SESSION_KEY] = settings.AUTHENTICATION_BACKENDS[0]
319
+ sesion[HASH_SESSION_KEY] = user.get_session_auth_hash()
320
+
321
+ if hasattr(sesion, "acreate"):
322
+ await sesion.acreate()
323
+ else: # pragma: no cover - Django < 5.0
324
+ from asgiref.sync import sync_to_async
325
+
326
+ await sync_to_async(sesion.create)()
327
+ return sesion.session_key
328
+
329
+
330
+ __all__ = ["WebSocketClient", "TimeoutDelEsperado", "WebSocketDisconnect", "InvalidJSON"]