asd
This commit is contained in:
28
venv/lib/python3.12/site-packages/socketio/__init__.py
Normal file
28
venv/lib/python3.12/site-packages/socketio/__init__.py
Normal file
@ -0,0 +1,28 @@
|
||||
from .client import Client
|
||||
from .simple_client import SimpleClient
|
||||
from .manager import Manager
|
||||
from .pubsub_manager import PubSubManager
|
||||
from .kombu_manager import KombuManager
|
||||
from .redis_manager import RedisManager
|
||||
from .kafka_manager import KafkaManager
|
||||
from .zmq_manager import ZmqManager
|
||||
from .server import Server
|
||||
from .namespace import Namespace, ClientNamespace
|
||||
from .middleware import WSGIApp, Middleware
|
||||
from .tornado import get_tornado_handler
|
||||
from .async_client import AsyncClient
|
||||
from .async_simple_client import AsyncSimpleClient
|
||||
from .async_server import AsyncServer
|
||||
from .async_manager import AsyncManager
|
||||
from .async_namespace import AsyncNamespace, AsyncClientNamespace
|
||||
from .async_redis_manager import AsyncRedisManager
|
||||
from .async_aiopika_manager import AsyncAioPikaManager
|
||||
from .asgi import ASGIApp
|
||||
|
||||
__all__ = ['SimpleClient', 'Client', 'Server', 'Manager', 'PubSubManager',
|
||||
'KombuManager', 'RedisManager', 'ZmqManager', 'KafkaManager',
|
||||
'Namespace', 'ClientNamespace', 'WSGIApp', 'Middleware',
|
||||
'AsyncSimpleClient', 'AsyncClient', 'AsyncServer',
|
||||
'AsyncNamespace', 'AsyncClientNamespace', 'AsyncManager',
|
||||
'AsyncRedisManager', 'ASGIApp', 'get_tornado_handler',
|
||||
'AsyncAioPikaManager']
|
||||
405
venv/lib/python3.12/site-packages/socketio/admin.py
Normal file
405
venv/lib/python3.12/site-packages/socketio/admin.py
Normal file
@ -0,0 +1,405 @@
|
||||
from datetime import datetime
|
||||
import functools
|
||||
import os
|
||||
import socket
|
||||
import time
|
||||
from urllib.parse import parse_qs
|
||||
from .exceptions import ConnectionRefusedError
|
||||
|
||||
HOSTNAME = socket.gethostname()
|
||||
PID = os.getpid()
|
||||
|
||||
|
||||
class EventBuffer:
|
||||
def __init__(self):
|
||||
self.buffer = {}
|
||||
|
||||
def push(self, type, count=1):
|
||||
timestamp = int(time.time()) * 1000
|
||||
key = '{};{}'.format(timestamp, type)
|
||||
if key not in self.buffer:
|
||||
self.buffer[key] = {
|
||||
'timestamp': timestamp,
|
||||
'type': type,
|
||||
'count': count,
|
||||
}
|
||||
else:
|
||||
self.buffer[key]['count'] += count
|
||||
|
||||
def get_and_clear(self):
|
||||
buffer = self.buffer
|
||||
self.buffer = {}
|
||||
return [value for value in buffer.values()]
|
||||
|
||||
|
||||
class InstrumentedServer:
|
||||
def __init__(self, sio, auth=None, mode='development', read_only=False,
|
||||
server_id=None, namespace='/admin', server_stats_interval=2):
|
||||
"""Instrument the Socket.IO server for monitoring with the `Socket.IO
|
||||
Admin UI <https://socket.io/docs/v4/admin-ui/>`_.
|
||||
"""
|
||||
if auth is None:
|
||||
raise ValueError('auth must be specified')
|
||||
self.sio = sio
|
||||
self.auth = auth
|
||||
self.admin_namespace = namespace
|
||||
self.read_only = read_only
|
||||
self.server_id = server_id or (
|
||||
self.sio.manager.host_id if hasattr(self.sio.manager, 'host_id')
|
||||
else HOSTNAME
|
||||
)
|
||||
self.mode = mode
|
||||
self.server_stats_interval = server_stats_interval
|
||||
self.event_buffer = EventBuffer()
|
||||
|
||||
# task that emits "server_stats" every 2 seconds
|
||||
self.stop_stats_event = None
|
||||
self.stats_task = None
|
||||
|
||||
# monkey-patch the server to report metrics to the admin UI
|
||||
self.instrument()
|
||||
|
||||
def instrument(self):
|
||||
self.sio.on('connect', self.admin_connect,
|
||||
namespace=self.admin_namespace)
|
||||
|
||||
if self.mode == 'development':
|
||||
if not self.read_only: # pragma: no branch
|
||||
self.sio.on('emit', self.admin_emit,
|
||||
namespace=self.admin_namespace)
|
||||
self.sio.on('join', self.admin_enter_room,
|
||||
namespace=self.admin_namespace)
|
||||
self.sio.on('leave', self.admin_leave_room,
|
||||
namespace=self.admin_namespace)
|
||||
self.sio.on('_disconnect', self.admin_disconnect,
|
||||
namespace=self.admin_namespace)
|
||||
|
||||
# track socket connection times
|
||||
self.sio.manager._timestamps = {}
|
||||
|
||||
# report socket.io connections
|
||||
self.sio.manager.__connect = self.sio.manager.connect
|
||||
self.sio.manager.connect = self._connect
|
||||
|
||||
# report socket.io disconnection
|
||||
self.sio.manager.__disconnect = self.sio.manager.disconnect
|
||||
self.sio.manager.disconnect = self._disconnect
|
||||
|
||||
# report join rooms
|
||||
self.sio.manager.__basic_enter_room = \
|
||||
self.sio.manager.basic_enter_room
|
||||
self.sio.manager.basic_enter_room = self._basic_enter_room
|
||||
|
||||
# report leave rooms
|
||||
self.sio.manager.__basic_leave_room = \
|
||||
self.sio.manager.basic_leave_room
|
||||
self.sio.manager.basic_leave_room = self._basic_leave_room
|
||||
|
||||
# report emit events
|
||||
self.sio.manager.__emit = self.sio.manager.emit
|
||||
self.sio.manager.emit = self._emit
|
||||
|
||||
# report receive events
|
||||
self.sio.__handle_event_internal = self.sio._handle_event_internal
|
||||
self.sio._handle_event_internal = self._handle_event_internal
|
||||
|
||||
# report engine.io connections
|
||||
self.sio.eio.on('connect', self._handle_eio_connect)
|
||||
self.sio.eio.on('disconnect', self._handle_eio_disconnect)
|
||||
|
||||
# report polling packets
|
||||
from engineio.socket import Socket
|
||||
self.sio.eio.__ok = self.sio.eio._ok
|
||||
self.sio.eio._ok = self._eio_http_response
|
||||
Socket.__handle_post_request = Socket.handle_post_request
|
||||
Socket.handle_post_request = functools.partialmethod(
|
||||
self.__class__._eio_handle_post_request, self)
|
||||
|
||||
# report websocket packets
|
||||
Socket.__websocket_handler = Socket._websocket_handler
|
||||
Socket._websocket_handler = functools.partialmethod(
|
||||
self.__class__._eio_websocket_handler, self)
|
||||
|
||||
# report connected sockets with each ping
|
||||
if self.mode == 'development':
|
||||
Socket.__send_ping = Socket._send_ping
|
||||
Socket._send_ping = functools.partialmethod(
|
||||
self.__class__._eio_send_ping, self)
|
||||
|
||||
def uninstrument(self): # pragma: no cover
|
||||
if self.mode == 'development':
|
||||
self.sio.manager.connect = self.sio.manager.__connect
|
||||
self.sio.manager.disconnect = self.sio.manager.__disconnect
|
||||
self.sio.manager.basic_enter_room = \
|
||||
self.sio.manager.__basic_enter_room
|
||||
self.sio.manager.basic_leave_room = \
|
||||
self.sio.manager.__basic_leave_room
|
||||
self.sio.manager.emit = self.sio.manager.__emit
|
||||
self.sio._handle_event_internal = self.sio.__handle_event_internal
|
||||
self.sio.eio._ok = self.sio.eio.__ok
|
||||
|
||||
from engineio.socket import Socket
|
||||
Socket.handle_post_request = Socket.__handle_post_request
|
||||
Socket._websocket_handler = Socket.__websocket_handler
|
||||
if self.mode == 'development':
|
||||
Socket._send_ping = Socket.__send_ping
|
||||
|
||||
def admin_connect(self, sid, environ, client_auth):
|
||||
if self.auth:
|
||||
authenticated = False
|
||||
if isinstance(self.auth, dict):
|
||||
authenticated = client_auth == self.auth
|
||||
elif isinstance(self.auth, list):
|
||||
authenticated = client_auth in self.auth
|
||||
else:
|
||||
authenticated = self.auth(client_auth)
|
||||
if not authenticated:
|
||||
raise ConnectionRefusedError('authentication failed')
|
||||
|
||||
def config(sid):
|
||||
self.sio.sleep(0.1)
|
||||
|
||||
# supported features
|
||||
features = ['AGGREGATED_EVENTS']
|
||||
if not self.read_only:
|
||||
features += ['EMIT', 'JOIN', 'LEAVE', 'DISCONNECT', 'MJOIN',
|
||||
'MLEAVE', 'MDISCONNECT']
|
||||
if self.mode == 'development':
|
||||
features.append('ALL_EVENTS')
|
||||
self.sio.emit('config', {'supportedFeatures': features},
|
||||
to=sid, namespace=self.admin_namespace)
|
||||
|
||||
# send current sockets
|
||||
if self.mode == 'development':
|
||||
all_sockets = []
|
||||
for nsp in self.sio.manager.get_namespaces():
|
||||
for sid, eio_sid in self.sio.manager.get_participants(
|
||||
nsp, None):
|
||||
all_sockets.append(
|
||||
self.serialize_socket(sid, nsp, eio_sid))
|
||||
self.sio.emit('all_sockets', all_sockets, to=sid,
|
||||
namespace=self.admin_namespace)
|
||||
|
||||
self.sio.start_background_task(config, sid)
|
||||
|
||||
def admin_emit(self, _, namespace, room_filter, event, *data):
|
||||
self.sio.emit(event, data, to=room_filter, namespace=namespace)
|
||||
|
||||
def admin_enter_room(self, _, namespace, room, room_filter=None):
|
||||
for sid, _ in self.sio.manager.get_participants(
|
||||
namespace, room_filter):
|
||||
self.sio.enter_room(sid, room, namespace=namespace)
|
||||
|
||||
def admin_leave_room(self, _, namespace, room, room_filter=None):
|
||||
for sid, _ in self.sio.manager.get_participants(
|
||||
namespace, room_filter):
|
||||
self.sio.leave_room(sid, room, namespace=namespace)
|
||||
|
||||
def admin_disconnect(self, _, namespace, close, room_filter=None):
|
||||
for sid, _ in self.sio.manager.get_participants(
|
||||
namespace, room_filter):
|
||||
self.sio.disconnect(sid, namespace=namespace)
|
||||
|
||||
def shutdown(self):
|
||||
if self.stats_task: # pragma: no branch
|
||||
self.stop_stats_event.set()
|
||||
self.stats_task.join()
|
||||
|
||||
def _connect(self, eio_sid, namespace):
|
||||
sid = self.sio.manager.__connect(eio_sid, namespace)
|
||||
t = time.time()
|
||||
self.sio.manager._timestamps[sid] = t
|
||||
serialized_socket = self.serialize_socket(sid, namespace, eio_sid)
|
||||
self.sio.emit('socket_connected', (
|
||||
serialized_socket,
|
||||
datetime.utcfromtimestamp(t).isoformat() + 'Z',
|
||||
), namespace=self.admin_namespace)
|
||||
return sid
|
||||
|
||||
def _disconnect(self, sid, namespace, **kwargs):
|
||||
del self.sio.manager._timestamps[sid]
|
||||
self.sio.emit('socket_disconnected', (
|
||||
namespace,
|
||||
sid,
|
||||
'N/A',
|
||||
datetime.utcnow().isoformat() + 'Z',
|
||||
), namespace=self.admin_namespace)
|
||||
return self.sio.manager.__disconnect(sid, namespace, **kwargs)
|
||||
|
||||
def _check_for_upgrade(self, eio_sid, sid, namespace): # pragma: no cover
|
||||
for _ in range(5):
|
||||
self.sio.sleep(5)
|
||||
try:
|
||||
if self.sio.eio._get_socket(eio_sid).upgraded:
|
||||
self.sio.emit('socket_updated', {
|
||||
'id': sid,
|
||||
'nsp': namespace,
|
||||
'transport': 'websocket',
|
||||
}, namespace=self.admin_namespace)
|
||||
break
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def _basic_enter_room(self, sid, namespace, room, eio_sid=None):
|
||||
ret = self.sio.manager.__basic_enter_room(sid, namespace, room,
|
||||
eio_sid)
|
||||
if room:
|
||||
self.sio.emit('room_joined', (
|
||||
namespace,
|
||||
room,
|
||||
sid,
|
||||
datetime.utcnow().isoformat() + 'Z',
|
||||
), namespace=self.admin_namespace)
|
||||
return ret
|
||||
|
||||
def _basic_leave_room(self, sid, namespace, room):
|
||||
if room:
|
||||
self.sio.emit('room_left', (
|
||||
namespace,
|
||||
room,
|
||||
sid,
|
||||
datetime.utcnow().isoformat() + 'Z',
|
||||
), namespace=self.admin_namespace)
|
||||
return self.sio.manager.__basic_leave_room(sid, namespace, room)
|
||||
|
||||
def _emit(self, event, data, namespace, room=None, skip_sid=None,
|
||||
callback=None, **kwargs):
|
||||
ret = self.sio.manager.__emit(event, data, namespace, room=room,
|
||||
skip_sid=skip_sid, callback=callback,
|
||||
**kwargs)
|
||||
if namespace != self.admin_namespace:
|
||||
event_data = [event] + list(data) if isinstance(data, tuple) \
|
||||
else [data]
|
||||
if not isinstance(skip_sid, list): # pragma: no branch
|
||||
skip_sid = [skip_sid]
|
||||
for sid, _ in self.sio.manager.get_participants(namespace, room):
|
||||
if sid not in skip_sid:
|
||||
self.sio.emit('event_sent', (
|
||||
namespace,
|
||||
sid,
|
||||
event_data,
|
||||
datetime.utcnow().isoformat() + 'Z',
|
||||
), namespace=self.admin_namespace)
|
||||
return ret
|
||||
|
||||
def _handle_event_internal(self, server, sid, eio_sid, data, namespace,
|
||||
id):
|
||||
ret = self.sio.__handle_event_internal(server, sid, eio_sid, data,
|
||||
namespace, id)
|
||||
self.sio.emit('event_received', (
|
||||
namespace,
|
||||
sid,
|
||||
data,
|
||||
datetime.utcnow().isoformat() + 'Z',
|
||||
), namespace=self.admin_namespace)
|
||||
return ret
|
||||
|
||||
def _handle_eio_connect(self, eio_sid, environ):
|
||||
if self.stop_stats_event is None:
|
||||
self.stop_stats_event = self.sio.eio.create_event()
|
||||
self.stats_task = self.sio.start_background_task(
|
||||
self._emit_server_stats)
|
||||
|
||||
self.event_buffer.push('rawConnection')
|
||||
return self.sio._handle_eio_connect(eio_sid, environ)
|
||||
|
||||
def _handle_eio_disconnect(self, eio_sid):
|
||||
self.event_buffer.push('rawDisconnection')
|
||||
return self.sio._handle_eio_disconnect(eio_sid)
|
||||
|
||||
def _eio_http_response(self, packets=None, headers=None, jsonp_index=None):
|
||||
ret = self.sio.eio.__ok(packets=packets, headers=headers,
|
||||
jsonp_index=jsonp_index)
|
||||
self.event_buffer.push('packetsOut')
|
||||
self.event_buffer.push('bytesOut', len(ret['response']))
|
||||
return ret
|
||||
|
||||
def _eio_handle_post_request(socket, self, environ):
|
||||
ret = socket.__handle_post_request(environ)
|
||||
self.event_buffer.push('packetsIn')
|
||||
self.event_buffer.push(
|
||||
'bytesIn', int(environ.get('CONTENT_LENGTH', 0)))
|
||||
return ret
|
||||
|
||||
def _eio_websocket_handler(socket, self, ws):
|
||||
def _send(ws, data, *args, **kwargs):
|
||||
self.event_buffer.push('packetsOut')
|
||||
self.event_buffer.push('bytesOut', len(data))
|
||||
return ws.__send(data, *args, **kwargs)
|
||||
|
||||
def _wait(ws):
|
||||
ret = ws.__wait()
|
||||
self.event_buffer.push('packetsIn')
|
||||
self.event_buffer.push('bytesIn', len(ret or ''))
|
||||
return ret
|
||||
|
||||
ws.__send = ws.send
|
||||
ws.send = functools.partial(_send, ws)
|
||||
ws.__wait = ws.wait
|
||||
ws.wait = functools.partial(_wait, ws)
|
||||
return socket.__websocket_handler(ws)
|
||||
|
||||
def _eio_send_ping(socket, self): # pragma: no cover
|
||||
eio_sid = socket.sid
|
||||
t = time.time()
|
||||
for namespace in self.sio.manager.get_namespaces():
|
||||
sid = self.sio.manager.sid_from_eio_sid(eio_sid, namespace)
|
||||
if sid:
|
||||
serialized_socket = self.serialize_socket(sid, namespace,
|
||||
eio_sid)
|
||||
self.sio.emit('socket_connected', (
|
||||
serialized_socket,
|
||||
datetime.utcfromtimestamp(t).isoformat() + 'Z',
|
||||
), namespace=self.admin_namespace)
|
||||
return socket.__send_ping()
|
||||
|
||||
def _emit_server_stats(self):
|
||||
start_time = time.time()
|
||||
namespaces = list(self.sio.handlers.keys())
|
||||
namespaces.sort()
|
||||
while not self.stop_stats_event.is_set():
|
||||
self.sio.sleep(self.server_stats_interval)
|
||||
self.sio.emit('server_stats', {
|
||||
'serverId': self.server_id,
|
||||
'hostname': HOSTNAME,
|
||||
'pid': PID,
|
||||
'uptime': time.time() - start_time,
|
||||
'clientsCount': len(self.sio.eio.sockets),
|
||||
'pollingClientsCount': len(
|
||||
[s for s in self.sio.eio.sockets.values()
|
||||
if not s.upgraded]),
|
||||
'aggregatedEvents': self.event_buffer.get_and_clear(),
|
||||
'namespaces': [{
|
||||
'name': nsp,
|
||||
'socketsCount': len(self.sio.manager.rooms.get(
|
||||
nsp, {None: []}).get(None, []))
|
||||
} for nsp in namespaces],
|
||||
}, namespace=self.admin_namespace)
|
||||
|
||||
def serialize_socket(self, sid, namespace, eio_sid=None):
|
||||
if eio_sid is None: # pragma: no cover
|
||||
eio_sid = self.sio.manager.eio_sid_from_sid(sid)
|
||||
socket = self.sio.eio._get_socket(eio_sid)
|
||||
environ = self.sio.environ.get(eio_sid, {})
|
||||
tm = self.sio.manager._timestamps[sid] if sid in \
|
||||
self.sio.manager._timestamps else 0
|
||||
return {
|
||||
'id': sid,
|
||||
'clientId': eio_sid,
|
||||
'transport': 'websocket' if socket.upgraded else 'polling',
|
||||
'nsp': namespace,
|
||||
'data': {},
|
||||
'handshake': {
|
||||
'address': environ.get('REMOTE_ADDR', ''),
|
||||
'headers': {k[5:].lower(): v for k, v in environ.items()
|
||||
if k.startswith('HTTP_')},
|
||||
'query': {k: v[0] if len(v) == 1 else v for k, v in parse_qs(
|
||||
environ.get('QUERY_STRING', '')).items()},
|
||||
'secure': environ.get('wsgi.url_scheme', '') == 'https',
|
||||
'url': environ.get('PATH_INFO', ''),
|
||||
'issued': tm * 1000,
|
||||
'time': datetime.utcfromtimestamp(tm).isoformat() + 'Z'
|
||||
if tm else '',
|
||||
},
|
||||
'rooms': self.sio.manager.get_rooms(sid, namespace),
|
||||
}
|
||||
47
venv/lib/python3.12/site-packages/socketio/asgi.py
Normal file
47
venv/lib/python3.12/site-packages/socketio/asgi.py
Normal file
@ -0,0 +1,47 @@
|
||||
import engineio
|
||||
|
||||
|
||||
class ASGIApp(engineio.ASGIApp): # pragma: no cover
|
||||
"""ASGI application middleware for Socket.IO.
|
||||
|
||||
This middleware dispatches traffic to an Socket.IO application. It can
|
||||
also serve a list of static files to the client, or forward unrelated
|
||||
HTTP traffic to another ASGI application.
|
||||
|
||||
:param socketio_server: The Socket.IO server. Must be an instance of the
|
||||
``socketio.AsyncServer`` class.
|
||||
:param static_files: A dictionary with static file mapping rules. See the
|
||||
documentation for details on this argument.
|
||||
:param other_asgi_app: A separate ASGI app that receives all other traffic.
|
||||
:param socketio_path: The endpoint where the Socket.IO application should
|
||||
be installed. The default value is appropriate for
|
||||
most cases. With a value of ``None``, all incoming
|
||||
traffic is directed to the Socket.IO server, with the
|
||||
assumption that routing, if necessary, is handled by
|
||||
a different layer. When this option is set to
|
||||
``None``, ``static_files`` and ``other_asgi_app`` are
|
||||
ignored.
|
||||
:param on_startup: function to be called on application startup; can be
|
||||
coroutine
|
||||
:param on_shutdown: function to be called on application shutdown; can be
|
||||
coroutine
|
||||
|
||||
Example usage::
|
||||
|
||||
import socketio
|
||||
import uvicorn
|
||||
|
||||
sio = socketio.AsyncServer()
|
||||
app = socketio.ASGIApp(sio, static_files={
|
||||
'/': 'index.html',
|
||||
'/static': './public',
|
||||
})
|
||||
uvicorn.run(app, host='127.0.0.1', port=5000)
|
||||
"""
|
||||
def __init__(self, socketio_server, other_asgi_app=None,
|
||||
static_files=None, socketio_path='socket.io',
|
||||
on_startup=None, on_shutdown=None):
|
||||
super().__init__(socketio_server, other_asgi_app,
|
||||
static_files=static_files,
|
||||
engineio_path=socketio_path, on_startup=on_startup,
|
||||
on_shutdown=on_shutdown)
|
||||
398
venv/lib/python3.12/site-packages/socketio/async_admin.py
Normal file
398
venv/lib/python3.12/site-packages/socketio/async_admin.py
Normal file
@ -0,0 +1,398 @@
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
import functools
|
||||
import os
|
||||
import socket
|
||||
import time
|
||||
from urllib.parse import parse_qs
|
||||
from .admin import EventBuffer
|
||||
from .exceptions import ConnectionRefusedError
|
||||
|
||||
HOSTNAME = socket.gethostname()
|
||||
PID = os.getpid()
|
||||
|
||||
|
||||
class InstrumentedAsyncServer:
|
||||
def __init__(self, sio, auth=None, namespace='/admin', read_only=False,
|
||||
server_id=None, mode='development', server_stats_interval=2):
|
||||
"""Instrument the Socket.IO server for monitoring with the `Socket.IO
|
||||
Admin UI <https://socket.io/docs/v4/admin-ui/>`_.
|
||||
"""
|
||||
if auth is None:
|
||||
raise ValueError('auth must be specified')
|
||||
self.sio = sio
|
||||
self.auth = auth
|
||||
self.admin_namespace = namespace
|
||||
self.read_only = read_only
|
||||
self.server_id = server_id or (
|
||||
self.sio.manager.host_id if hasattr(self.sio.manager, 'host_id')
|
||||
else HOSTNAME
|
||||
)
|
||||
self.mode = mode
|
||||
self.server_stats_interval = server_stats_interval
|
||||
self.admin_queue = []
|
||||
self.event_buffer = EventBuffer()
|
||||
|
||||
# task that emits "server_stats" every 2 seconds
|
||||
self.stop_stats_event = None
|
||||
self.stats_task = None
|
||||
|
||||
# monkey-patch the server to report metrics to the admin UI
|
||||
self.instrument()
|
||||
|
||||
def instrument(self):
|
||||
self.sio.on('connect', self.admin_connect,
|
||||
namespace=self.admin_namespace)
|
||||
|
||||
if self.mode == 'development':
|
||||
if not self.read_only: # pragma: no branch
|
||||
self.sio.on('emit', self.admin_emit,
|
||||
namespace=self.admin_namespace)
|
||||
self.sio.on('join', self.admin_enter_room,
|
||||
namespace=self.admin_namespace)
|
||||
self.sio.on('leave', self.admin_leave_room,
|
||||
namespace=self.admin_namespace)
|
||||
self.sio.on('_disconnect', self.admin_disconnect,
|
||||
namespace=self.admin_namespace)
|
||||
|
||||
# track socket connection times
|
||||
self.sio.manager._timestamps = {}
|
||||
|
||||
# report socket.io connections
|
||||
self.sio.manager.__connect = self.sio.manager.connect
|
||||
self.sio.manager.connect = self._connect
|
||||
|
||||
# report socket.io disconnection
|
||||
self.sio.manager.__disconnect = self.sio.manager.disconnect
|
||||
self.sio.manager.disconnect = self._disconnect
|
||||
|
||||
# report join rooms
|
||||
self.sio.manager.__basic_enter_room = \
|
||||
self.sio.manager.basic_enter_room
|
||||
self.sio.manager.basic_enter_room = self._basic_enter_room
|
||||
|
||||
# report leave rooms
|
||||
self.sio.manager.__basic_leave_room = \
|
||||
self.sio.manager.basic_leave_room
|
||||
self.sio.manager.basic_leave_room = self._basic_leave_room
|
||||
|
||||
# report emit events
|
||||
self.sio.manager.__emit = self.sio.manager.emit
|
||||
self.sio.manager.emit = self._emit
|
||||
|
||||
# report receive events
|
||||
self.sio.__handle_event_internal = self.sio._handle_event_internal
|
||||
self.sio._handle_event_internal = self._handle_event_internal
|
||||
|
||||
# report engine.io connections
|
||||
self.sio.eio.on('connect', self._handle_eio_connect)
|
||||
self.sio.eio.on('disconnect', self._handle_eio_disconnect)
|
||||
|
||||
# report polling packets
|
||||
from engineio.async_socket import AsyncSocket
|
||||
self.sio.eio.__ok = self.sio.eio._ok
|
||||
self.sio.eio._ok = self._eio_http_response
|
||||
AsyncSocket.__handle_post_request = AsyncSocket.handle_post_request
|
||||
AsyncSocket.handle_post_request = functools.partialmethod(
|
||||
self.__class__._eio_handle_post_request, self)
|
||||
|
||||
# report websocket packets
|
||||
AsyncSocket.__websocket_handler = AsyncSocket._websocket_handler
|
||||
AsyncSocket._websocket_handler = functools.partialmethod(
|
||||
self.__class__._eio_websocket_handler, self)
|
||||
|
||||
# report connected sockets with each ping
|
||||
if self.mode == 'development':
|
||||
AsyncSocket.__send_ping = AsyncSocket._send_ping
|
||||
AsyncSocket._send_ping = functools.partialmethod(
|
||||
self.__class__._eio_send_ping, self)
|
||||
|
||||
def uninstrument(self): # pragma: no cover
|
||||
if self.mode == 'development':
|
||||
self.sio.manager.connect = self.sio.manager.__connect
|
||||
self.sio.manager.disconnect = self.sio.manager.__disconnect
|
||||
self.sio.manager.basic_enter_room = \
|
||||
self.sio.manager.__basic_enter_room
|
||||
self.sio.manager.basic_leave_room = \
|
||||
self.sio.manager.__basic_leave_room
|
||||
self.sio.manager.emit = self.sio.manager.__emit
|
||||
self.sio._handle_event_internal = self.sio.__handle_event_internal
|
||||
self.sio.eio._ok = self.sio.eio.__ok
|
||||
|
||||
from engineio.async_socket import AsyncSocket
|
||||
AsyncSocket.handle_post_request = AsyncSocket.__handle_post_request
|
||||
AsyncSocket._websocket_handler = AsyncSocket.__websocket_handler
|
||||
if self.mode == 'development':
|
||||
AsyncSocket._send_ping = AsyncSocket.__send_ping
|
||||
|
||||
async def admin_connect(self, sid, environ, client_auth):
|
||||
authenticated = True
|
||||
if self.auth:
|
||||
authenticated = False
|
||||
if isinstance(self.auth, dict):
|
||||
authenticated = client_auth == self.auth
|
||||
elif isinstance(self.auth, list):
|
||||
authenticated = client_auth in self.auth
|
||||
else:
|
||||
if asyncio.iscoroutinefunction(self.auth):
|
||||
authenticated = await self.auth(client_auth)
|
||||
else:
|
||||
authenticated = self.auth(client_auth)
|
||||
if not authenticated:
|
||||
raise ConnectionRefusedError('authentication failed')
|
||||
|
||||
async def config(sid):
|
||||
await self.sio.sleep(0.1)
|
||||
|
||||
# supported features
|
||||
features = ['AGGREGATED_EVENTS']
|
||||
if not self.read_only:
|
||||
features += ['EMIT', 'JOIN', 'LEAVE', 'DISCONNECT', 'MJOIN',
|
||||
'MLEAVE', 'MDISCONNECT']
|
||||
if self.mode == 'development':
|
||||
features.append('ALL_EVENTS')
|
||||
await self.sio.emit('config', {'supportedFeatures': features},
|
||||
to=sid, namespace=self.admin_namespace)
|
||||
|
||||
# send current sockets
|
||||
if self.mode == 'development':
|
||||
all_sockets = []
|
||||
for nsp in self.sio.manager.get_namespaces():
|
||||
for sid, eio_sid in self.sio.manager.get_participants(
|
||||
nsp, None):
|
||||
all_sockets.append(
|
||||
self.serialize_socket(sid, nsp, eio_sid))
|
||||
await self.sio.emit('all_sockets', all_sockets, to=sid,
|
||||
namespace=self.admin_namespace)
|
||||
|
||||
self.sio.start_background_task(config, sid)
|
||||
self.stop_stats_event = self.sio.eio.create_event()
|
||||
self.stats_task = self.sio.start_background_task(
|
||||
self._emit_server_stats)
|
||||
|
||||
async def admin_emit(self, _, namespace, room_filter, event, *data):
|
||||
await self.sio.emit(event, data, to=room_filter, namespace=namespace)
|
||||
|
||||
async def admin_enter_room(self, _, namespace, room, room_filter=None):
|
||||
for sid, _ in self.sio.manager.get_participants(
|
||||
namespace, room_filter):
|
||||
await self.sio.enter_room(sid, room, namespace=namespace)
|
||||
|
||||
async def admin_leave_room(self, _, namespace, room, room_filter=None):
|
||||
for sid, _ in self.sio.manager.get_participants(
|
||||
namespace, room_filter):
|
||||
await self.sio.leave_room(sid, room, namespace=namespace)
|
||||
|
||||
async def admin_disconnect(self, _, namespace, close, room_filter=None):
|
||||
for sid, _ in self.sio.manager.get_participants(
|
||||
namespace, room_filter):
|
||||
await self.sio.disconnect(sid, namespace=namespace)
|
||||
|
||||
async def shutdown(self):
|
||||
if self.stats_task: # pragma: no branch
|
||||
self.stop_stats_event.set()
|
||||
await asyncio.gather(self.stats_task)
|
||||
|
||||
async def _connect(self, eio_sid, namespace):
|
||||
sid = await self.sio.manager.__connect(eio_sid, namespace)
|
||||
t = time.time()
|
||||
self.sio.manager._timestamps[sid] = t
|
||||
serialized_socket = self.serialize_socket(sid, namespace, eio_sid)
|
||||
await self.sio.emit('socket_connected', (
|
||||
serialized_socket,
|
||||
datetime.utcfromtimestamp(t).isoformat() + 'Z',
|
||||
), namespace=self.admin_namespace)
|
||||
return sid
|
||||
|
||||
async def _disconnect(self, sid, namespace, **kwargs):
|
||||
del self.sio.manager._timestamps[sid]
|
||||
await self.sio.emit('socket_disconnected', (
|
||||
namespace,
|
||||
sid,
|
||||
'N/A',
|
||||
datetime.utcnow().isoformat() + 'Z',
|
||||
), namespace=self.admin_namespace)
|
||||
return await self.sio.manager.__disconnect(sid, namespace, **kwargs)
|
||||
|
||||
async def _check_for_upgrade(self, eio_sid, sid,
|
||||
namespace): # pragma: no cover
|
||||
for _ in range(5):
|
||||
await self.sio.sleep(5)
|
||||
try:
|
||||
if self.sio.eio._get_socket(eio_sid).upgraded:
|
||||
await self.sio.emit('socket_updated', {
|
||||
'id': sid,
|
||||
'nsp': namespace,
|
||||
'transport': 'websocket',
|
||||
}, namespace=self.admin_namespace)
|
||||
break
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def _basic_enter_room(self, sid, namespace, room, eio_sid=None):
|
||||
ret = self.sio.manager.__basic_enter_room(sid, namespace, room,
|
||||
eio_sid)
|
||||
if room:
|
||||
self.admin_queue.append(('room_joined', (
|
||||
namespace,
|
||||
room,
|
||||
sid,
|
||||
datetime.utcnow().isoformat() + 'Z',
|
||||
)))
|
||||
return ret
|
||||
|
||||
def _basic_leave_room(self, sid, namespace, room):
|
||||
if room:
|
||||
self.admin_queue.append(('room_left', (
|
||||
namespace,
|
||||
room,
|
||||
sid,
|
||||
datetime.utcnow().isoformat() + 'Z',
|
||||
)))
|
||||
return self.sio.manager.__basic_leave_room(sid, namespace, room)
|
||||
|
||||
async def _emit(self, event, data, namespace, room=None, skip_sid=None,
|
||||
callback=None, **kwargs):
|
||||
ret = await self.sio.manager.__emit(
|
||||
event, data, namespace, room=room, skip_sid=skip_sid,
|
||||
callback=callback, **kwargs)
|
||||
if namespace != self.admin_namespace:
|
||||
event_data = [event] + list(data) if isinstance(data, tuple) \
|
||||
else [data]
|
||||
if not isinstance(skip_sid, list): # pragma: no branch
|
||||
skip_sid = [skip_sid]
|
||||
for sid, _ in self.sio.manager.get_participants(namespace, room):
|
||||
if sid not in skip_sid:
|
||||
await self.sio.emit('event_sent', (
|
||||
namespace,
|
||||
sid,
|
||||
event_data,
|
||||
datetime.utcnow().isoformat() + 'Z',
|
||||
), namespace=self.admin_namespace)
|
||||
return ret
|
||||
|
||||
async def _handle_event_internal(self, server, sid, eio_sid, data,
|
||||
namespace, id):
|
||||
ret = await self.sio.__handle_event_internal(server, sid, eio_sid,
|
||||
data, namespace, id)
|
||||
await self.sio.emit('event_received', (
|
||||
namespace,
|
||||
sid,
|
||||
data,
|
||||
datetime.utcnow().isoformat() + 'Z',
|
||||
), namespace=self.admin_namespace)
|
||||
return ret
|
||||
|
||||
async def _handle_eio_connect(self, eio_sid, environ):
|
||||
if self.stop_stats_event is None:
|
||||
self.stop_stats_event = self.sio.eio.create_event()
|
||||
self.stats_task = self.sio.start_background_task(
|
||||
self._emit_server_stats)
|
||||
|
||||
self.event_buffer.push('rawConnection')
|
||||
return await self.sio._handle_eio_connect(eio_sid, environ)
|
||||
|
||||
async def _handle_eio_disconnect(self, eio_sid):
|
||||
self.event_buffer.push('rawDisconnection')
|
||||
return await self.sio._handle_eio_disconnect(eio_sid)
|
||||
|
||||
def _eio_http_response(self, packets=None, headers=None, jsonp_index=None):
|
||||
ret = self.sio.eio.__ok(packets=packets, headers=headers,
|
||||
jsonp_index=jsonp_index)
|
||||
self.event_buffer.push('packetsOut')
|
||||
self.event_buffer.push('bytesOut', len(ret['response']))
|
||||
return ret
|
||||
|
||||
async def _eio_handle_post_request(socket, self, environ):
|
||||
ret = await socket.__handle_post_request(environ)
|
||||
self.event_buffer.push('packetsIn')
|
||||
self.event_buffer.push(
|
||||
'bytesIn', int(environ.get('CONTENT_LENGTH', 0)))
|
||||
return ret
|
||||
|
||||
async def _eio_websocket_handler(socket, self, ws):
|
||||
async def _send(ws, data):
|
||||
self.event_buffer.push('packetsOut')
|
||||
self.event_buffer.push('bytesOut', len(data))
|
||||
return await ws.__send(data)
|
||||
|
||||
async def _wait(ws):
|
||||
ret = await ws.__wait()
|
||||
self.event_buffer.push('packetsIn')
|
||||
self.event_buffer.push('bytesIn', len(ret or ''))
|
||||
return ret
|
||||
|
||||
ws.__send = ws.send
|
||||
ws.send = functools.partial(_send, ws)
|
||||
ws.__wait = ws.wait
|
||||
ws.wait = functools.partial(_wait, ws)
|
||||
return await socket.__websocket_handler(ws)
|
||||
|
||||
async def _eio_send_ping(socket, self): # pragma: no cover
|
||||
eio_sid = socket.sid
|
||||
t = time.time()
|
||||
for namespace in self.sio.manager.get_namespaces():
|
||||
sid = self.sio.manager.sid_from_eio_sid(eio_sid, namespace)
|
||||
if sid:
|
||||
serialized_socket = self.serialize_socket(sid, namespace,
|
||||
eio_sid)
|
||||
await self.sio.emit('socket_connected', (
|
||||
serialized_socket,
|
||||
datetime.utcfromtimestamp(t).isoformat() + 'Z',
|
||||
), namespace=self.admin_namespace)
|
||||
return await socket.__send_ping()
|
||||
|
||||
async def _emit_server_stats(self):
|
||||
start_time = time.time()
|
||||
namespaces = list(self.sio.handlers.keys())
|
||||
namespaces.sort()
|
||||
while not self.stop_stats_event.is_set():
|
||||
await self.sio.sleep(self.server_stats_interval)
|
||||
await self.sio.emit('server_stats', {
|
||||
'serverId': self.server_id,
|
||||
'hostname': HOSTNAME,
|
||||
'pid': PID,
|
||||
'uptime': time.time() - start_time,
|
||||
'clientsCount': len(self.sio.eio.sockets),
|
||||
'pollingClientsCount': len(
|
||||
[s for s in self.sio.eio.sockets.values()
|
||||
if not s.upgraded]),
|
||||
'aggregatedEvents': self.event_buffer.get_and_clear(),
|
||||
'namespaces': [{
|
||||
'name': nsp,
|
||||
'socketsCount': len(self.sio.manager.rooms.get(
|
||||
nsp, {None: []}).get(None, []))
|
||||
} for nsp in namespaces],
|
||||
}, namespace=self.admin_namespace)
|
||||
while self.admin_queue:
|
||||
event, args = self.admin_queue.pop(0)
|
||||
await self.sio.emit(event, args,
|
||||
namespace=self.admin_namespace)
|
||||
|
||||
def serialize_socket(self, sid, namespace, eio_sid=None):
|
||||
if eio_sid is None: # pragma: no cover
|
||||
eio_sid = self.sio.manager.eio_sid_from_sid(sid)
|
||||
socket = self.sio.eio._get_socket(eio_sid)
|
||||
environ = self.sio.environ.get(eio_sid, {})
|
||||
tm = self.sio.manager._timestamps[sid] if sid in \
|
||||
self.sio.manager._timestamps else 0
|
||||
return {
|
||||
'id': sid,
|
||||
'clientId': eio_sid,
|
||||
'transport': 'websocket' if socket.upgraded else 'polling',
|
||||
'nsp': namespace,
|
||||
'data': {},
|
||||
'handshake': {
|
||||
'address': environ.get('REMOTE_ADDR', ''),
|
||||
'headers': {k[5:].lower(): v for k, v in environ.items()
|
||||
if k.startswith('HTTP_')},
|
||||
'query': {k: v[0] if len(v) == 1 else v for k, v in parse_qs(
|
||||
environ.get('QUERY_STRING', '')).items()},
|
||||
'secure': environ.get('wsgi.url_scheme', '') == 'https',
|
||||
'url': environ.get('PATH_INFO', ''),
|
||||
'issued': tm * 1000,
|
||||
'time': datetime.utcfromtimestamp(tm).isoformat() + 'Z'
|
||||
if tm else '',
|
||||
},
|
||||
'rooms': self.sio.manager.get_rooms(sid, namespace),
|
||||
}
|
||||
@ -0,0 +1,126 @@
|
||||
import asyncio
|
||||
import pickle
|
||||
|
||||
from .async_pubsub_manager import AsyncPubSubManager
|
||||
|
||||
try:
|
||||
import aio_pika
|
||||
except ImportError:
|
||||
aio_pika = None
|
||||
|
||||
|
||||
class AsyncAioPikaManager(AsyncPubSubManager): # pragma: no cover
|
||||
"""Client manager that uses aio_pika for inter-process messaging under
|
||||
asyncio.
|
||||
|
||||
This class implements a client manager backend for event sharing across
|
||||
multiple processes, using RabbitMQ
|
||||
|
||||
To use a aio_pika backend, initialize the :class:`Server` instance as
|
||||
follows::
|
||||
|
||||
url = 'amqp://user:password@hostname:port//'
|
||||
server = socketio.Server(client_manager=socketio.AsyncAioPikaManager(
|
||||
url))
|
||||
|
||||
:param url: The connection URL for the backend messaging queue. Example
|
||||
connection URLs are ``'amqp://guest:guest@localhost:5672//'``
|
||||
for RabbitMQ.
|
||||
:param channel: The channel name on which the server sends and receives
|
||||
notifications. Must be the same in all the servers.
|
||||
With this manager, the channel name is the exchange name
|
||||
in rabbitmq
|
||||
:param write_only: If set to ``True``, only initialize to emit events. The
|
||||
default of ``False`` initializes the class for emitting
|
||||
and receiving.
|
||||
"""
|
||||
|
||||
name = 'asyncaiopika'
|
||||
|
||||
def __init__(self, url='amqp://guest:guest@localhost:5672//',
|
||||
channel='socketio', write_only=False, logger=None):
|
||||
if aio_pika is None:
|
||||
raise RuntimeError('aio_pika package is not installed '
|
||||
'(Run "pip install aio_pika" in your '
|
||||
'virtualenv).')
|
||||
self.url = url
|
||||
self._lock = asyncio.Lock()
|
||||
self.publisher_connection = None
|
||||
self.publisher_channel = None
|
||||
self.publisher_exchange = None
|
||||
super().__init__(channel=channel, write_only=write_only, logger=logger)
|
||||
|
||||
async def _connection(self):
|
||||
return await aio_pika.connect_robust(self.url)
|
||||
|
||||
async def _channel(self, connection):
|
||||
return await connection.channel()
|
||||
|
||||
async def _exchange(self, channel):
|
||||
return await channel.declare_exchange(self.channel,
|
||||
aio_pika.ExchangeType.FANOUT)
|
||||
|
||||
async def _queue(self, channel, exchange):
|
||||
queue = await channel.declare_queue(durable=False,
|
||||
arguments={'x-expires': 300000})
|
||||
await queue.bind(exchange)
|
||||
return queue
|
||||
|
||||
async def _publish(self, data):
|
||||
if self.publisher_connection is None:
|
||||
async with self._lock:
|
||||
if self.publisher_connection is None:
|
||||
self.publisher_connection = await self._connection()
|
||||
self.publisher_channel = await self._channel(
|
||||
self.publisher_connection
|
||||
)
|
||||
self.publisher_exchange = await self._exchange(
|
||||
self.publisher_channel
|
||||
)
|
||||
retry = True
|
||||
while True:
|
||||
try:
|
||||
await self.publisher_exchange.publish(
|
||||
aio_pika.Message(
|
||||
body=pickle.dumps(data),
|
||||
delivery_mode=aio_pika.DeliveryMode.PERSISTENT
|
||||
), routing_key='*',
|
||||
)
|
||||
break
|
||||
except aio_pika.AMQPException:
|
||||
if retry:
|
||||
self._get_logger().error('Cannot publish to rabbitmq... '
|
||||
'retrying')
|
||||
retry = False
|
||||
else:
|
||||
self._get_logger().error(
|
||||
'Cannot publish to rabbitmq... giving up')
|
||||
break
|
||||
except aio_pika.exceptions.ChannelInvalidStateError:
|
||||
# aio_pika raises this exception when the task is cancelled
|
||||
raise asyncio.CancelledError()
|
||||
|
||||
async def _listen(self):
|
||||
async with (await self._connection()) as connection:
|
||||
channel = await self._channel(connection)
|
||||
await channel.set_qos(prefetch_count=1)
|
||||
exchange = await self._exchange(channel)
|
||||
queue = await self._queue(channel, exchange)
|
||||
|
||||
retry_sleep = 1
|
||||
while True:
|
||||
try:
|
||||
async with queue.iterator() as queue_iter:
|
||||
async for message in queue_iter:
|
||||
async with message.process():
|
||||
yield pickle.loads(message.body)
|
||||
retry_sleep = 1
|
||||
except aio_pika.AMQPException:
|
||||
self._get_logger().error(
|
||||
'Cannot receive from rabbitmq... '
|
||||
'retrying in {} secs'.format(retry_sleep))
|
||||
await asyncio.sleep(retry_sleep)
|
||||
retry_sleep = min(retry_sleep * 2, 60)
|
||||
except aio_pika.exceptions.ChannelInvalidStateError:
|
||||
# aio_pika raises this exception when the task is cancelled
|
||||
raise asyncio.CancelledError()
|
||||
586
venv/lib/python3.12/site-packages/socketio/async_client.py
Normal file
586
venv/lib/python3.12/site-packages/socketio/async_client.py
Normal file
@ -0,0 +1,586 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import random
|
||||
|
||||
import engineio
|
||||
|
||||
from . import base_client
|
||||
from . import exceptions
|
||||
from . import packet
|
||||
|
||||
default_logger = logging.getLogger('socketio.client')
|
||||
|
||||
|
||||
class AsyncClient(base_client.BaseClient):
|
||||
"""A Socket.IO client for asyncio.
|
||||
|
||||
This class implements a fully compliant Socket.IO web client with support
|
||||
for websocket and long-polling transports.
|
||||
|
||||
:param reconnection: ``True`` if the client should automatically attempt to
|
||||
reconnect to the server after an interruption, or
|
||||
``False`` to not reconnect. The default is ``True``.
|
||||
:param reconnection_attempts: How many reconnection attempts to issue
|
||||
before giving up, or 0 for infinite attempts.
|
||||
The default is 0.
|
||||
:param reconnection_delay: How long to wait in seconds before the first
|
||||
reconnection attempt. Each successive attempt
|
||||
doubles this delay.
|
||||
:param reconnection_delay_max: The maximum delay between reconnection
|
||||
attempts.
|
||||
:param randomization_factor: Randomization amount for each delay between
|
||||
reconnection attempts. The default is 0.5,
|
||||
which means that each delay is randomly
|
||||
adjusted by +/- 50%.
|
||||
:param logger: To enable logging set to ``True`` or pass a logger object to
|
||||
use. To disable logging set to ``False``. The default is
|
||||
``False``. Note that fatal errors are logged even when
|
||||
``logger`` is ``False``.
|
||||
:param json: An alternative json module to use for encoding and decoding
|
||||
packets. Custom json modules must have ``dumps`` and ``loads``
|
||||
functions that are compatible with the standard library
|
||||
versions.
|
||||
:param handle_sigint: Set to ``True`` to automatically handle disconnection
|
||||
when the process is interrupted, or to ``False`` to
|
||||
leave interrupt handling to the calling application.
|
||||
Interrupt handling can only be enabled when the
|
||||
client instance is created in the main thread.
|
||||
|
||||
The Engine.IO configuration supports the following settings:
|
||||
|
||||
:param request_timeout: A timeout in seconds for requests. The default is
|
||||
5 seconds.
|
||||
:param http_session: an initialized ``aiohttp.ClientSession`` object to be
|
||||
used when sending requests to the server. Use it if
|
||||
you need to add special client options such as proxy
|
||||
servers, SSL certificates, custom CA bundle, etc.
|
||||
:param ssl_verify: ``True`` to verify SSL certificates, or ``False`` to
|
||||
skip SSL certificate verification, allowing
|
||||
connections to servers with self signed certificates.
|
||||
The default is ``True``.
|
||||
:param websocket_extra_options: Dictionary containing additional keyword
|
||||
arguments passed to
|
||||
``websocket.create_connection()``.
|
||||
:param engineio_logger: To enable Engine.IO logging set to ``True`` or pass
|
||||
a logger object to use. To disable logging set to
|
||||
``False``. The default is ``False``. Note that
|
||||
fatal errors are logged even when
|
||||
``engineio_logger`` is ``False``.
|
||||
"""
|
||||
def is_asyncio_based(self):
|
||||
return True
|
||||
|
||||
async def connect(self, url, headers={}, auth=None, transports=None,
|
||||
namespaces=None, socketio_path='socket.io', wait=True,
|
||||
wait_timeout=1, retry=False):
|
||||
"""Connect to a Socket.IO server.
|
||||
|
||||
:param url: The URL of the Socket.IO server. It can include custom
|
||||
query string parameters if required by the server. If a
|
||||
function is provided, the client will invoke it to obtain
|
||||
the URL each time a connection or reconnection is
|
||||
attempted.
|
||||
:param headers: A dictionary with custom headers to send with the
|
||||
connection request. If a function is provided, the
|
||||
client will invoke it to obtain the headers dictionary
|
||||
each time a connection or reconnection is attempted.
|
||||
:param auth: Authentication data passed to the server with the
|
||||
connection request, normally a dictionary with one or
|
||||
more string key/value pairs. If a function is provided,
|
||||
the client will invoke it to obtain the authentication
|
||||
data each time a connection or reconnection is attempted.
|
||||
:param transports: The list of allowed transports. Valid transports
|
||||
are ``'polling'`` and ``'websocket'``. If not
|
||||
given, the polling transport is connected first,
|
||||
then an upgrade to websocket is attempted.
|
||||
:param namespaces: The namespaces to connect as a string or list of
|
||||
strings. If not given, the namespaces that have
|
||||
registered event handlers are connected.
|
||||
:param socketio_path: The endpoint where the Socket.IO server is
|
||||
installed. The default value is appropriate for
|
||||
most cases.
|
||||
:param wait: if set to ``True`` (the default) the call only returns
|
||||
when all the namespaces are connected. If set to
|
||||
``False``, the call returns as soon as the Engine.IO
|
||||
transport is connected, and the namespaces will connect
|
||||
in the background.
|
||||
:param wait_timeout: How long the client should wait for the
|
||||
connection. The default is 1 second. This
|
||||
argument is only considered when ``wait`` is set
|
||||
to ``True``.
|
||||
:param retry: Apply the reconnection logic if the initial connection
|
||||
attempt fails. The default is ``False``.
|
||||
|
||||
Note: this method is a coroutine.
|
||||
|
||||
Example usage::
|
||||
|
||||
sio = socketio.AsyncClient()
|
||||
sio.connect('http://localhost:5000')
|
||||
"""
|
||||
if self.connected:
|
||||
raise exceptions.ConnectionError('Already connected')
|
||||
|
||||
self.connection_url = url
|
||||
self.connection_headers = headers
|
||||
self.connection_auth = auth
|
||||
self.connection_transports = transports
|
||||
self.connection_namespaces = namespaces
|
||||
self.socketio_path = socketio_path
|
||||
|
||||
if namespaces is None:
|
||||
namespaces = list(set(self.handlers.keys()).union(
|
||||
set(self.namespace_handlers.keys())))
|
||||
if '*' in namespaces:
|
||||
namespaces.remove('*')
|
||||
if len(namespaces) == 0:
|
||||
namespaces = ['/']
|
||||
elif isinstance(namespaces, str):
|
||||
namespaces = [namespaces]
|
||||
self.connection_namespaces = namespaces
|
||||
self.namespaces = {}
|
||||
if self._connect_event is None:
|
||||
self._connect_event = self.eio.create_event()
|
||||
else:
|
||||
self._connect_event.clear()
|
||||
real_url = await self._get_real_value(self.connection_url)
|
||||
real_headers = await self._get_real_value(self.connection_headers)
|
||||
try:
|
||||
await self.eio.connect(real_url, headers=real_headers,
|
||||
transports=transports,
|
||||
engineio_path=socketio_path)
|
||||
except engineio.exceptions.ConnectionError as exc:
|
||||
for n in self.connection_namespaces:
|
||||
await self._trigger_event(
|
||||
'connect_error', n,
|
||||
exc.args[1] if len(exc.args) > 1 else exc.args[0])
|
||||
if retry: # pragma: no cover
|
||||
await self._handle_reconnect()
|
||||
if self.eio.state == 'connected':
|
||||
return
|
||||
raise exceptions.ConnectionError(exc.args[0]) from None
|
||||
|
||||
if wait:
|
||||
try:
|
||||
while True:
|
||||
await asyncio.wait_for(self._connect_event.wait(),
|
||||
wait_timeout)
|
||||
self._connect_event.clear()
|
||||
if set(self.namespaces) == set(self.connection_namespaces):
|
||||
break
|
||||
except asyncio.TimeoutError:
|
||||
pass
|
||||
if set(self.namespaces) != set(self.connection_namespaces):
|
||||
await self.disconnect()
|
||||
raise exceptions.ConnectionError(
|
||||
'One or more namespaces failed to connect')
|
||||
|
||||
self.connected = True
|
||||
|
||||
async def wait(self):
|
||||
"""Wait until the connection with the server ends.
|
||||
|
||||
Client applications can use this function to block the main thread
|
||||
during the life of the connection.
|
||||
|
||||
Note: this method is a coroutine.
|
||||
"""
|
||||
while True:
|
||||
await self.eio.wait()
|
||||
await self.sleep(1) # give the reconnect task time to start up
|
||||
if not self._reconnect_task:
|
||||
break
|
||||
await self._reconnect_task
|
||||
if self.eio.state != 'connected':
|
||||
break
|
||||
|
||||
async def emit(self, event, data=None, namespace=None, callback=None):
|
||||
"""Emit a custom event to the server.
|
||||
|
||||
:param event: The event name. It can be any string. The event names
|
||||
``'connect'``, ``'message'`` and ``'disconnect'`` are
|
||||
reserved and should not be used.
|
||||
:param data: The data to send to the server. Data can be of
|
||||
type ``str``, ``bytes``, ``list`` or ``dict``. To send
|
||||
multiple arguments, use a tuple where each element is of
|
||||
one of the types indicated above.
|
||||
:param namespace: The Socket.IO namespace for the event. If this
|
||||
argument is omitted the event is emitted to the
|
||||
default namespace.
|
||||
:param callback: If given, this function will be called to acknowledge
|
||||
the server has received the message. The arguments
|
||||
that will be passed to the function are those provided
|
||||
by the server.
|
||||
|
||||
Note: this method is not designed to be used concurrently. If multiple
|
||||
tasks are emitting at the same time on the same client connection, then
|
||||
messages composed of multiple packets may end up being sent in an
|
||||
incorrect sequence. Use standard concurrency solutions (such as a Lock
|
||||
object) to prevent this situation.
|
||||
|
||||
Note 2: this method is a coroutine.
|
||||
"""
|
||||
namespace = namespace or '/'
|
||||
if namespace not in self.namespaces:
|
||||
raise exceptions.BadNamespaceError(
|
||||
namespace + ' is not a connected namespace.')
|
||||
self.logger.info('Emitting event "%s" [%s]', event, namespace)
|
||||
if callback is not None:
|
||||
id = self._generate_ack_id(namespace, callback)
|
||||
else:
|
||||
id = None
|
||||
# tuples are expanded to multiple arguments, everything else is sent
|
||||
# as a single argument
|
||||
if isinstance(data, tuple):
|
||||
data = list(data)
|
||||
elif data is not None:
|
||||
data = [data]
|
||||
else:
|
||||
data = []
|
||||
await self._send_packet(self.packet_class(
|
||||
packet.EVENT, namespace=namespace, data=[event] + data, id=id))
|
||||
|
||||
async def send(self, data, namespace=None, callback=None):
|
||||
"""Send a message to the server.
|
||||
|
||||
This function emits an event with the name ``'message'``. Use
|
||||
:func:`emit` to issue custom event names.
|
||||
|
||||
:param data: The data to send to the server. Data can be of
|
||||
type ``str``, ``bytes``, ``list`` or ``dict``. To send
|
||||
multiple arguments, use a tuple where each element is of
|
||||
one of the types indicated above.
|
||||
:param namespace: The Socket.IO namespace for the event. If this
|
||||
argument is omitted the event is emitted to the
|
||||
default namespace.
|
||||
:param callback: If given, this function will be called to acknowledge
|
||||
the server has received the message. The arguments
|
||||
that will be passed to the function are those provided
|
||||
by the server.
|
||||
|
||||
Note: this method is a coroutine.
|
||||
"""
|
||||
await self.emit('message', data=data, namespace=namespace,
|
||||
callback=callback)
|
||||
|
||||
async def call(self, event, data=None, namespace=None, timeout=60):
|
||||
"""Emit a custom event to the server and wait for the response.
|
||||
|
||||
This method issues an emit with a callback and waits for the callback
|
||||
to be invoked before returning. If the callback isn't invoked before
|
||||
the timeout, then a ``TimeoutError`` exception is raised. If the
|
||||
Socket.IO connection drops during the wait, this method still waits
|
||||
until the specified timeout.
|
||||
|
||||
:param event: The event name. It can be any string. The event names
|
||||
``'connect'``, ``'message'`` and ``'disconnect'`` are
|
||||
reserved and should not be used.
|
||||
:param data: The data to send to the server. Data can be of
|
||||
type ``str``, ``bytes``, ``list`` or ``dict``. To send
|
||||
multiple arguments, use a tuple where each element is of
|
||||
one of the types indicated above.
|
||||
:param namespace: The Socket.IO namespace for the event. If this
|
||||
argument is omitted the event is emitted to the
|
||||
default namespace.
|
||||
:param timeout: The waiting timeout. If the timeout is reached before
|
||||
the server acknowledges the event, then a
|
||||
``TimeoutError`` exception is raised.
|
||||
|
||||
Note: this method is not designed to be used concurrently. If multiple
|
||||
tasks are emitting at the same time on the same client connection, then
|
||||
messages composed of multiple packets may end up being sent in an
|
||||
incorrect sequence. Use standard concurrency solutions (such as a Lock
|
||||
object) to prevent this situation.
|
||||
|
||||
Note 2: this method is a coroutine.
|
||||
"""
|
||||
callback_event = self.eio.create_event()
|
||||
callback_args = []
|
||||
|
||||
def event_callback(*args):
|
||||
callback_args.append(args)
|
||||
callback_event.set()
|
||||
|
||||
await self.emit(event, data=data, namespace=namespace,
|
||||
callback=event_callback)
|
||||
try:
|
||||
await asyncio.wait_for(callback_event.wait(), timeout)
|
||||
except asyncio.TimeoutError:
|
||||
raise exceptions.TimeoutError() from None
|
||||
return callback_args[0] if len(callback_args[0]) > 1 \
|
||||
else callback_args[0][0] if len(callback_args[0]) == 1 \
|
||||
else None
|
||||
|
||||
async def disconnect(self):
|
||||
"""Disconnect from the server.
|
||||
|
||||
Note: this method is a coroutine.
|
||||
"""
|
||||
# here we just request the disconnection
|
||||
# later in _handle_eio_disconnect we invoke the disconnect handler
|
||||
for n in self.namespaces:
|
||||
await self._send_packet(self.packet_class(packet.DISCONNECT,
|
||||
namespace=n))
|
||||
await self.eio.disconnect(abort=True)
|
||||
|
||||
async def shutdown(self):
|
||||
"""Stop the client.
|
||||
|
||||
If the client is connected to a server, it is disconnected. If the
|
||||
client is attempting to reconnect to server, the reconnection attempts
|
||||
are stopped. If the client is not connected to a server and is not
|
||||
attempting to reconnect, then this function does nothing.
|
||||
"""
|
||||
if self.connected:
|
||||
await self.disconnect()
|
||||
elif self._reconnect_task: # pragma: no branch
|
||||
self._reconnect_abort.set()
|
||||
print(self._reconnect_task)
|
||||
await self._reconnect_task
|
||||
|
||||
def start_background_task(self, target, *args, **kwargs):
|
||||
"""Start a background task using the appropriate async model.
|
||||
|
||||
This is a utility function that applications can use to start a
|
||||
background task using the method that is compatible with the
|
||||
selected async mode.
|
||||
|
||||
:param target: the target function to execute.
|
||||
:param args: arguments to pass to the function.
|
||||
:param kwargs: keyword arguments to pass to the function.
|
||||
|
||||
The return value is a ``asyncio.Task`` object.
|
||||
"""
|
||||
return self.eio.start_background_task(target, *args, **kwargs)
|
||||
|
||||
async def sleep(self, seconds=0):
|
||||
"""Sleep for the requested amount of time using the appropriate async
|
||||
model.
|
||||
|
||||
This is a utility function that applications can use to put a task to
|
||||
sleep without having to worry about using the correct call for the
|
||||
selected async mode.
|
||||
|
||||
Note: this method is a coroutine.
|
||||
"""
|
||||
return await self.eio.sleep(seconds)
|
||||
|
||||
async def _get_real_value(self, value):
|
||||
"""Return the actual value, for parameters that can also be given as
|
||||
callables."""
|
||||
if not callable(value):
|
||||
return value
|
||||
if asyncio.iscoroutinefunction(value):
|
||||
return await value()
|
||||
return value()
|
||||
|
||||
async def _send_packet(self, pkt):
|
||||
"""Send a Socket.IO packet to the server."""
|
||||
encoded_packet = pkt.encode()
|
||||
if isinstance(encoded_packet, list):
|
||||
for ep in encoded_packet:
|
||||
await self.eio.send(ep)
|
||||
else:
|
||||
await self.eio.send(encoded_packet)
|
||||
|
||||
async def _handle_connect(self, namespace, data):
|
||||
namespace = namespace or '/'
|
||||
if namespace not in self.namespaces:
|
||||
self.logger.info('Namespace {} is connected'.format(namespace))
|
||||
self.namespaces[namespace] = (data or {}).get('sid', self.sid)
|
||||
await self._trigger_event('connect', namespace=namespace)
|
||||
self._connect_event.set()
|
||||
|
||||
async def _handle_disconnect(self, namespace):
|
||||
if not self.connected:
|
||||
return
|
||||
namespace = namespace or '/'
|
||||
await self._trigger_event('disconnect', namespace=namespace)
|
||||
await self._trigger_event('__disconnect_final', namespace=namespace)
|
||||
if namespace in self.namespaces:
|
||||
del self.namespaces[namespace]
|
||||
if not self.namespaces:
|
||||
self.connected = False
|
||||
await self.eio.disconnect(abort=True)
|
||||
|
||||
async def _handle_event(self, namespace, id, data):
|
||||
namespace = namespace or '/'
|
||||
self.logger.info('Received event "%s" [%s]', data[0], namespace)
|
||||
r = await self._trigger_event(data[0], namespace, *data[1:])
|
||||
if id is not None:
|
||||
# send ACK packet with the response returned by the handler
|
||||
# tuples are expanded as multiple arguments
|
||||
if r is None:
|
||||
data = []
|
||||
elif isinstance(r, tuple):
|
||||
data = list(r)
|
||||
else:
|
||||
data = [r]
|
||||
await self._send_packet(self.packet_class(
|
||||
packet.ACK, namespace=namespace, id=id, data=data))
|
||||
|
||||
async def _handle_ack(self, namespace, id, data):
|
||||
namespace = namespace or '/'
|
||||
self.logger.info('Received ack [%s]', namespace)
|
||||
callback = None
|
||||
try:
|
||||
callback = self.callbacks[namespace][id]
|
||||
except KeyError:
|
||||
# if we get an unknown callback we just ignore it
|
||||
self.logger.warning('Unknown callback received, ignoring.')
|
||||
else:
|
||||
del self.callbacks[namespace][id]
|
||||
if callback is not None:
|
||||
if asyncio.iscoroutinefunction(callback):
|
||||
await callback(*data)
|
||||
else:
|
||||
callback(*data)
|
||||
|
||||
async def _handle_error(self, namespace, data):
|
||||
namespace = namespace or '/'
|
||||
self.logger.info('Connection to namespace {} was rejected'.format(
|
||||
namespace))
|
||||
if data is None:
|
||||
data = tuple()
|
||||
elif not isinstance(data, (tuple, list)):
|
||||
data = (data,)
|
||||
await self._trigger_event('connect_error', namespace, *data)
|
||||
self._connect_event.set()
|
||||
if namespace in self.namespaces:
|
||||
del self.namespaces[namespace]
|
||||
if namespace == '/':
|
||||
self.namespaces = {}
|
||||
self.connected = False
|
||||
|
||||
async def _trigger_event(self, event, namespace, *args):
|
||||
"""Invoke an application event handler."""
|
||||
# first see if we have an explicit handler for the event
|
||||
handler, args = self._get_event_handler(event, namespace, args)
|
||||
if handler:
|
||||
if asyncio.iscoroutinefunction(handler):
|
||||
try:
|
||||
ret = await handler(*args)
|
||||
except asyncio.CancelledError: # pragma: no cover
|
||||
ret = None
|
||||
else:
|
||||
ret = handler(*args)
|
||||
return ret
|
||||
|
||||
# or else, forward the event to a namepsace handler if one exists
|
||||
handler, args = self._get_namespace_handler(namespace, args)
|
||||
if handler:
|
||||
return await handler.trigger_event(event, *args)
|
||||
|
||||
async def _handle_reconnect(self):
|
||||
if self._reconnect_abort is None: # pragma: no cover
|
||||
self._reconnect_abort = self.eio.create_event()
|
||||
self._reconnect_abort.clear()
|
||||
base_client.reconnecting_clients.append(self)
|
||||
attempt_count = 0
|
||||
current_delay = self.reconnection_delay
|
||||
while True:
|
||||
delay = current_delay
|
||||
current_delay *= 2
|
||||
if delay > self.reconnection_delay_max:
|
||||
delay = self.reconnection_delay_max
|
||||
delay += self.randomization_factor * (2 * random.random() - 1)
|
||||
self.logger.info(
|
||||
'Connection failed, new attempt in {:.02f} seconds'.format(
|
||||
delay))
|
||||
abort = False
|
||||
try:
|
||||
await asyncio.wait_for(self._reconnect_abort.wait(), delay)
|
||||
abort = True
|
||||
except asyncio.TimeoutError:
|
||||
pass
|
||||
except asyncio.CancelledError: # pragma: no cover
|
||||
abort = True
|
||||
if abort:
|
||||
self.logger.info('Reconnect task aborted')
|
||||
for n in self.connection_namespaces:
|
||||
await self._trigger_event('__disconnect_final',
|
||||
namespace=n)
|
||||
break
|
||||
attempt_count += 1
|
||||
try:
|
||||
await self.connect(self.connection_url,
|
||||
headers=self.connection_headers,
|
||||
auth=self.connection_auth,
|
||||
transports=self.connection_transports,
|
||||
namespaces=self.connection_namespaces,
|
||||
socketio_path=self.socketio_path,
|
||||
retry=False)
|
||||
except (exceptions.ConnectionError, ValueError):
|
||||
pass
|
||||
else:
|
||||
self.logger.info('Reconnection successful')
|
||||
self._reconnect_task = None
|
||||
break
|
||||
if self.reconnection_attempts and \
|
||||
attempt_count >= self.reconnection_attempts:
|
||||
self.logger.info(
|
||||
'Maximum reconnection attempts reached, giving up')
|
||||
for n in self.connection_namespaces:
|
||||
await self._trigger_event('__disconnect_final',
|
||||
namespace=n)
|
||||
break
|
||||
base_client.reconnecting_clients.remove(self)
|
||||
|
||||
async def _handle_eio_connect(self):
|
||||
"""Handle the Engine.IO connection event."""
|
||||
self.logger.info('Engine.IO connection established')
|
||||
self.sid = self.eio.sid
|
||||
real_auth = await self._get_real_value(self.connection_auth) or {}
|
||||
for n in self.connection_namespaces:
|
||||
await self._send_packet(self.packet_class(
|
||||
packet.CONNECT, data=real_auth, namespace=n))
|
||||
|
||||
async def _handle_eio_message(self, data):
|
||||
"""Dispatch Engine.IO messages."""
|
||||
if self._binary_packet:
|
||||
pkt = self._binary_packet
|
||||
if pkt.add_attachment(data):
|
||||
self._binary_packet = None
|
||||
if pkt.packet_type == packet.BINARY_EVENT:
|
||||
await self._handle_event(pkt.namespace, pkt.id, pkt.data)
|
||||
else:
|
||||
await self._handle_ack(pkt.namespace, pkt.id, pkt.data)
|
||||
else:
|
||||
pkt = self.packet_class(encoded_packet=data)
|
||||
if pkt.packet_type == packet.CONNECT:
|
||||
await self._handle_connect(pkt.namespace, pkt.data)
|
||||
elif pkt.packet_type == packet.DISCONNECT:
|
||||
await self._handle_disconnect(pkt.namespace)
|
||||
elif pkt.packet_type == packet.EVENT:
|
||||
await self._handle_event(pkt.namespace, pkt.id, pkt.data)
|
||||
elif pkt.packet_type == packet.ACK:
|
||||
await self._handle_ack(pkt.namespace, pkt.id, pkt.data)
|
||||
elif pkt.packet_type == packet.BINARY_EVENT or \
|
||||
pkt.packet_type == packet.BINARY_ACK:
|
||||
self._binary_packet = pkt
|
||||
elif pkt.packet_type == packet.CONNECT_ERROR:
|
||||
await self._handle_error(pkt.namespace, pkt.data)
|
||||
else:
|
||||
raise ValueError('Unknown packet type.')
|
||||
|
||||
async def _handle_eio_disconnect(self):
|
||||
"""Handle the Engine.IO disconnection event."""
|
||||
self.logger.info('Engine.IO connection dropped')
|
||||
will_reconnect = self.reconnection and self.eio.state == 'connected'
|
||||
if self.connected:
|
||||
for n in self.namespaces:
|
||||
await self._trigger_event('disconnect', namespace=n)
|
||||
if not will_reconnect:
|
||||
await self._trigger_event('__disconnect_final',
|
||||
namespace=n)
|
||||
self.namespaces = {}
|
||||
self.connected = False
|
||||
self.callbacks = {}
|
||||
self._binary_packet = None
|
||||
self.sid = None
|
||||
if will_reconnect:
|
||||
self._reconnect_task = self.start_background_task(
|
||||
self._handle_reconnect)
|
||||
|
||||
def _engineio_client_class(self):
|
||||
return engineio.AsyncClient
|
||||
120
venv/lib/python3.12/site-packages/socketio/async_manager.py
Normal file
120
venv/lib/python3.12/site-packages/socketio/async_manager.py
Normal file
@ -0,0 +1,120 @@
|
||||
import asyncio
|
||||
|
||||
from engineio import packet as eio_packet
|
||||
from socketio import packet
|
||||
from .base_manager import BaseManager
|
||||
|
||||
|
||||
class AsyncManager(BaseManager):
|
||||
"""Manage a client list for an asyncio server."""
|
||||
async def can_disconnect(self, sid, namespace):
|
||||
return self.is_connected(sid, namespace)
|
||||
|
||||
async def emit(self, event, data, namespace, room=None, skip_sid=None,
|
||||
callback=None, to=None, **kwargs):
|
||||
"""Emit a message to a single client, a room, or all the clients
|
||||
connected to the namespace.
|
||||
|
||||
Note: this method is a coroutine.
|
||||
"""
|
||||
room = to or room
|
||||
if namespace not in self.rooms:
|
||||
return
|
||||
if isinstance(data, tuple):
|
||||
# tuples are expanded to multiple arguments, everything else is
|
||||
# sent as a single argument
|
||||
data = list(data)
|
||||
elif data is not None:
|
||||
data = [data]
|
||||
else:
|
||||
data = []
|
||||
if not isinstance(skip_sid, list):
|
||||
skip_sid = [skip_sid]
|
||||
tasks = []
|
||||
if not callback:
|
||||
# when callbacks aren't used the packets sent to each recipient are
|
||||
# identical, so they can be generated once and reused
|
||||
pkt = self.server.packet_class(
|
||||
packet.EVENT, namespace=namespace, data=[event] + data)
|
||||
encoded_packet = pkt.encode()
|
||||
if not isinstance(encoded_packet, list):
|
||||
encoded_packet = [encoded_packet]
|
||||
eio_pkt = [eio_packet.Packet(eio_packet.MESSAGE, p)
|
||||
for p in encoded_packet]
|
||||
for sid, eio_sid in self.get_participants(namespace, room):
|
||||
if sid not in skip_sid:
|
||||
for p in eio_pkt:
|
||||
tasks.append(asyncio.create_task(
|
||||
self.server._send_eio_packet(eio_sid, p)))
|
||||
else:
|
||||
# callbacks are used, so each recipient must be sent a packet that
|
||||
# contains a unique callback id
|
||||
# note that callbacks when addressing a group of people are
|
||||
# implemented but not tested or supported
|
||||
for sid, eio_sid in self.get_participants(namespace, room):
|
||||
if sid not in skip_sid: # pragma: no branch
|
||||
id = self._generate_ack_id(sid, callback)
|
||||
pkt = self.server.packet_class(
|
||||
packet.EVENT, namespace=namespace, data=[event] + data,
|
||||
id=id)
|
||||
tasks.append(asyncio.create_task(
|
||||
self.server._send_packet(eio_sid, pkt)))
|
||||
if tasks == []: # pragma: no cover
|
||||
return
|
||||
await asyncio.wait(tasks)
|
||||
|
||||
async def connect(self, eio_sid, namespace):
|
||||
"""Register a client connection to a namespace.
|
||||
|
||||
Note: this method is a coroutine.
|
||||
"""
|
||||
return super().connect(eio_sid, namespace)
|
||||
|
||||
async def disconnect(self, sid, namespace, **kwargs):
|
||||
"""Disconnect a client.
|
||||
|
||||
Note: this method is a coroutine.
|
||||
"""
|
||||
return self.basic_disconnect(sid, namespace, **kwargs)
|
||||
|
||||
async def enter_room(self, sid, namespace, room, eio_sid=None):
|
||||
"""Add a client to a room.
|
||||
|
||||
Note: this method is a coroutine.
|
||||
"""
|
||||
return self.basic_enter_room(sid, namespace, room, eio_sid=eio_sid)
|
||||
|
||||
async def leave_room(self, sid, namespace, room):
|
||||
"""Remove a client from a room.
|
||||
|
||||
Note: this method is a coroutine.
|
||||
"""
|
||||
return self.basic_leave_room(sid, namespace, room)
|
||||
|
||||
async def close_room(self, room, namespace):
|
||||
"""Remove all participants from a room.
|
||||
|
||||
Note: this method is a coroutine.
|
||||
"""
|
||||
return self.basic_close_room(room, namespace)
|
||||
|
||||
async def trigger_callback(self, sid, id, data):
|
||||
"""Invoke an application callback.
|
||||
|
||||
Note: this method is a coroutine.
|
||||
"""
|
||||
callback = None
|
||||
try:
|
||||
callback = self.callbacks[sid][id]
|
||||
except KeyError:
|
||||
# if we get an unknown callback we just ignore it
|
||||
self._get_logger().warning('Unknown callback received, ignoring.')
|
||||
else:
|
||||
del self.callbacks[sid][id]
|
||||
if callback is not None:
|
||||
ret = callback(*data)
|
||||
if asyncio.iscoroutine(ret):
|
||||
try:
|
||||
await ret
|
||||
except asyncio.CancelledError: # pragma: no cover
|
||||
pass
|
||||
255
venv/lib/python3.12/site-packages/socketio/async_namespace.py
Normal file
255
venv/lib/python3.12/site-packages/socketio/async_namespace.py
Normal file
@ -0,0 +1,255 @@
|
||||
import asyncio
|
||||
|
||||
from socketio import base_namespace
|
||||
|
||||
|
||||
class AsyncNamespace(base_namespace.BaseServerNamespace):
|
||||
"""Base class for asyncio server-side class-based namespaces.
|
||||
|
||||
A class-based namespace is a class that contains all the event handlers
|
||||
for a Socket.IO namespace. The event handlers are methods of the class
|
||||
with the prefix ``on_``, such as ``on_connect``, ``on_disconnect``,
|
||||
``on_message``, ``on_json``, and so on. These can be regular functions or
|
||||
coroutines.
|
||||
|
||||
:param namespace: The Socket.IO namespace to be used with all the event
|
||||
handlers defined in this class. If this argument is
|
||||
omitted, the default namespace is used.
|
||||
"""
|
||||
def is_asyncio_based(self):
|
||||
return True
|
||||
|
||||
async def trigger_event(self, event, *args):
|
||||
"""Dispatch an event to the proper handler method.
|
||||
|
||||
In the most common usage, this method is not overloaded by subclasses,
|
||||
as it performs the routing of events to methods. However, this
|
||||
method can be overridden if special dispatching rules are needed, or if
|
||||
having a single method that catches all events is desired.
|
||||
|
||||
Note: this method is a coroutine.
|
||||
"""
|
||||
handler_name = 'on_' + (event or '')
|
||||
if hasattr(self, handler_name):
|
||||
handler = getattr(self, handler_name)
|
||||
if asyncio.iscoroutinefunction(handler) is True:
|
||||
try:
|
||||
ret = await handler(*args)
|
||||
except asyncio.CancelledError: # pragma: no cover
|
||||
ret = None
|
||||
else:
|
||||
ret = handler(*args)
|
||||
return ret
|
||||
|
||||
async def emit(self, event, data=None, to=None, room=None, skip_sid=None,
|
||||
namespace=None, callback=None, ignore_queue=False):
|
||||
"""Emit a custom event to one or more connected clients.
|
||||
|
||||
The only difference with the :func:`socketio.Server.emit` method is
|
||||
that when the ``namespace`` argument is not given the namespace
|
||||
associated with the class is used.
|
||||
|
||||
Note: this method is a coroutine.
|
||||
"""
|
||||
return await self.server.emit(event, data=data, to=to, room=room,
|
||||
skip_sid=skip_sid,
|
||||
namespace=namespace or self.namespace,
|
||||
callback=callback,
|
||||
ignore_queue=ignore_queue)
|
||||
|
||||
async def send(self, data, to=None, room=None, skip_sid=None,
|
||||
namespace=None, callback=None, ignore_queue=False):
|
||||
"""Send a message to one or more connected clients.
|
||||
|
||||
The only difference with the :func:`socketio.Server.send` method is
|
||||
that when the ``namespace`` argument is not given the namespace
|
||||
associated with the class is used.
|
||||
|
||||
Note: this method is a coroutine.
|
||||
"""
|
||||
return await self.server.send(data, to=to, room=room,
|
||||
skip_sid=skip_sid,
|
||||
namespace=namespace or self.namespace,
|
||||
callback=callback,
|
||||
ignore_queue=ignore_queue)
|
||||
|
||||
async def call(self, event, data=None, to=None, sid=None, namespace=None,
|
||||
timeout=None, ignore_queue=False):
|
||||
"""Emit a custom event to a client and wait for the response.
|
||||
|
||||
The only difference with the :func:`socketio.Server.call` method is
|
||||
that when the ``namespace`` argument is not given the namespace
|
||||
associated with the class is used.
|
||||
"""
|
||||
return await self.server.call(event, data=data, to=to, sid=sid,
|
||||
namespace=namespace or self.namespace,
|
||||
timeout=timeout,
|
||||
ignore_queue=ignore_queue)
|
||||
|
||||
async def enter_room(self, sid, room, namespace=None):
|
||||
"""Enter a room.
|
||||
|
||||
The only difference with the :func:`socketio.Server.enter_room` method
|
||||
is that when the ``namespace`` argument is not given the namespace
|
||||
associated with the class is used.
|
||||
|
||||
Note: this method is a coroutine.
|
||||
"""
|
||||
return await self.server.enter_room(
|
||||
sid, room, namespace=namespace or self.namespace)
|
||||
|
||||
async def leave_room(self, sid, room, namespace=None):
|
||||
"""Leave a room.
|
||||
|
||||
The only difference with the :func:`socketio.Server.leave_room` method
|
||||
is that when the ``namespace`` argument is not given the namespace
|
||||
associated with the class is used.
|
||||
|
||||
Note: this method is a coroutine.
|
||||
"""
|
||||
return await self.server.leave_room(
|
||||
sid, room, namespace=namespace or self.namespace)
|
||||
|
||||
async def close_room(self, room, namespace=None):
|
||||
"""Close a room.
|
||||
|
||||
The only difference with the :func:`socketio.Server.close_room` method
|
||||
is that when the ``namespace`` argument is not given the namespace
|
||||
associated with the class is used.
|
||||
|
||||
Note: this method is a coroutine.
|
||||
"""
|
||||
return await self.server.close_room(
|
||||
room, namespace=namespace or self.namespace)
|
||||
|
||||
async def get_session(self, sid, namespace=None):
|
||||
"""Return the user session for a client.
|
||||
|
||||
The only difference with the :func:`socketio.Server.get_session`
|
||||
method is that when the ``namespace`` argument is not given the
|
||||
namespace associated with the class is used.
|
||||
|
||||
Note: this method is a coroutine.
|
||||
"""
|
||||
return await self.server.get_session(
|
||||
sid, namespace=namespace or self.namespace)
|
||||
|
||||
async def save_session(self, sid, session, namespace=None):
|
||||
"""Store the user session for a client.
|
||||
|
||||
The only difference with the :func:`socketio.Server.save_session`
|
||||
method is that when the ``namespace`` argument is not given the
|
||||
namespace associated with the class is used.
|
||||
|
||||
Note: this method is a coroutine.
|
||||
"""
|
||||
return await self.server.save_session(
|
||||
sid, session, namespace=namespace or self.namespace)
|
||||
|
||||
def session(self, sid, namespace=None):
|
||||
"""Return the user session for a client with context manager syntax.
|
||||
|
||||
The only difference with the :func:`socketio.Server.session` method is
|
||||
that when the ``namespace`` argument is not given the namespace
|
||||
associated with the class is used.
|
||||
"""
|
||||
return self.server.session(sid, namespace=namespace or self.namespace)
|
||||
|
||||
async def disconnect(self, sid, namespace=None):
|
||||
"""Disconnect a client.
|
||||
|
||||
The only difference with the :func:`socketio.Server.disconnect` method
|
||||
is that when the ``namespace`` argument is not given the namespace
|
||||
associated with the class is used.
|
||||
|
||||
Note: this method is a coroutine.
|
||||
"""
|
||||
return await self.server.disconnect(
|
||||
sid, namespace=namespace or self.namespace)
|
||||
|
||||
|
||||
class AsyncClientNamespace(base_namespace.BaseClientNamespace):
|
||||
"""Base class for asyncio client-side class-based namespaces.
|
||||
|
||||
A class-based namespace is a class that contains all the event handlers
|
||||
for a Socket.IO namespace. The event handlers are methods of the class
|
||||
with the prefix ``on_``, such as ``on_connect``, ``on_disconnect``,
|
||||
``on_message``, ``on_json``, and so on. These can be regular functions or
|
||||
coroutines.
|
||||
|
||||
:param namespace: The Socket.IO namespace to be used with all the event
|
||||
handlers defined in this class. If this argument is
|
||||
omitted, the default namespace is used.
|
||||
"""
|
||||
def is_asyncio_based(self):
|
||||
return True
|
||||
|
||||
async def trigger_event(self, event, *args):
|
||||
"""Dispatch an event to the proper handler method.
|
||||
|
||||
In the most common usage, this method is not overloaded by subclasses,
|
||||
as it performs the routing of events to methods. However, this
|
||||
method can be overridden if special dispatching rules are needed, or if
|
||||
having a single method that catches all events is desired.
|
||||
|
||||
Note: this method is a coroutine.
|
||||
"""
|
||||
handler_name = 'on_' + (event or '')
|
||||
if hasattr(self, handler_name):
|
||||
handler = getattr(self, handler_name)
|
||||
if asyncio.iscoroutinefunction(handler) is True:
|
||||
try:
|
||||
ret = await handler(*args)
|
||||
except asyncio.CancelledError: # pragma: no cover
|
||||
ret = None
|
||||
else:
|
||||
ret = handler(*args)
|
||||
return ret
|
||||
|
||||
async def emit(self, event, data=None, namespace=None, callback=None):
|
||||
"""Emit a custom event to the server.
|
||||
|
||||
The only difference with the :func:`socketio.Client.emit` method is
|
||||
that when the ``namespace`` argument is not given the namespace
|
||||
associated with the class is used.
|
||||
|
||||
Note: this method is a coroutine.
|
||||
"""
|
||||
return await self.client.emit(event, data=data,
|
||||
namespace=namespace or self.namespace,
|
||||
callback=callback)
|
||||
|
||||
async def send(self, data, namespace=None, callback=None):
|
||||
"""Send a message to the server.
|
||||
|
||||
The only difference with the :func:`socketio.Client.send` method is
|
||||
that when the ``namespace`` argument is not given the namespace
|
||||
associated with the class is used.
|
||||
|
||||
Note: this method is a coroutine.
|
||||
"""
|
||||
return await self.client.send(data,
|
||||
namespace=namespace or self.namespace,
|
||||
callback=callback)
|
||||
|
||||
async def call(self, event, data=None, namespace=None, timeout=None):
|
||||
"""Emit a custom event to the server and wait for the response.
|
||||
|
||||
The only difference with the :func:`socketio.Client.call` method is
|
||||
that when the ``namespace`` argument is not given the namespace
|
||||
associated with the class is used.
|
||||
"""
|
||||
return await self.client.call(event, data=data,
|
||||
namespace=namespace or self.namespace,
|
||||
timeout=timeout)
|
||||
|
||||
async def disconnect(self):
|
||||
"""Disconnect a client.
|
||||
|
||||
The only difference with the :func:`socketio.Client.disconnect` method
|
||||
is that when the ``namespace`` argument is not given the namespace
|
||||
associated with the class is used.
|
||||
|
||||
Note: this method is a coroutine.
|
||||
"""
|
||||
return await self.client.disconnect()
|
||||
@ -0,0 +1,243 @@
|
||||
import asyncio
|
||||
from functools import partial
|
||||
import uuid
|
||||
|
||||
from engineio import json
|
||||
import pickle
|
||||
|
||||
from .async_manager import AsyncManager
|
||||
|
||||
|
||||
class AsyncPubSubManager(AsyncManager):
|
||||
"""Manage a client list attached to a pub/sub backend under asyncio.
|
||||
|
||||
This is a base class that enables multiple servers to share the list of
|
||||
clients, with the servers communicating events through a pub/sub backend.
|
||||
The use of a pub/sub backend also allows any client connected to the
|
||||
backend to emit events addressed to Socket.IO clients.
|
||||
|
||||
The actual backends must be implemented by subclasses, this class only
|
||||
provides a pub/sub generic framework for asyncio applications.
|
||||
|
||||
:param channel: The channel name on which the server sends and receives
|
||||
notifications.
|
||||
"""
|
||||
name = 'asyncpubsub'
|
||||
|
||||
def __init__(self, channel='socketio', write_only=False, logger=None):
|
||||
super().__init__()
|
||||
self.channel = channel
|
||||
self.write_only = write_only
|
||||
self.host_id = uuid.uuid4().hex
|
||||
self.logger = logger
|
||||
|
||||
def initialize(self):
|
||||
super().initialize()
|
||||
if not self.write_only:
|
||||
self.thread = self.server.start_background_task(self._thread)
|
||||
self._get_logger().info(self.name + ' backend initialized.')
|
||||
|
||||
async def emit(self, event, data, namespace=None, room=None, skip_sid=None,
|
||||
callback=None, to=None, **kwargs):
|
||||
"""Emit a message to a single client, a room, or all the clients
|
||||
connected to the namespace.
|
||||
|
||||
This method takes care or propagating the message to all the servers
|
||||
that are connected through the message queue.
|
||||
|
||||
The parameters are the same as in :meth:`.Server.emit`.
|
||||
|
||||
Note: this method is a coroutine.
|
||||
"""
|
||||
room = to or room
|
||||
if kwargs.get('ignore_queue'):
|
||||
return await super().emit(
|
||||
event, data, namespace=namespace, room=room, skip_sid=skip_sid,
|
||||
callback=callback)
|
||||
namespace = namespace or '/'
|
||||
if callback is not None:
|
||||
if self.server is None:
|
||||
raise RuntimeError('Callbacks can only be issued from the '
|
||||
'context of a server.')
|
||||
if room is None:
|
||||
raise ValueError('Cannot use callback without a room set.')
|
||||
id = self._generate_ack_id(room, callback)
|
||||
callback = (room, namespace, id)
|
||||
else:
|
||||
callback = None
|
||||
message = {'method': 'emit', 'event': event, 'data': data,
|
||||
'namespace': namespace, 'room': room,
|
||||
'skip_sid': skip_sid, 'callback': callback,
|
||||
'host_id': self.host_id}
|
||||
await self._handle_emit(message) # handle in this host
|
||||
await self._publish(message) # notify other hosts
|
||||
|
||||
async def can_disconnect(self, sid, namespace):
|
||||
if self.is_connected(sid, namespace):
|
||||
# client is in this server, so we can disconnect directly
|
||||
return await super().can_disconnect(sid, namespace)
|
||||
else:
|
||||
# client is in another server, so we post request to the queue
|
||||
await self._publish({'method': 'disconnect', 'sid': sid,
|
||||
'namespace': namespace or '/',
|
||||
'host_id': self.host_id})
|
||||
|
||||
async def disconnect(self, sid, namespace, **kwargs):
|
||||
if kwargs.get('ignore_queue'):
|
||||
return await super().disconnect(
|
||||
sid, namespace=namespace)
|
||||
message = {'method': 'disconnect', 'sid': sid,
|
||||
'namespace': namespace or '/', 'host_id': self.host_id}
|
||||
await self._handle_disconnect(message) # handle in this host
|
||||
await self._publish(message) # notify other hosts
|
||||
|
||||
async def enter_room(self, sid, namespace, room, eio_sid=None):
|
||||
if self.is_connected(sid, namespace):
|
||||
# client is in this server, so we can disconnect directly
|
||||
return await super().enter_room(sid, namespace, room,
|
||||
eio_sid=eio_sid)
|
||||
else:
|
||||
message = {'method': 'enter_room', 'sid': sid, 'room': room,
|
||||
'namespace': namespace or '/', 'host_id': self.host_id}
|
||||
await self._publish(message) # notify other hosts
|
||||
|
||||
async def leave_room(self, sid, namespace, room):
|
||||
if self.is_connected(sid, namespace):
|
||||
# client is in this server, so we can disconnect directly
|
||||
return await super().leave_room(sid, namespace, room)
|
||||
else:
|
||||
message = {'method': 'leave_room', 'sid': sid, 'room': room,
|
||||
'namespace': namespace or '/', 'host_id': self.host_id}
|
||||
await self._publish(message) # notify other hosts
|
||||
|
||||
async def close_room(self, room, namespace=None):
|
||||
message = {'method': 'close_room', 'room': room,
|
||||
'namespace': namespace or '/', 'host_id': self.host_id}
|
||||
await self._handle_close_room(message) # handle in this host
|
||||
await self._publish(message) # notify other hosts
|
||||
|
||||
async def _publish(self, data):
|
||||
"""Publish a message on the Socket.IO channel.
|
||||
|
||||
This method needs to be implemented by the different subclasses that
|
||||
support pub/sub backends.
|
||||
"""
|
||||
raise NotImplementedError('This method must be implemented in a '
|
||||
'subclass.') # pragma: no cover
|
||||
|
||||
async def _listen(self):
|
||||
"""Return the next message published on the Socket.IO channel,
|
||||
blocking until a message is available.
|
||||
|
||||
This method needs to be implemented by the different subclasses that
|
||||
support pub/sub backends.
|
||||
"""
|
||||
raise NotImplementedError('This method must be implemented in a '
|
||||
'subclass.') # pragma: no cover
|
||||
|
||||
async def _handle_emit(self, message):
|
||||
# Events with callbacks are very tricky to handle across hosts
|
||||
# Here in the receiving end we set up a local callback that preserves
|
||||
# the callback host and id from the sender
|
||||
remote_callback = message.get('callback')
|
||||
remote_host_id = message.get('host_id')
|
||||
if remote_callback is not None and len(remote_callback) == 3:
|
||||
callback = partial(self._return_callback, remote_host_id,
|
||||
*remote_callback)
|
||||
else:
|
||||
callback = None
|
||||
await super().emit(message['event'], message['data'],
|
||||
namespace=message.get('namespace'),
|
||||
room=message.get('room'),
|
||||
skip_sid=message.get('skip_sid'),
|
||||
callback=callback)
|
||||
|
||||
async def _handle_callback(self, message):
|
||||
if self.host_id == message.get('host_id'):
|
||||
try:
|
||||
sid = message['sid']
|
||||
id = message['id']
|
||||
args = message['args']
|
||||
except KeyError:
|
||||
return
|
||||
await self.trigger_callback(sid, id, args)
|
||||
|
||||
async def _return_callback(self, host_id, sid, namespace, callback_id,
|
||||
*args):
|
||||
# When an event callback is received, the callback is returned back
|
||||
# the sender, which is identified by the host_id
|
||||
if host_id == self.host_id:
|
||||
await self.trigger_callback(sid, callback_id, args)
|
||||
else:
|
||||
await self._publish({'method': 'callback', 'host_id': host_id,
|
||||
'sid': sid, 'namespace': namespace,
|
||||
'id': callback_id, 'args': args})
|
||||
|
||||
async def _handle_disconnect(self, message):
|
||||
await self.server.disconnect(sid=message.get('sid'),
|
||||
namespace=message.get('namespace'),
|
||||
ignore_queue=True)
|
||||
|
||||
async def _handle_enter_room(self, message):
|
||||
sid = message.get('sid')
|
||||
namespace = message.get('namespace')
|
||||
if self.is_connected(sid, namespace):
|
||||
await super().enter_room(sid, namespace, message.get('room'))
|
||||
|
||||
async def _handle_leave_room(self, message):
|
||||
sid = message.get('sid')
|
||||
namespace = message.get('namespace')
|
||||
if self.is_connected(sid, namespace):
|
||||
await super().leave_room(sid, namespace, message.get('room'))
|
||||
|
||||
async def _handle_close_room(self, message):
|
||||
await super().close_room(room=message.get('room'),
|
||||
namespace=message.get('namespace'))
|
||||
|
||||
async def _thread(self):
|
||||
while True:
|
||||
try:
|
||||
async for message in self._listen(): # pragma: no branch
|
||||
data = None
|
||||
if isinstance(message, dict):
|
||||
data = message
|
||||
else:
|
||||
if isinstance(message, bytes): # pragma: no cover
|
||||
try:
|
||||
data = pickle.loads(message)
|
||||
except:
|
||||
pass
|
||||
if data is None:
|
||||
try:
|
||||
data = json.loads(message)
|
||||
except:
|
||||
pass
|
||||
if data and 'method' in data:
|
||||
self._get_logger().debug('pubsub message: {}'.format(
|
||||
data['method']))
|
||||
try:
|
||||
if data['method'] == 'callback':
|
||||
await self._handle_callback(data)
|
||||
elif data.get('host_id') != self.host_id:
|
||||
if data['method'] == 'emit':
|
||||
await self._handle_emit(data)
|
||||
elif data['method'] == 'disconnect':
|
||||
await self._handle_disconnect(data)
|
||||
elif data['method'] == 'enter_room':
|
||||
await self._handle_enter_room(data)
|
||||
elif data['method'] == 'leave_room':
|
||||
await self._handle_leave_room(data)
|
||||
elif data['method'] == 'close_room':
|
||||
await self._handle_close_room(data)
|
||||
except asyncio.CancelledError:
|
||||
raise # let the outer try/except handle it
|
||||
except Exception:
|
||||
self.server.logger.exception(
|
||||
'Handler error in pubsub listening thread')
|
||||
self.server.logger.error('pubsub listen() exited unexpectedly')
|
||||
break # loop should never exit except in unit tests!
|
||||
except asyncio.CancelledError: # pragma: no cover
|
||||
break
|
||||
except Exception: # pragma: no cover
|
||||
self.server.logger.exception('Unexpected Error in pubsub '
|
||||
'listening thread')
|
||||
@ -0,0 +1,107 @@
|
||||
import asyncio
|
||||
import pickle
|
||||
|
||||
try: # pragma: no cover
|
||||
from redis import asyncio as aioredis
|
||||
from redis.exceptions import RedisError
|
||||
except ImportError: # pragma: no cover
|
||||
try:
|
||||
import aioredis
|
||||
from aioredis.exceptions import RedisError
|
||||
except ImportError:
|
||||
aioredis = None
|
||||
RedisError = None
|
||||
|
||||
from .async_pubsub_manager import AsyncPubSubManager
|
||||
|
||||
|
||||
class AsyncRedisManager(AsyncPubSubManager): # pragma: no cover
|
||||
"""Redis based client manager for asyncio servers.
|
||||
|
||||
This class implements a Redis backend for event sharing across multiple
|
||||
processes.
|
||||
|
||||
To use a Redis backend, initialize the :class:`AsyncServer` instance as
|
||||
follows::
|
||||
|
||||
url = 'redis://hostname:port/0'
|
||||
server = socketio.AsyncServer(
|
||||
client_manager=socketio.AsyncRedisManager(url))
|
||||
|
||||
:param url: The connection URL for the Redis server. For a default Redis
|
||||
store running on the same host, use ``redis://``. To use an
|
||||
SSL connection, use ``rediss://``.
|
||||
:param channel: The channel name on which the server sends and receives
|
||||
notifications. Must be the same in all the servers.
|
||||
:param write_only: If set to ``True``, only initialize to emit events. The
|
||||
default of ``False`` initializes the class for emitting
|
||||
and receiving.
|
||||
:param redis_options: additional keyword arguments to be passed to
|
||||
``aioredis.from_url()``.
|
||||
"""
|
||||
name = 'aioredis'
|
||||
|
||||
def __init__(self, url='redis://localhost:6379/0', channel='socketio',
|
||||
write_only=False, logger=None, redis_options=None):
|
||||
if aioredis is None:
|
||||
raise RuntimeError('Redis package is not installed '
|
||||
'(Run "pip install redis" in your virtualenv).')
|
||||
if not hasattr(aioredis.Redis, 'from_url'):
|
||||
raise RuntimeError('Version 2 of aioredis package is required.')
|
||||
self.redis_url = url
|
||||
self.redis_options = redis_options or {}
|
||||
self._redis_connect()
|
||||
super().__init__(channel=channel, write_only=write_only, logger=logger)
|
||||
|
||||
def _redis_connect(self):
|
||||
self.redis = aioredis.Redis.from_url(self.redis_url,
|
||||
**self.redis_options)
|
||||
self.pubsub = self.redis.pubsub(ignore_subscribe_messages=True)
|
||||
|
||||
async def _publish(self, data):
|
||||
retry = True
|
||||
while True:
|
||||
try:
|
||||
if not retry:
|
||||
self._redis_connect()
|
||||
return await self.redis.publish(
|
||||
self.channel, pickle.dumps(data))
|
||||
except RedisError:
|
||||
if retry:
|
||||
self._get_logger().error('Cannot publish to redis... '
|
||||
'retrying')
|
||||
retry = False
|
||||
else:
|
||||
self._get_logger().error('Cannot publish to redis... '
|
||||
'giving up')
|
||||
break
|
||||
|
||||
async def _redis_listen_with_retries(self):
|
||||
retry_sleep = 1
|
||||
connect = False
|
||||
while True:
|
||||
try:
|
||||
if connect:
|
||||
self._redis_connect()
|
||||
await self.pubsub.subscribe(self.channel)
|
||||
retry_sleep = 1
|
||||
async for message in self.pubsub.listen():
|
||||
yield message
|
||||
except RedisError:
|
||||
self._get_logger().error('Cannot receive from redis... '
|
||||
'retrying in '
|
||||
'{} secs'.format(retry_sleep))
|
||||
connect = True
|
||||
await asyncio.sleep(retry_sleep)
|
||||
retry_sleep *= 2
|
||||
if retry_sleep > 60:
|
||||
retry_sleep = 60
|
||||
|
||||
async def _listen(self):
|
||||
channel = self.channel.encode('utf-8')
|
||||
await self.pubsub.subscribe(self.channel)
|
||||
async for message in self._redis_listen_with_retries():
|
||||
if message['channel'] == channel and \
|
||||
message['type'] == 'message' and 'data' in message:
|
||||
yield message['data']
|
||||
await self.pubsub.unsubscribe(self.channel)
|
||||
697
venv/lib/python3.12/site-packages/socketio/async_server.py
Normal file
697
venv/lib/python3.12/site-packages/socketio/async_server.py
Normal file
@ -0,0 +1,697 @@
|
||||
import asyncio
|
||||
|
||||
import engineio
|
||||
|
||||
from . import async_manager
|
||||
from . import base_server
|
||||
from . import exceptions
|
||||
from . import packet
|
||||
|
||||
# this set is used to keep references to background tasks to prevent them from
|
||||
# being garbage collected mid-execution. Solution taken from
|
||||
# https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task
|
||||
task_reference_holder = set()
|
||||
|
||||
|
||||
class AsyncServer(base_server.BaseServer):
|
||||
"""A Socket.IO server for asyncio.
|
||||
|
||||
This class implements a fully compliant Socket.IO web server with support
|
||||
for websocket and long-polling transports, compatible with the asyncio
|
||||
framework.
|
||||
|
||||
:param client_manager: The client manager instance that will manage the
|
||||
client list. When this is omitted, the client list
|
||||
is stored in an in-memory structure, so the use of
|
||||
multiple connected servers is not possible.
|
||||
:param logger: To enable logging set to ``True`` or pass a logger object to
|
||||
use. To disable logging set to ``False``. Note that fatal
|
||||
errors are logged even when ``logger`` is ``False``.
|
||||
:param json: An alternative json module to use for encoding and decoding
|
||||
packets. Custom json modules must have ``dumps`` and ``loads``
|
||||
functions that are compatible with the standard library
|
||||
versions.
|
||||
:param async_handlers: If set to ``True``, event handlers for a client are
|
||||
executed in separate threads. To run handlers for a
|
||||
client synchronously, set to ``False``. The default
|
||||
is ``True``.
|
||||
:param always_connect: When set to ``False``, new connections are
|
||||
provisory until the connect handler returns
|
||||
something other than ``False``, at which point they
|
||||
are accepted. When set to ``True``, connections are
|
||||
immediately accepted, and then if the connect
|
||||
handler returns ``False`` a disconnect is issued.
|
||||
Set to ``True`` if you need to emit events from the
|
||||
connect handler and your client is confused when it
|
||||
receives events before the connection acceptance.
|
||||
In any other case use the default of ``False``.
|
||||
:param namespaces: a list of namespaces that are accepted, in addition to
|
||||
any namespaces for which handlers have been defined. The
|
||||
default is `['/']`, which always accepts connections to
|
||||
the default namespace. Set to `'*'` to accept all
|
||||
namespaces.
|
||||
:param kwargs: Connection parameters for the underlying Engine.IO server.
|
||||
|
||||
The Engine.IO configuration supports the following settings:
|
||||
|
||||
:param async_mode: The asynchronous model to use. See the Deployment
|
||||
section in the documentation for a description of the
|
||||
available options. Valid async modes are "aiohttp",
|
||||
"sanic", "tornado" and "asgi". If this argument is not
|
||||
given, "aiohttp" is tried first, followed by "sanic",
|
||||
"tornado", and finally "asgi". The first async mode that
|
||||
has all its dependencies installed is the one that is
|
||||
chosen.
|
||||
:param ping_interval: The interval in seconds at which the server pings
|
||||
the client. The default is 25 seconds. For advanced
|
||||
control, a two element tuple can be given, where
|
||||
the first number is the ping interval and the second
|
||||
is a grace period added by the server.
|
||||
:param ping_timeout: The time in seconds that the client waits for the
|
||||
server to respond before disconnecting. The default
|
||||
is 20 seconds.
|
||||
:param max_http_buffer_size: The maximum size that is accepted for incoming
|
||||
messages. The default is 1,000,000 bytes. In
|
||||
spite of its name, the value set in this
|
||||
argument is enforced for HTTP long-polling and
|
||||
WebSocket connections.
|
||||
:param allow_upgrades: Whether to allow transport upgrades or not. The
|
||||
default is ``True``.
|
||||
:param http_compression: Whether to compress packages when using the
|
||||
polling transport. The default is ``True``.
|
||||
:param compression_threshold: Only compress messages when their byte size
|
||||
is greater than this value. The default is
|
||||
1024 bytes.
|
||||
:param cookie: If set to a string, it is the name of the HTTP cookie the
|
||||
server sends back to the client containing the client
|
||||
session id. If set to a dictionary, the ``'name'`` key
|
||||
contains the cookie name and other keys define cookie
|
||||
attributes, where the value of each attribute can be a
|
||||
string, a callable with no arguments, or a boolean. If set
|
||||
to ``None`` (the default), a cookie is not sent to the
|
||||
client.
|
||||
:param cors_allowed_origins: Origin or list of origins that are allowed to
|
||||
connect to this server. Only the same origin
|
||||
is allowed by default. Set this argument to
|
||||
``'*'`` to allow all origins, or to ``[]`` to
|
||||
disable CORS handling.
|
||||
:param cors_credentials: Whether credentials (cookies, authentication) are
|
||||
allowed in requests to this server. The default is
|
||||
``True``.
|
||||
:param monitor_clients: If set to ``True``, a background task will ensure
|
||||
inactive clients are closed. Set to ``False`` to
|
||||
disable the monitoring task (not recommended). The
|
||||
default is ``True``.
|
||||
:param transports: The list of allowed transports. Valid transports
|
||||
are ``'polling'`` and ``'websocket'``. Defaults to
|
||||
``['polling', 'websocket']``.
|
||||
:param engineio_logger: To enable Engine.IO logging set to ``True`` or pass
|
||||
a logger object to use. To disable logging set to
|
||||
``False``. The default is ``False``. Note that
|
||||
fatal errors are logged even when
|
||||
``engineio_logger`` is ``False``.
|
||||
"""
|
||||
def __init__(self, client_manager=None, logger=False, json=None,
|
||||
async_handlers=True, namespaces=None, **kwargs):
|
||||
if client_manager is None:
|
||||
client_manager = async_manager.AsyncManager()
|
||||
super().__init__(client_manager=client_manager, logger=logger,
|
||||
json=json, async_handlers=async_handlers,
|
||||
namespaces=namespaces, **kwargs)
|
||||
|
||||
def is_asyncio_based(self):
|
||||
return True
|
||||
|
||||
def attach(self, app, socketio_path='socket.io'):
|
||||
"""Attach the Socket.IO server to an application."""
|
||||
self.eio.attach(app, socketio_path)
|
||||
|
||||
async def emit(self, event, data=None, to=None, room=None, skip_sid=None,
|
||||
namespace=None, callback=None, ignore_queue=False):
|
||||
"""Emit a custom event to one or more connected clients.
|
||||
|
||||
:param event: The event name. It can be any string. The event names
|
||||
``'connect'``, ``'message'`` and ``'disconnect'`` are
|
||||
reserved and should not be used.
|
||||
:param data: The data to send to the client or clients. Data can be of
|
||||
type ``str``, ``bytes``, ``list`` or ``dict``. To send
|
||||
multiple arguments, use a tuple where each element is of
|
||||
one of the types indicated above.
|
||||
:param to: The recipient of the message. This can be set to the
|
||||
session ID of a client to address only that client, to any
|
||||
any custom room created by the application to address all
|
||||
the clients in that room, or to a list of custom room
|
||||
names. If this argument is omitted the event is broadcasted
|
||||
to all connected clients.
|
||||
:param room: Alias for the ``to`` parameter.
|
||||
:param skip_sid: The session ID of a client to skip when broadcasting
|
||||
to a room or to all clients. This can be used to
|
||||
prevent a message from being sent to the sender.
|
||||
:param namespace: The Socket.IO namespace for the event. If this
|
||||
argument is omitted the event is emitted to the
|
||||
default namespace.
|
||||
:param callback: If given, this function will be called to acknowledge
|
||||
the client has received the message. The arguments
|
||||
that will be passed to the function are those provided
|
||||
by the client. Callback functions can only be used
|
||||
when addressing an individual client.
|
||||
:param ignore_queue: Only used when a message queue is configured. If
|
||||
set to ``True``, the event is emitted to the
|
||||
clients directly, without going through the queue.
|
||||
This is more efficient, but only works when a
|
||||
single server process is used. It is recommended
|
||||
to always leave this parameter with its default
|
||||
value of ``False``.
|
||||
|
||||
Note: this method is not designed to be used concurrently. If multiple
|
||||
tasks are emitting at the same time to the same client connection, then
|
||||
messages composed of multiple packets may end up being sent in an
|
||||
incorrect sequence. Use standard concurrency solutions (such as a Lock
|
||||
object) to prevent this situation.
|
||||
|
||||
Note 2: this method is a coroutine.
|
||||
"""
|
||||
namespace = namespace or '/'
|
||||
room = to or room
|
||||
self.logger.info('emitting event "%s" to %s [%s]', event,
|
||||
room or 'all', namespace)
|
||||
await self.manager.emit(event, data, namespace, room=room,
|
||||
skip_sid=skip_sid, callback=callback,
|
||||
ignore_queue=ignore_queue)
|
||||
|
||||
async def send(self, data, to=None, room=None, skip_sid=None,
|
||||
namespace=None, callback=None, ignore_queue=False):
|
||||
"""Send a message to one or more connected clients.
|
||||
|
||||
This function emits an event with the name ``'message'``. Use
|
||||
:func:`emit` to issue custom event names.
|
||||
|
||||
:param data: The data to send to the client or clients. Data can be of
|
||||
type ``str``, ``bytes``, ``list`` or ``dict``. To send
|
||||
multiple arguments, use a tuple where each element is of
|
||||
one of the types indicated above.
|
||||
:param to: The recipient of the message. This can be set to the
|
||||
session ID of a client to address only that client, to any
|
||||
any custom room created by the application to address all
|
||||
the clients in that room, or to a list of custom room
|
||||
names. If this argument is omitted the event is broadcasted
|
||||
to all connected clients.
|
||||
:param room: Alias for the ``to`` parameter.
|
||||
:param skip_sid: The session ID of a client to skip when broadcasting
|
||||
to a room or to all clients. This can be used to
|
||||
prevent a message from being sent to the sender.
|
||||
:param namespace: The Socket.IO namespace for the event. If this
|
||||
argument is omitted the event is emitted to the
|
||||
default namespace.
|
||||
:param callback: If given, this function will be called to acknowledge
|
||||
the client has received the message. The arguments
|
||||
that will be passed to the function are those provided
|
||||
by the client. Callback functions can only be used
|
||||
when addressing an individual client.
|
||||
:param ignore_queue: Only used when a message queue is configured. If
|
||||
set to ``True``, the event is emitted to the
|
||||
clients directly, without going through the queue.
|
||||
This is more efficient, but only works when a
|
||||
single server process is used. It is recommended
|
||||
to always leave this parameter with its default
|
||||
value of ``False``.
|
||||
|
||||
Note: this method is a coroutine.
|
||||
"""
|
||||
await self.emit('message', data=data, to=to, room=room,
|
||||
skip_sid=skip_sid, namespace=namespace,
|
||||
callback=callback, ignore_queue=ignore_queue)
|
||||
|
||||
async def call(self, event, data=None, to=None, sid=None, namespace=None,
|
||||
timeout=60, ignore_queue=False):
|
||||
"""Emit a custom event to a client and wait for the response.
|
||||
|
||||
This method issues an emit with a callback and waits for the callback
|
||||
to be invoked before returning. If the callback isn't invoked before
|
||||
the timeout, then a ``TimeoutError`` exception is raised. If the
|
||||
Socket.IO connection drops during the wait, this method still waits
|
||||
until the specified timeout.
|
||||
|
||||
:param event: The event name. It can be any string. The event names
|
||||
``'connect'``, ``'message'`` and ``'disconnect'`` are
|
||||
reserved and should not be used.
|
||||
:param data: The data to send to the client or clients. Data can be of
|
||||
type ``str``, ``bytes``, ``list`` or ``dict``. To send
|
||||
multiple arguments, use a tuple where each element is of
|
||||
one of the types indicated above.
|
||||
:param to: The session ID of the recipient client.
|
||||
:param sid: Alias for the ``to`` parameter.
|
||||
:param namespace: The Socket.IO namespace for the event. If this
|
||||
argument is omitted the event is emitted to the
|
||||
default namespace.
|
||||
:param timeout: The waiting timeout. If the timeout is reached before
|
||||
the client acknowledges the event, then a
|
||||
``TimeoutError`` exception is raised.
|
||||
:param ignore_queue: Only used when a message queue is configured. If
|
||||
set to ``True``, the event is emitted to the
|
||||
client directly, without going through the queue.
|
||||
This is more efficient, but only works when a
|
||||
single server process is used. It is recommended
|
||||
to always leave this parameter with its default
|
||||
value of ``False``.
|
||||
|
||||
Note: this method is not designed to be used concurrently. If multiple
|
||||
tasks are emitting at the same time to the same client connection, then
|
||||
messages composed of multiple packets may end up being sent in an
|
||||
incorrect sequence. Use standard concurrency solutions (such as a Lock
|
||||
object) to prevent this situation.
|
||||
|
||||
Note 2: this method is a coroutine.
|
||||
"""
|
||||
if to is None and sid is None:
|
||||
raise ValueError('Cannot use call() to broadcast.')
|
||||
if not self.async_handlers:
|
||||
raise RuntimeError(
|
||||
'Cannot use call() when async_handlers is False.')
|
||||
callback_event = self.eio.create_event()
|
||||
callback_args = []
|
||||
|
||||
def event_callback(*args):
|
||||
callback_args.append(args)
|
||||
callback_event.set()
|
||||
|
||||
await self.emit(event, data=data, room=to or sid, namespace=namespace,
|
||||
callback=event_callback, ignore_queue=ignore_queue)
|
||||
try:
|
||||
await asyncio.wait_for(callback_event.wait(), timeout)
|
||||
except asyncio.TimeoutError:
|
||||
raise exceptions.TimeoutError() from None
|
||||
return callback_args[0] if len(callback_args[0]) > 1 \
|
||||
else callback_args[0][0] if len(callback_args[0]) == 1 \
|
||||
else None
|
||||
|
||||
async def enter_room(self, sid, room, namespace=None):
|
||||
"""Enter a room.
|
||||
|
||||
This function adds the client to a room. The :func:`emit` and
|
||||
:func:`send` functions can optionally broadcast events to all the
|
||||
clients in a room.
|
||||
|
||||
:param sid: Session ID of the client.
|
||||
:param room: Room name. If the room does not exist it is created.
|
||||
:param namespace: The Socket.IO namespace for the event. If this
|
||||
argument is omitted the default namespace is used.
|
||||
|
||||
Note: this method is a coroutine.
|
||||
"""
|
||||
namespace = namespace or '/'
|
||||
self.logger.info('%s is entering room %s [%s]', sid, room, namespace)
|
||||
await self.manager.enter_room(sid, namespace, room)
|
||||
|
||||
async def leave_room(self, sid, room, namespace=None):
|
||||
"""Leave a room.
|
||||
|
||||
This function removes the client from a room.
|
||||
|
||||
:param sid: Session ID of the client.
|
||||
:param room: Room name.
|
||||
:param namespace: The Socket.IO namespace for the event. If this
|
||||
argument is omitted the default namespace is used.
|
||||
|
||||
Note: this method is a coroutine.
|
||||
"""
|
||||
namespace = namespace or '/'
|
||||
self.logger.info('%s is leaving room %s [%s]', sid, room, namespace)
|
||||
await self.manager.leave_room(sid, namespace, room)
|
||||
|
||||
async def close_room(self, room, namespace=None):
|
||||
"""Close a room.
|
||||
|
||||
This function removes all the clients from the given room.
|
||||
|
||||
:param room: Room name.
|
||||
:param namespace: The Socket.IO namespace for the event. If this
|
||||
argument is omitted the default namespace is used.
|
||||
|
||||
Note: this method is a coroutine.
|
||||
"""
|
||||
namespace = namespace or '/'
|
||||
self.logger.info('room %s is closing [%s]', room, namespace)
|
||||
await self.manager.close_room(room, namespace)
|
||||
|
||||
async def get_session(self, sid, namespace=None):
|
||||
"""Return the user session for a client.
|
||||
|
||||
:param sid: The session id of the client.
|
||||
:param namespace: The Socket.IO namespace. If this argument is omitted
|
||||
the default namespace is used.
|
||||
|
||||
The return value is a dictionary. Modifications made to this
|
||||
dictionary are not guaranteed to be preserved. If you want to modify
|
||||
the user session, use the ``session`` context manager instead.
|
||||
"""
|
||||
namespace = namespace or '/'
|
||||
eio_sid = self.manager.eio_sid_from_sid(sid, namespace)
|
||||
eio_session = await self.eio.get_session(eio_sid)
|
||||
return eio_session.setdefault(namespace, {})
|
||||
|
||||
async def save_session(self, sid, session, namespace=None):
|
||||
"""Store the user session for a client.
|
||||
|
||||
:param sid: The session id of the client.
|
||||
:param session: The session dictionary.
|
||||
:param namespace: The Socket.IO namespace. If this argument is omitted
|
||||
the default namespace is used.
|
||||
"""
|
||||
namespace = namespace or '/'
|
||||
eio_sid = self.manager.eio_sid_from_sid(sid, namespace)
|
||||
eio_session = await self.eio.get_session(eio_sid)
|
||||
eio_session[namespace] = session
|
||||
|
||||
def session(self, sid, namespace=None):
|
||||
"""Return the user session for a client with context manager syntax.
|
||||
|
||||
:param sid: The session id of the client.
|
||||
|
||||
This is a context manager that returns the user session dictionary for
|
||||
the client. Any changes that are made to this dictionary inside the
|
||||
context manager block are saved back to the session. Example usage::
|
||||
|
||||
@eio.on('connect')
|
||||
def on_connect(sid, environ):
|
||||
username = authenticate_user(environ)
|
||||
if not username:
|
||||
return False
|
||||
with eio.session(sid) as session:
|
||||
session['username'] = username
|
||||
|
||||
@eio.on('message')
|
||||
def on_message(sid, msg):
|
||||
async with eio.session(sid) as session:
|
||||
print('received message from ', session['username'])
|
||||
"""
|
||||
class _session_context_manager(object):
|
||||
def __init__(self, server, sid, namespace):
|
||||
self.server = server
|
||||
self.sid = sid
|
||||
self.namespace = namespace
|
||||
self.session = None
|
||||
|
||||
async def __aenter__(self):
|
||||
self.session = await self.server.get_session(
|
||||
sid, namespace=self.namespace)
|
||||
return self.session
|
||||
|
||||
async def __aexit__(self, *args):
|
||||
await self.server.save_session(sid, self.session,
|
||||
namespace=self.namespace)
|
||||
|
||||
return _session_context_manager(self, sid, namespace)
|
||||
|
||||
async def disconnect(self, sid, namespace=None, ignore_queue=False):
|
||||
"""Disconnect a client.
|
||||
|
||||
:param sid: Session ID of the client.
|
||||
:param namespace: The Socket.IO namespace to disconnect. If this
|
||||
argument is omitted the default namespace is used.
|
||||
:param ignore_queue: Only used when a message queue is configured. If
|
||||
set to ``True``, the disconnect is processed
|
||||
locally, without broadcasting on the queue. It is
|
||||
recommended to always leave this parameter with
|
||||
its default value of ``False``.
|
||||
|
||||
Note: this method is a coroutine.
|
||||
"""
|
||||
namespace = namespace or '/'
|
||||
if ignore_queue:
|
||||
delete_it = self.manager.is_connected(sid, namespace)
|
||||
else:
|
||||
delete_it = await self.manager.can_disconnect(sid, namespace)
|
||||
if delete_it:
|
||||
self.logger.info('Disconnecting %s [%s]', sid, namespace)
|
||||
eio_sid = self.manager.pre_disconnect(sid, namespace=namespace)
|
||||
await self._send_packet(eio_sid, self.packet_class(
|
||||
packet.DISCONNECT, namespace=namespace))
|
||||
await self._trigger_event('disconnect', namespace, sid)
|
||||
await self.manager.disconnect(sid, namespace=namespace,
|
||||
ignore_queue=True)
|
||||
|
||||
async def shutdown(self):
|
||||
"""Stop Socket.IO background tasks.
|
||||
|
||||
This method stops all background activity initiated by the Socket.IO
|
||||
server. It must be called before shutting down the web server.
|
||||
"""
|
||||
self.logger.info('Socket.IO is shutting down')
|
||||
await self.eio.shutdown()
|
||||
|
||||
async def handle_request(self, *args, **kwargs):
|
||||
"""Handle an HTTP request from the client.
|
||||
|
||||
This is the entry point of the Socket.IO application. This function
|
||||
returns the HTTP response body to deliver to the client.
|
||||
|
||||
Note: this method is a coroutine.
|
||||
"""
|
||||
return await self.eio.handle_request(*args, **kwargs)
|
||||
|
||||
def start_background_task(self, target, *args, **kwargs):
|
||||
"""Start a background task using the appropriate async model.
|
||||
|
||||
This is a utility function that applications can use to start a
|
||||
background task using the method that is compatible with the
|
||||
selected async mode.
|
||||
|
||||
:param target: the target function to execute. Must be a coroutine.
|
||||
:param args: arguments to pass to the function.
|
||||
:param kwargs: keyword arguments to pass to the function.
|
||||
|
||||
The return value is a ``asyncio.Task`` object.
|
||||
"""
|
||||
return self.eio.start_background_task(target, *args, **kwargs)
|
||||
|
||||
async def sleep(self, seconds=0):
|
||||
"""Sleep for the requested amount of time using the appropriate async
|
||||
model.
|
||||
|
||||
This is a utility function that applications can use to put a task to
|
||||
sleep without having to worry about using the correct call for the
|
||||
selected async mode.
|
||||
|
||||
Note: this method is a coroutine.
|
||||
"""
|
||||
return await self.eio.sleep(seconds)
|
||||
|
||||
def instrument(self, auth=None, mode='development', read_only=False,
|
||||
server_id=None, namespace='/admin',
|
||||
server_stats_interval=2):
|
||||
"""Instrument the Socket.IO server for monitoring with the `Socket.IO
|
||||
Admin UI <https://socket.io/docs/v4/admin-ui/>`_.
|
||||
|
||||
:param auth: Authentication credentials for Admin UI access. Set to a
|
||||
dictionary with the expected login (usually ``username``
|
||||
and ``password``) or a list of dictionaries if more than
|
||||
one set of credentials need to be available. For more
|
||||
complex authentication methods, set to a callable that
|
||||
receives the authentication dictionary as an argument and
|
||||
returns ``True`` if the user is allowed or ``False``
|
||||
otherwise. To disable authentication, set this argument to
|
||||
``False`` (not recommended, never do this on a production
|
||||
server).
|
||||
:param mode: The reporting mode. The default is ``'development'``,
|
||||
which is best used while debugging, as it may have a
|
||||
significant performance effect. Set to ``'production'`` to
|
||||
reduce the amount of information that is reported to the
|
||||
admin UI.
|
||||
:param read_only: If set to ``True``, the admin interface will be
|
||||
read-only, with no option to modify room assignments
|
||||
or disconnect clients. The default is ``False``.
|
||||
:param server_id: The server name to use for this server. If this
|
||||
argument is omitted, the server generates its own
|
||||
name.
|
||||
:param namespace: The Socket.IO namespace to use for the admin
|
||||
interface. The default is ``/admin``.
|
||||
:param server_stats_interval: The interval in seconds at which the
|
||||
server emits a summary of it stats to all
|
||||
connected admins.
|
||||
"""
|
||||
from .async_admin import InstrumentedAsyncServer
|
||||
return InstrumentedAsyncServer(
|
||||
self, auth=auth, mode=mode, read_only=read_only,
|
||||
server_id=server_id, namespace=namespace,
|
||||
server_stats_interval=server_stats_interval)
|
||||
|
||||
async def _send_packet(self, eio_sid, pkt):
|
||||
"""Send a Socket.IO packet to a client."""
|
||||
encoded_packet = pkt.encode()
|
||||
if isinstance(encoded_packet, list):
|
||||
for ep in encoded_packet:
|
||||
await self.eio.send(eio_sid, ep)
|
||||
else:
|
||||
await self.eio.send(eio_sid, encoded_packet)
|
||||
|
||||
async def _send_eio_packet(self, eio_sid, eio_pkt):
|
||||
"""Send a raw Engine.IO packet to a client."""
|
||||
await self.eio.send_packet(eio_sid, eio_pkt)
|
||||
|
||||
async def _handle_connect(self, eio_sid, namespace, data):
|
||||
"""Handle a client connection request."""
|
||||
namespace = namespace or '/'
|
||||
sid = None
|
||||
if namespace in self.handlers or namespace in self.namespace_handlers \
|
||||
or self.namespaces == '*' or namespace in self.namespaces:
|
||||
sid = await self.manager.connect(eio_sid, namespace)
|
||||
if sid is None:
|
||||
await self._send_packet(eio_sid, self.packet_class(
|
||||
packet.CONNECT_ERROR, data='Unable to connect',
|
||||
namespace=namespace))
|
||||
return
|
||||
|
||||
if self.always_connect:
|
||||
await self._send_packet(eio_sid, self.packet_class(
|
||||
packet.CONNECT, {'sid': sid}, namespace=namespace))
|
||||
fail_reason = exceptions.ConnectionRefusedError().error_args
|
||||
try:
|
||||
if data:
|
||||
success = await self._trigger_event(
|
||||
'connect', namespace, sid, self.environ[eio_sid], data)
|
||||
else:
|
||||
try:
|
||||
success = await self._trigger_event(
|
||||
'connect', namespace, sid, self.environ[eio_sid])
|
||||
except TypeError:
|
||||
success = await self._trigger_event(
|
||||
'connect', namespace, sid, self.environ[eio_sid], None)
|
||||
except exceptions.ConnectionRefusedError as exc:
|
||||
fail_reason = exc.error_args
|
||||
success = False
|
||||
|
||||
if success is False:
|
||||
if self.always_connect:
|
||||
self.manager.pre_disconnect(sid, namespace)
|
||||
await self._send_packet(eio_sid, self.packet_class(
|
||||
packet.DISCONNECT, data=fail_reason, namespace=namespace))
|
||||
else:
|
||||
await self._send_packet(eio_sid, self.packet_class(
|
||||
packet.CONNECT_ERROR, data=fail_reason,
|
||||
namespace=namespace))
|
||||
await self.manager.disconnect(sid, namespace, ignore_queue=True)
|
||||
elif not self.always_connect:
|
||||
await self._send_packet(eio_sid, self.packet_class(
|
||||
packet.CONNECT, {'sid': sid}, namespace=namespace))
|
||||
|
||||
async def _handle_disconnect(self, eio_sid, namespace):
|
||||
"""Handle a client disconnect."""
|
||||
namespace = namespace or '/'
|
||||
sid = self.manager.sid_from_eio_sid(eio_sid, namespace)
|
||||
if not self.manager.is_connected(sid, namespace): # pragma: no cover
|
||||
return
|
||||
self.manager.pre_disconnect(sid, namespace=namespace)
|
||||
await self._trigger_event('disconnect', namespace, sid)
|
||||
await self.manager.disconnect(sid, namespace, ignore_queue=True)
|
||||
|
||||
async def _handle_event(self, eio_sid, namespace, id, data):
|
||||
"""Handle an incoming client event."""
|
||||
namespace = namespace or '/'
|
||||
sid = self.manager.sid_from_eio_sid(eio_sid, namespace)
|
||||
self.logger.info('received event "%s" from %s [%s]', data[0], sid,
|
||||
namespace)
|
||||
if not self.manager.is_connected(sid, namespace):
|
||||
self.logger.warning('%s is not connected to namespace %s',
|
||||
sid, namespace)
|
||||
return
|
||||
if self.async_handlers:
|
||||
task = self.start_background_task(
|
||||
self._handle_event_internal, self, sid, eio_sid, data,
|
||||
namespace, id)
|
||||
task_reference_holder.add(task)
|
||||
task.add_done_callback(task_reference_holder.discard)
|
||||
else:
|
||||
await self._handle_event_internal(self, sid, eio_sid, data,
|
||||
namespace, id)
|
||||
|
||||
async def _handle_event_internal(self, server, sid, eio_sid, data,
|
||||
namespace, id):
|
||||
r = await server._trigger_event(data[0], namespace, sid, *data[1:])
|
||||
if r != self.not_handled and id is not None:
|
||||
# send ACK packet with the response returned by the handler
|
||||
# tuples are expanded as multiple arguments
|
||||
if r is None:
|
||||
data = []
|
||||
elif isinstance(r, tuple):
|
||||
data = list(r)
|
||||
else:
|
||||
data = [r]
|
||||
await server._send_packet(eio_sid, self.packet_class(
|
||||
packet.ACK, namespace=namespace, id=id, data=data))
|
||||
|
||||
async def _handle_ack(self, eio_sid, namespace, id, data):
|
||||
"""Handle ACK packets from the client."""
|
||||
namespace = namespace or '/'
|
||||
sid = self.manager.sid_from_eio_sid(eio_sid, namespace)
|
||||
self.logger.info('received ack from %s [%s]', sid, namespace)
|
||||
await self.manager.trigger_callback(sid, id, data)
|
||||
|
||||
async def _trigger_event(self, event, namespace, *args):
|
||||
"""Invoke an application event handler."""
|
||||
# first see if we have an explicit handler for the event
|
||||
handler, args = self._get_event_handler(event, namespace, args)
|
||||
if handler:
|
||||
if asyncio.iscoroutinefunction(handler):
|
||||
try:
|
||||
ret = await handler(*args)
|
||||
except asyncio.CancelledError: # pragma: no cover
|
||||
ret = None
|
||||
else:
|
||||
ret = handler(*args)
|
||||
return ret
|
||||
# or else, forward the event to a namespace handler if one exists
|
||||
handler, args = self._get_namespace_handler(namespace, args)
|
||||
if handler:
|
||||
return await handler.trigger_event(event, *args)
|
||||
else:
|
||||
return self.not_handled
|
||||
|
||||
async def _handle_eio_connect(self, eio_sid, environ):
|
||||
"""Handle the Engine.IO connection event."""
|
||||
if not self.manager_initialized:
|
||||
self.manager_initialized = True
|
||||
self.manager.initialize()
|
||||
self.environ[eio_sid] = environ
|
||||
|
||||
async def _handle_eio_message(self, eio_sid, data):
|
||||
"""Dispatch Engine.IO messages."""
|
||||
if eio_sid in self._binary_packet:
|
||||
pkt = self._binary_packet[eio_sid]
|
||||
if pkt.add_attachment(data):
|
||||
del self._binary_packet[eio_sid]
|
||||
if pkt.packet_type == packet.BINARY_EVENT:
|
||||
await self._handle_event(eio_sid, pkt.namespace, pkt.id,
|
||||
pkt.data)
|
||||
else:
|
||||
await self._handle_ack(eio_sid, pkt.namespace, pkt.id,
|
||||
pkt.data)
|
||||
else:
|
||||
pkt = self.packet_class(encoded_packet=data)
|
||||
if pkt.packet_type == packet.CONNECT:
|
||||
await self._handle_connect(eio_sid, pkt.namespace, pkt.data)
|
||||
elif pkt.packet_type == packet.DISCONNECT:
|
||||
await self._handle_disconnect(eio_sid, pkt.namespace)
|
||||
elif pkt.packet_type == packet.EVENT:
|
||||
await self._handle_event(eio_sid, pkt.namespace, pkt.id,
|
||||
pkt.data)
|
||||
elif pkt.packet_type == packet.ACK:
|
||||
await self._handle_ack(eio_sid, pkt.namespace, pkt.id,
|
||||
pkt.data)
|
||||
elif pkt.packet_type == packet.BINARY_EVENT or \
|
||||
pkt.packet_type == packet.BINARY_ACK:
|
||||
self._binary_packet[eio_sid] = pkt
|
||||
elif pkt.packet_type == packet.CONNECT_ERROR:
|
||||
raise ValueError('Unexpected CONNECT_ERROR packet.')
|
||||
else:
|
||||
raise ValueError('Unknown packet type.')
|
||||
|
||||
async def _handle_eio_disconnect(self, eio_sid):
|
||||
"""Handle Engine.IO disconnect event."""
|
||||
for n in list(self.manager.get_namespaces()).copy():
|
||||
await self._handle_disconnect(eio_sid, n)
|
||||
if eio_sid in self.environ:
|
||||
del self.environ[eio_sid]
|
||||
|
||||
def _engineio_server_class(self):
|
||||
return engineio.AsyncServer
|
||||
@ -0,0 +1,209 @@
|
||||
import asyncio
|
||||
from socketio import AsyncClient
|
||||
from socketio.exceptions import SocketIOError, TimeoutError, DisconnectedError
|
||||
|
||||
|
||||
class AsyncSimpleClient:
|
||||
"""A Socket.IO client.
|
||||
|
||||
This class implements a simple, yet fully compliant Socket.IO web client
|
||||
with support for websocket and long-polling transports.
|
||||
|
||||
The positional and keyword arguments given in the constructor are passed
|
||||
to the underlying :func:`socketio.AsyncClient` object.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.client_args = args
|
||||
self.client_kwargs = kwargs
|
||||
self.client = None
|
||||
self.namespace = '/'
|
||||
self.connected_event = asyncio.Event()
|
||||
self.connected = False
|
||||
self.input_event = asyncio.Event()
|
||||
self.input_buffer = []
|
||||
|
||||
async def connect(self, url, headers={}, auth=None, transports=None,
|
||||
namespace='/', socketio_path='socket.io',
|
||||
wait_timeout=5):
|
||||
"""Connect to a Socket.IO server.
|
||||
|
||||
:param url: The URL of the Socket.IO server. It can include custom
|
||||
query string parameters if required by the server. If a
|
||||
function is provided, the client will invoke it to obtain
|
||||
the URL each time a connection or reconnection is
|
||||
attempted.
|
||||
:param headers: A dictionary with custom headers to send with the
|
||||
connection request. If a function is provided, the
|
||||
client will invoke it to obtain the headers dictionary
|
||||
each time a connection or reconnection is attempted.
|
||||
:param auth: Authentication data passed to the server with the
|
||||
connection request, normally a dictionary with one or
|
||||
more string key/value pairs. If a function is provided,
|
||||
the client will invoke it to obtain the authentication
|
||||
data each time a connection or reconnection is attempted.
|
||||
:param transports: The list of allowed transports. Valid transports
|
||||
are ``'polling'`` and ``'websocket'``. If not
|
||||
given, the polling transport is connected first,
|
||||
then an upgrade to websocket is attempted.
|
||||
:param namespace: The namespace to connect to as a string. If not
|
||||
given, the default namespace ``/`` is used.
|
||||
:param socketio_path: The endpoint where the Socket.IO server is
|
||||
installed. The default value is appropriate for
|
||||
most cases.
|
||||
:param wait_timeout: How long the client should wait for the
|
||||
connection. The default is 5 seconds.
|
||||
|
||||
Note: this method is a coroutine.
|
||||
"""
|
||||
if self.connected:
|
||||
raise RuntimeError('Already connected')
|
||||
self.namespace = namespace
|
||||
self.input_buffer = []
|
||||
self.input_event.clear()
|
||||
self.client = AsyncClient(*self.client_args, **self.client_kwargs)
|
||||
|
||||
@self.client.event(namespace=self.namespace)
|
||||
def connect(): # pragma: no cover
|
||||
self.connected = True
|
||||
self.connected_event.set()
|
||||
|
||||
@self.client.event(namespace=self.namespace)
|
||||
def disconnect(): # pragma: no cover
|
||||
self.connected_event.clear()
|
||||
|
||||
@self.client.event(namespace=self.namespace)
|
||||
def __disconnect_final(): # pragma: no cover
|
||||
self.connected = False
|
||||
self.connected_event.set()
|
||||
|
||||
@self.client.on('*', namespace=self.namespace)
|
||||
def on_event(event, *args): # pragma: no cover
|
||||
self.input_buffer.append([event, *args])
|
||||
self.input_event.set()
|
||||
|
||||
await self.client.connect(
|
||||
url, headers=headers, auth=auth, transports=transports,
|
||||
namespaces=[namespace], socketio_path=socketio_path,
|
||||
wait_timeout=wait_timeout)
|
||||
|
||||
@property
|
||||
def sid(self):
|
||||
"""The session ID received from the server.
|
||||
|
||||
The session ID is not guaranteed to remain constant throughout the life
|
||||
of the connection, as reconnections can cause it to change.
|
||||
"""
|
||||
return self.client.get_sid(self.namespace) if self.client else None
|
||||
|
||||
@property
|
||||
def transport(self):
|
||||
"""The name of the transport currently in use.
|
||||
|
||||
The transport is returned as a string and can be one of ``polling``
|
||||
and ``websocket``.
|
||||
"""
|
||||
return self.client.transport if self.client else ''
|
||||
|
||||
async def emit(self, event, data=None):
|
||||
"""Emit an event to the server.
|
||||
|
||||
:param event: The event name. It can be any string. The event names
|
||||
``'connect'``, ``'message'`` and ``'disconnect'`` are
|
||||
reserved and should not be used.
|
||||
:param data: The data to send to the server. Data can be of
|
||||
type ``str``, ``bytes``, ``list`` or ``dict``. To send
|
||||
multiple arguments, use a tuple where each element is of
|
||||
one of the types indicated above.
|
||||
|
||||
Note: this method is a coroutine.
|
||||
|
||||
This method schedules the event to be sent out and returns, without
|
||||
actually waiting for its delivery. In cases where the client needs to
|
||||
ensure that the event was received, :func:`socketio.SimpleClient.call`
|
||||
should be used instead.
|
||||
"""
|
||||
while True:
|
||||
await self.connected_event.wait()
|
||||
if not self.connected:
|
||||
raise DisconnectedError()
|
||||
try:
|
||||
return await self.client.emit(event, data,
|
||||
namespace=self.namespace)
|
||||
except SocketIOError:
|
||||
pass
|
||||
|
||||
async def call(self, event, data=None, timeout=60):
|
||||
"""Emit an event to the server and wait for a response.
|
||||
|
||||
This method issues an emit and waits for the server to provide a
|
||||
response or acknowledgement. If the response does not arrive before the
|
||||
timeout, then a ``TimeoutError`` exception is raised.
|
||||
|
||||
:param event: The event name. It can be any string. The event names
|
||||
``'connect'``, ``'message'`` and ``'disconnect'`` are
|
||||
reserved and should not be used.
|
||||
:param data: The data to send to the server. Data can be of
|
||||
type ``str``, ``bytes``, ``list`` or ``dict``. To send
|
||||
multiple arguments, use a tuple where each element is of
|
||||
one of the types indicated above.
|
||||
:param timeout: The waiting timeout. If the timeout is reached before
|
||||
the server acknowledges the event, then a
|
||||
``TimeoutError`` exception is raised.
|
||||
|
||||
Note: this method is a coroutine.
|
||||
"""
|
||||
while True:
|
||||
await self.connected_event.wait()
|
||||
if not self.connected:
|
||||
raise DisconnectedError()
|
||||
try:
|
||||
return await self.client.call(event, data,
|
||||
namespace=self.namespace,
|
||||
timeout=timeout)
|
||||
except SocketIOError:
|
||||
pass
|
||||
|
||||
async def receive(self, timeout=None):
|
||||
"""Wait for an event from the server.
|
||||
|
||||
:param timeout: The waiting timeout. If the timeout is reached before
|
||||
the server acknowledges the event, then a
|
||||
``TimeoutError`` exception is raised.
|
||||
|
||||
Note: this method is a coroutine.
|
||||
|
||||
The return value is a list with the event name as the first element. If
|
||||
the server included arguments with the event, they are returned as
|
||||
additional list elements.
|
||||
"""
|
||||
while not self.input_buffer:
|
||||
try:
|
||||
await asyncio.wait_for(self.connected_event.wait(),
|
||||
timeout=timeout)
|
||||
except asyncio.TimeoutError: # pragma: no cover
|
||||
raise TimeoutError()
|
||||
if not self.connected:
|
||||
raise DisconnectedError()
|
||||
try:
|
||||
await asyncio.wait_for(self.input_event.wait(),
|
||||
timeout=timeout)
|
||||
except asyncio.TimeoutError:
|
||||
raise TimeoutError()
|
||||
self.input_event.clear()
|
||||
return self.input_buffer.pop(0)
|
||||
|
||||
async def disconnect(self):
|
||||
"""Disconnect from the server.
|
||||
|
||||
Note: this method is a coroutine.
|
||||
"""
|
||||
if self.connected:
|
||||
await self.client.disconnect()
|
||||
self.client = None
|
||||
self.connected = False
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
await self.disconnect()
|
||||
292
venv/lib/python3.12/site-packages/socketio/base_client.py
Normal file
292
venv/lib/python3.12/site-packages/socketio/base_client.py
Normal file
@ -0,0 +1,292 @@
|
||||
import itertools
|
||||
import logging
|
||||
import signal
|
||||
import threading
|
||||
|
||||
from . import base_namespace
|
||||
from . import packet
|
||||
|
||||
default_logger = logging.getLogger('socketio.client')
|
||||
reconnecting_clients = []
|
||||
|
||||
|
||||
def signal_handler(sig, frame): # pragma: no cover
|
||||
"""SIGINT handler.
|
||||
|
||||
Notify any clients that are in a reconnect loop to abort. Other
|
||||
disconnection tasks are handled at the engine.io level.
|
||||
"""
|
||||
for client in reconnecting_clients[:]:
|
||||
client._reconnect_abort.set()
|
||||
if callable(original_signal_handler):
|
||||
return original_signal_handler(sig, frame)
|
||||
else: # pragma: no cover
|
||||
# Handle case where no original SIGINT handler was present.
|
||||
return signal.default_int_handler(sig, frame)
|
||||
|
||||
|
||||
original_signal_handler = None
|
||||
|
||||
|
||||
class BaseClient:
|
||||
reserved_events = ['connect', 'connect_error', 'disconnect',
|
||||
'__disconnect_final']
|
||||
|
||||
def __init__(self, reconnection=True, reconnection_attempts=0,
|
||||
reconnection_delay=1, reconnection_delay_max=5,
|
||||
randomization_factor=0.5, logger=False, serializer='default',
|
||||
json=None, handle_sigint=True, **kwargs):
|
||||
global original_signal_handler
|
||||
if handle_sigint and original_signal_handler is None and \
|
||||
threading.current_thread() == threading.main_thread():
|
||||
original_signal_handler = signal.signal(signal.SIGINT,
|
||||
signal_handler)
|
||||
self.reconnection = reconnection
|
||||
self.reconnection_attempts = reconnection_attempts
|
||||
self.reconnection_delay = reconnection_delay
|
||||
self.reconnection_delay_max = reconnection_delay_max
|
||||
self.randomization_factor = randomization_factor
|
||||
self.handle_sigint = handle_sigint
|
||||
|
||||
engineio_options = kwargs
|
||||
engineio_options['handle_sigint'] = handle_sigint
|
||||
engineio_logger = engineio_options.pop('engineio_logger', None)
|
||||
if engineio_logger is not None:
|
||||
engineio_options['logger'] = engineio_logger
|
||||
if serializer == 'default':
|
||||
self.packet_class = packet.Packet
|
||||
elif serializer == 'msgpack':
|
||||
from . import msgpack_packet
|
||||
self.packet_class = msgpack_packet.MsgPackPacket
|
||||
else:
|
||||
self.packet_class = serializer
|
||||
if json is not None:
|
||||
self.packet_class.json = json
|
||||
engineio_options['json'] = json
|
||||
|
||||
self.eio = self._engineio_client_class()(**engineio_options)
|
||||
self.eio.on('connect', self._handle_eio_connect)
|
||||
self.eio.on('message', self._handle_eio_message)
|
||||
self.eio.on('disconnect', self._handle_eio_disconnect)
|
||||
|
||||
if not isinstance(logger, bool):
|
||||
self.logger = logger
|
||||
else:
|
||||
self.logger = default_logger
|
||||
if self.logger.level == logging.NOTSET:
|
||||
if logger:
|
||||
self.logger.setLevel(logging.INFO)
|
||||
else:
|
||||
self.logger.setLevel(logging.ERROR)
|
||||
self.logger.addHandler(logging.StreamHandler())
|
||||
|
||||
self.connection_url = None
|
||||
self.connection_headers = None
|
||||
self.connection_auth = None
|
||||
self.connection_transports = None
|
||||
self.connection_namespaces = []
|
||||
self.socketio_path = None
|
||||
self.sid = None
|
||||
|
||||
self.connected = False #: Indicates if the client is connected or not.
|
||||
self.namespaces = {} #: set of connected namespaces.
|
||||
self.handlers = {}
|
||||
self.namespace_handlers = {}
|
||||
self.callbacks = {}
|
||||
self._binary_packet = None
|
||||
self._connect_event = None
|
||||
self._reconnect_task = None
|
||||
self._reconnect_abort = None
|
||||
|
||||
def is_asyncio_based(self):
|
||||
return False
|
||||
|
||||
def on(self, event, handler=None, namespace=None):
|
||||
"""Register an event handler.
|
||||
|
||||
:param event: The event name. It can be any string. The event names
|
||||
``'connect'``, ``'message'`` and ``'disconnect'`` are
|
||||
reserved and should not be used. The ``'*'`` event name
|
||||
can be used to define a catch-all event handler.
|
||||
:param handler: The function that should be invoked to handle the
|
||||
event. When this parameter is not given, the method
|
||||
acts as a decorator for the handler function.
|
||||
:param namespace: The Socket.IO namespace for the event. If this
|
||||
argument is omitted the handler is associated with
|
||||
the default namespace. A catch-all namespace can be
|
||||
defined by passing ``'*'`` as the namespace.
|
||||
|
||||
Example usage::
|
||||
|
||||
# as a decorator:
|
||||
@sio.on('connect')
|
||||
def connect_handler():
|
||||
print('Connected!')
|
||||
|
||||
# as a method:
|
||||
def message_handler(msg):
|
||||
print('Received message: ', msg)
|
||||
sio.send( 'response')
|
||||
sio.on('message', message_handler)
|
||||
|
||||
The arguments passed to the handler function depend on the event type:
|
||||
|
||||
- The ``'connect'`` event handler does not take arguments.
|
||||
- The ``'disconnect'`` event handler does not take arguments.
|
||||
- The ``'message'`` handler and handlers for custom event names receive
|
||||
the message payload as only argument. Any values returned from a
|
||||
message handler will be passed to the client's acknowledgement
|
||||
callback function if it exists.
|
||||
- A catch-all event handler receives the event name as first argument,
|
||||
followed by any arguments specific to the event.
|
||||
- A catch-all namespace event handler receives the namespace as first
|
||||
argument, followed by any arguments specific to the event.
|
||||
- A combined catch-all namespace and catch-all event handler receives
|
||||
the event name as first argument and the namespace as second
|
||||
argument, followed by any arguments specific to the event.
|
||||
"""
|
||||
namespace = namespace or '/'
|
||||
|
||||
def set_handler(handler):
|
||||
if namespace not in self.handlers:
|
||||
self.handlers[namespace] = {}
|
||||
self.handlers[namespace][event] = handler
|
||||
return handler
|
||||
|
||||
if handler is None:
|
||||
return set_handler
|
||||
set_handler(handler)
|
||||
|
||||
def event(self, *args, **kwargs):
|
||||
"""Decorator to register an event handler.
|
||||
|
||||
This is a simplified version of the ``on()`` method that takes the
|
||||
event name from the decorated function.
|
||||
|
||||
Example usage::
|
||||
|
||||
@sio.event
|
||||
def my_event(data):
|
||||
print('Received data: ', data)
|
||||
|
||||
The above example is equivalent to::
|
||||
|
||||
@sio.on('my_event')
|
||||
def my_event(data):
|
||||
print('Received data: ', data)
|
||||
|
||||
A custom namespace can be given as an argument to the decorator::
|
||||
|
||||
@sio.event(namespace='/test')
|
||||
def my_event(data):
|
||||
print('Received data: ', data)
|
||||
"""
|
||||
if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
|
||||
# the decorator was invoked without arguments
|
||||
# args[0] is the decorated function
|
||||
return self.on(args[0].__name__)(args[0])
|
||||
else:
|
||||
# the decorator was invoked with arguments
|
||||
def set_handler(handler):
|
||||
return self.on(handler.__name__, *args, **kwargs)(handler)
|
||||
|
||||
return set_handler
|
||||
|
||||
def register_namespace(self, namespace_handler):
|
||||
"""Register a namespace handler object.
|
||||
|
||||
:param namespace_handler: An instance of a :class:`Namespace`
|
||||
subclass that handles all the event traffic
|
||||
for a namespace.
|
||||
"""
|
||||
if not isinstance(namespace_handler,
|
||||
base_namespace.BaseClientNamespace):
|
||||
raise ValueError('Not a namespace instance')
|
||||
if self.is_asyncio_based() != namespace_handler.is_asyncio_based():
|
||||
raise ValueError('Not a valid namespace class for this client')
|
||||
namespace_handler._set_client(self)
|
||||
self.namespace_handlers[namespace_handler.namespace] = \
|
||||
namespace_handler
|
||||
|
||||
def get_sid(self, namespace=None):
|
||||
"""Return the ``sid`` associated with a connection.
|
||||
|
||||
:param namespace: The Socket.IO namespace. If this argument is omitted
|
||||
the handler is associated with the default
|
||||
namespace. Note that unlike previous versions, the
|
||||
current version of the Socket.IO protocol uses
|
||||
different ``sid`` values per namespace.
|
||||
|
||||
This method returns the ``sid`` for the requested namespace as a
|
||||
string.
|
||||
"""
|
||||
return self.namespaces.get(namespace or '/')
|
||||
|
||||
def transport(self):
|
||||
"""Return the name of the transport used by the client.
|
||||
|
||||
The two possible values returned by this function are ``'polling'``
|
||||
and ``'websocket'``.
|
||||
"""
|
||||
return self.eio.transport()
|
||||
|
||||
def _get_event_handler(self, event, namespace, args):
|
||||
# return the appropriate application event handler
|
||||
#
|
||||
# Resolution priority:
|
||||
# - self.handlers[namespace][event]
|
||||
# - self.handlers[namespace]["*"]
|
||||
# - self.handlers["*"][event]
|
||||
# - self.handlers["*"]["*"]
|
||||
handler = None
|
||||
if namespace in self.handlers:
|
||||
if event in self.handlers[namespace]:
|
||||
handler = self.handlers[namespace][event]
|
||||
elif event not in self.reserved_events and \
|
||||
'*' in self.handlers[namespace]:
|
||||
handler = self.handlers[namespace]['*']
|
||||
args = (event, *args)
|
||||
elif '*' in self.handlers:
|
||||
if event in self.handlers['*']:
|
||||
handler = self.handlers['*'][event]
|
||||
args = (namespace, *args)
|
||||
elif event not in self.reserved_events and \
|
||||
'*' in self.handlers['*']:
|
||||
handler = self.handlers['*']['*']
|
||||
args = (event, namespace, *args)
|
||||
return handler, args
|
||||
|
||||
def _get_namespace_handler(self, namespace, args):
|
||||
# Return the appropriate application event handler.
|
||||
#
|
||||
# Resolution priority:
|
||||
# - self.namespace_handlers[namespace]
|
||||
# - self.namespace_handlers["*"]
|
||||
handler = None
|
||||
if namespace in self.namespace_handlers:
|
||||
handler = self.namespace_handlers[namespace]
|
||||
elif '*' in self.namespace_handlers:
|
||||
handler = self.namespace_handlers['*']
|
||||
args = (namespace, *args)
|
||||
return handler, args
|
||||
|
||||
def _generate_ack_id(self, namespace, callback):
|
||||
"""Generate a unique identifier for an ACK packet."""
|
||||
namespace = namespace or '/'
|
||||
if namespace not in self.callbacks:
|
||||
self.callbacks[namespace] = {0: itertools.count(1)}
|
||||
id = next(self.callbacks[namespace][0])
|
||||
self.callbacks[namespace][id] = callback
|
||||
return id
|
||||
|
||||
def _handle_eio_connect(self): # pragma: no cover
|
||||
raise NotImplementedError()
|
||||
|
||||
def _handle_eio_message(self, data): # pragma: no cover
|
||||
raise NotImplementedError()
|
||||
|
||||
def _handle_eio_disconnect(self): # pragma: no cover
|
||||
raise NotImplementedError()
|
||||
|
||||
def _engineio_client_class(self): # pragma: no cover
|
||||
raise NotImplementedError()
|
||||
162
venv/lib/python3.12/site-packages/socketio/base_manager.py
Normal file
162
venv/lib/python3.12/site-packages/socketio/base_manager.py
Normal file
@ -0,0 +1,162 @@
|
||||
import itertools
|
||||
import logging
|
||||
|
||||
from bidict import bidict, ValueDuplicationError
|
||||
|
||||
default_logger = logging.getLogger('socketio')
|
||||
|
||||
|
||||
class BaseManager:
|
||||
def __init__(self):
|
||||
self.logger = None
|
||||
self.server = None
|
||||
self.rooms = {} # self.rooms[namespace][room][sio_sid] = eio_sid
|
||||
self.eio_to_sid = {}
|
||||
self.callbacks = {}
|
||||
self.pending_disconnect = {}
|
||||
|
||||
def set_server(self, server):
|
||||
self.server = server
|
||||
|
||||
def initialize(self):
|
||||
"""Invoked before the first request is received. Subclasses can add
|
||||
their initialization code here.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_namespaces(self):
|
||||
"""Return an iterable with the active namespace names."""
|
||||
return self.rooms.keys()
|
||||
|
||||
def get_participants(self, namespace, room):
|
||||
"""Return an iterable with the active participants in a room."""
|
||||
ns = self.rooms.get(namespace, {})
|
||||
if hasattr(room, '__len__') and not isinstance(room, str):
|
||||
participants = ns[room[0]]._fwdm.copy() if room[0] in ns else {}
|
||||
for r in room[1:]:
|
||||
participants.update(ns[r]._fwdm if r in ns else {})
|
||||
else:
|
||||
participants = ns[room]._fwdm.copy() if room in ns else {}
|
||||
for sid, eio_sid in participants.items():
|
||||
yield sid, eio_sid
|
||||
|
||||
def connect(self, eio_sid, namespace):
|
||||
"""Register a client connection to a namespace."""
|
||||
sid = self.server.eio.generate_id()
|
||||
try:
|
||||
self.basic_enter_room(sid, namespace, None, eio_sid=eio_sid)
|
||||
except ValueDuplicationError:
|
||||
# already connected
|
||||
return None
|
||||
self.basic_enter_room(sid, namespace, sid, eio_sid=eio_sid)
|
||||
return sid
|
||||
|
||||
def is_connected(self, sid, namespace):
|
||||
if namespace in self.pending_disconnect and \
|
||||
sid in self.pending_disconnect[namespace]:
|
||||
# the client is in the process of being disconnected
|
||||
return False
|
||||
try:
|
||||
return self.rooms[namespace][None][sid] is not None
|
||||
except KeyError:
|
||||
pass
|
||||
return False
|
||||
|
||||
def sid_from_eio_sid(self, eio_sid, namespace):
|
||||
try:
|
||||
return self.rooms[namespace][None]._invm[eio_sid]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def eio_sid_from_sid(self, sid, namespace):
|
||||
if namespace in self.rooms:
|
||||
return self.rooms[namespace][None].get(sid)
|
||||
|
||||
def pre_disconnect(self, sid, namespace):
|
||||
"""Put the client in the to-be-disconnected list.
|
||||
|
||||
This allows the client data structures to be present while the
|
||||
disconnect handler is invoked, but still recognize the fact that the
|
||||
client is soon going away.
|
||||
"""
|
||||
if namespace not in self.pending_disconnect:
|
||||
self.pending_disconnect[namespace] = []
|
||||
self.pending_disconnect[namespace].append(sid)
|
||||
return self.rooms[namespace][None].get(sid)
|
||||
|
||||
def basic_disconnect(self, sid, namespace, **kwargs):
|
||||
if namespace not in self.rooms:
|
||||
return
|
||||
rooms = []
|
||||
for room_name, room in self.rooms[namespace].copy().items():
|
||||
if sid in room:
|
||||
rooms.append(room_name)
|
||||
for room in rooms:
|
||||
self.basic_leave_room(sid, namespace, room)
|
||||
if sid in self.callbacks:
|
||||
del self.callbacks[sid]
|
||||
if namespace in self.pending_disconnect and \
|
||||
sid in self.pending_disconnect[namespace]:
|
||||
self.pending_disconnect[namespace].remove(sid)
|
||||
if len(self.pending_disconnect[namespace]) == 0:
|
||||
del self.pending_disconnect[namespace]
|
||||
|
||||
def basic_enter_room(self, sid, namespace, room, eio_sid=None):
|
||||
if eio_sid is None and namespace not in self.rooms:
|
||||
raise ValueError('sid is not connected to requested namespace')
|
||||
if namespace not in self.rooms:
|
||||
self.rooms[namespace] = {}
|
||||
if room not in self.rooms[namespace]:
|
||||
self.rooms[namespace][room] = bidict()
|
||||
if eio_sid is None:
|
||||
eio_sid = self.rooms[namespace][None][sid]
|
||||
self.rooms[namespace][room][sid] = eio_sid
|
||||
|
||||
def basic_leave_room(self, sid, namespace, room):
|
||||
try:
|
||||
del self.rooms[namespace][room][sid]
|
||||
if len(self.rooms[namespace][room]) == 0:
|
||||
del self.rooms[namespace][room]
|
||||
if len(self.rooms[namespace]) == 0:
|
||||
del self.rooms[namespace]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def basic_close_room(self, room, namespace):
|
||||
try:
|
||||
for sid, _ in self.get_participants(namespace, room):
|
||||
self.basic_leave_room(sid, namespace, room)
|
||||
except KeyError: # pragma: no cover
|
||||
pass
|
||||
|
||||
def get_rooms(self, sid, namespace):
|
||||
"""Return the rooms a client is in."""
|
||||
r = []
|
||||
try:
|
||||
for room_name, room in self.rooms[namespace].items():
|
||||
if room_name is not None and sid in room:
|
||||
r.append(room_name)
|
||||
except KeyError:
|
||||
pass
|
||||
return r
|
||||
|
||||
def _generate_ack_id(self, sid, callback):
|
||||
"""Generate a unique identifier for an ACK packet."""
|
||||
if sid not in self.callbacks:
|
||||
self.callbacks[sid] = {0: itertools.count(1)}
|
||||
id = next(self.callbacks[sid][0])
|
||||
self.callbacks[sid][id] = callback
|
||||
return id
|
||||
|
||||
def _get_logger(self):
|
||||
"""Get the appropriate logger
|
||||
|
||||
Prevents uninitialized servers in write-only mode from failing.
|
||||
"""
|
||||
|
||||
if self.logger:
|
||||
return self.logger
|
||||
elif self.server:
|
||||
return self.server.logger
|
||||
else:
|
||||
return default_logger
|
||||
33
venv/lib/python3.12/site-packages/socketio/base_namespace.py
Normal file
33
venv/lib/python3.12/site-packages/socketio/base_namespace.py
Normal file
@ -0,0 +1,33 @@
|
||||
class BaseNamespace(object):
|
||||
def __init__(self, namespace=None):
|
||||
self.namespace = namespace or '/'
|
||||
|
||||
def is_asyncio_based(self):
|
||||
return False
|
||||
|
||||
|
||||
class BaseServerNamespace(BaseNamespace):
|
||||
def __init__(self, namespace=None):
|
||||
super().__init__(namespace=namespace)
|
||||
self.server = None
|
||||
|
||||
def _set_server(self, server):
|
||||
self.server = server
|
||||
|
||||
def rooms(self, sid, namespace=None):
|
||||
"""Return the rooms a client is in.
|
||||
|
||||
The only difference with the :func:`socketio.Server.rooms` method is
|
||||
that when the ``namespace`` argument is not given the namespace
|
||||
associated with the class is used.
|
||||
"""
|
||||
return self.server.rooms(sid, namespace=namespace or self.namespace)
|
||||
|
||||
|
||||
class BaseClientNamespace(BaseNamespace):
|
||||
def __init__(self, namespace=None):
|
||||
super().__init__(namespace=namespace)
|
||||
self.client = None
|
||||
|
||||
def _set_client(self, client):
|
||||
self.client = client
|
||||
263
venv/lib/python3.12/site-packages/socketio/base_server.py
Normal file
263
venv/lib/python3.12/site-packages/socketio/base_server.py
Normal file
@ -0,0 +1,263 @@
|
||||
import logging
|
||||
|
||||
from . import manager
|
||||
from . import base_namespace
|
||||
from . import packet
|
||||
|
||||
default_logger = logging.getLogger('socketio.server')
|
||||
|
||||
|
||||
class BaseServer:
|
||||
reserved_events = ['connect', 'disconnect']
|
||||
|
||||
def __init__(self, client_manager=None, logger=False, serializer='default',
|
||||
json=None, async_handlers=True, always_connect=False,
|
||||
namespaces=None, **kwargs):
|
||||
engineio_options = kwargs
|
||||
engineio_logger = engineio_options.pop('engineio_logger', None)
|
||||
if engineio_logger is not None:
|
||||
engineio_options['logger'] = engineio_logger
|
||||
if serializer == 'default':
|
||||
self.packet_class = packet.Packet
|
||||
elif serializer == 'msgpack':
|
||||
from . import msgpack_packet
|
||||
self.packet_class = msgpack_packet.MsgPackPacket
|
||||
else:
|
||||
self.packet_class = serializer
|
||||
if json is not None:
|
||||
self.packet_class.json = json
|
||||
engineio_options['json'] = json
|
||||
engineio_options['async_handlers'] = False
|
||||
self.eio = self._engineio_server_class()(**engineio_options)
|
||||
self.eio.on('connect', self._handle_eio_connect)
|
||||
self.eio.on('message', self._handle_eio_message)
|
||||
self.eio.on('disconnect', self._handle_eio_disconnect)
|
||||
|
||||
self.environ = {}
|
||||
self.handlers = {}
|
||||
self.namespace_handlers = {}
|
||||
self.not_handled = object()
|
||||
|
||||
self._binary_packet = {}
|
||||
|
||||
if not isinstance(logger, bool):
|
||||
self.logger = logger
|
||||
else:
|
||||
self.logger = default_logger
|
||||
if self.logger.level == logging.NOTSET:
|
||||
if logger:
|
||||
self.logger.setLevel(logging.INFO)
|
||||
else:
|
||||
self.logger.setLevel(logging.ERROR)
|
||||
self.logger.addHandler(logging.StreamHandler())
|
||||
|
||||
if client_manager is None:
|
||||
client_manager = manager.Manager()
|
||||
self.manager = client_manager
|
||||
self.manager.set_server(self)
|
||||
self.manager_initialized = False
|
||||
|
||||
self.async_handlers = async_handlers
|
||||
self.always_connect = always_connect
|
||||
self.namespaces = namespaces or ['/']
|
||||
|
||||
self.async_mode = self.eio.async_mode
|
||||
|
||||
def is_asyncio_based(self):
|
||||
return False
|
||||
|
||||
def on(self, event, handler=None, namespace=None):
|
||||
"""Register an event handler.
|
||||
|
||||
:param event: The event name. It can be any string. The event names
|
||||
``'connect'``, ``'message'`` and ``'disconnect'`` are
|
||||
reserved and should not be used. The ``'*'`` event name
|
||||
can be used to define a catch-all event handler.
|
||||
:param handler: The function that should be invoked to handle the
|
||||
event. When this parameter is not given, the method
|
||||
acts as a decorator for the handler function.
|
||||
:param namespace: The Socket.IO namespace for the event. If this
|
||||
argument is omitted the handler is associated with
|
||||
the default namespace. A catch-all namespace can be
|
||||
defined by passing ``'*'`` as the namespace.
|
||||
|
||||
Example usage::
|
||||
|
||||
# as a decorator:
|
||||
@sio.on('connect', namespace='/chat')
|
||||
def connect_handler(sid, environ):
|
||||
print('Connection request')
|
||||
if environ['REMOTE_ADDR'] in blacklisted:
|
||||
return False # reject
|
||||
|
||||
# as a method:
|
||||
def message_handler(sid, msg):
|
||||
print('Received message: ', msg)
|
||||
sio.send(sid, 'response')
|
||||
socket_io.on('message', namespace='/chat', handler=message_handler)
|
||||
|
||||
The arguments passed to the handler function depend on the event type:
|
||||
|
||||
- The ``'connect'`` event handler receives the ``sid`` (session ID) for
|
||||
the client and the WSGI environment dictionary as arguments.
|
||||
- The ``'disconnect'`` handler receives the ``sid`` for the client as
|
||||
only argument.
|
||||
- The ``'message'`` handler and handlers for custom event names receive
|
||||
the ``sid`` for the client and the message payload as arguments. Any
|
||||
values returned from a message handler will be passed to the client's
|
||||
acknowledgement callback function if it exists.
|
||||
- A catch-all event handler receives the event name as first argument,
|
||||
followed by any arguments specific to the event.
|
||||
- A catch-all namespace event handler receives the namespace as first
|
||||
argument, followed by any arguments specific to the event.
|
||||
- A combined catch-all namespace and catch-all event handler receives
|
||||
the event name as first argument and the namespace as second
|
||||
argument, followed by any arguments specific to the event.
|
||||
"""
|
||||
namespace = namespace or '/'
|
||||
|
||||
def set_handler(handler):
|
||||
if namespace not in self.handlers:
|
||||
self.handlers[namespace] = {}
|
||||
self.handlers[namespace][event] = handler
|
||||
return handler
|
||||
|
||||
if handler is None:
|
||||
return set_handler
|
||||
set_handler(handler)
|
||||
|
||||
def event(self, *args, **kwargs):
|
||||
"""Decorator to register an event handler.
|
||||
|
||||
This is a simplified version of the ``on()`` method that takes the
|
||||
event name from the decorated function.
|
||||
|
||||
Example usage::
|
||||
|
||||
@sio.event
|
||||
def my_event(data):
|
||||
print('Received data: ', data)
|
||||
|
||||
The above example is equivalent to::
|
||||
|
||||
@sio.on('my_event')
|
||||
def my_event(data):
|
||||
print('Received data: ', data)
|
||||
|
||||
A custom namespace can be given as an argument to the decorator::
|
||||
|
||||
@sio.event(namespace='/test')
|
||||
def my_event(data):
|
||||
print('Received data: ', data)
|
||||
"""
|
||||
if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
|
||||
# the decorator was invoked without arguments
|
||||
# args[0] is the decorated function
|
||||
return self.on(args[0].__name__)(args[0])
|
||||
else:
|
||||
# the decorator was invoked with arguments
|
||||
def set_handler(handler):
|
||||
return self.on(handler.__name__, *args, **kwargs)(handler)
|
||||
|
||||
return set_handler
|
||||
|
||||
def register_namespace(self, namespace_handler):
|
||||
"""Register a namespace handler object.
|
||||
|
||||
:param namespace_handler: An instance of a :class:`Namespace`
|
||||
subclass that handles all the event traffic
|
||||
for a namespace.
|
||||
"""
|
||||
if not isinstance(namespace_handler,
|
||||
base_namespace.BaseServerNamespace):
|
||||
raise ValueError('Not a namespace instance')
|
||||
if self.is_asyncio_based() != namespace_handler.is_asyncio_based():
|
||||
raise ValueError('Not a valid namespace class for this server')
|
||||
namespace_handler._set_server(self)
|
||||
self.namespace_handlers[namespace_handler.namespace] = \
|
||||
namespace_handler
|
||||
|
||||
def rooms(self, sid, namespace=None):
|
||||
"""Return the rooms a client is in.
|
||||
|
||||
:param sid: Session ID of the client.
|
||||
:param namespace: The Socket.IO namespace for the event. If this
|
||||
argument is omitted the default namespace is used.
|
||||
"""
|
||||
namespace = namespace or '/'
|
||||
return self.manager.get_rooms(sid, namespace)
|
||||
|
||||
def transport(self, sid, namespace=None):
|
||||
"""Return the name of the transport used by the client.
|
||||
|
||||
The two possible values returned by this function are ``'polling'``
|
||||
and ``'websocket'``.
|
||||
|
||||
:param sid: The session of the client.
|
||||
:param namespace: The Socket.IO namespace. If this argument is omitted
|
||||
the default namespace is used.
|
||||
"""
|
||||
eio_sid = self.manager.eio_sid_from_sid(sid, namespace or '/')
|
||||
return self.eio.transport(eio_sid)
|
||||
|
||||
def get_environ(self, sid, namespace=None):
|
||||
"""Return the WSGI environ dictionary for a client.
|
||||
|
||||
:param sid: The session of the client.
|
||||
:param namespace: The Socket.IO namespace. If this argument is omitted
|
||||
the default namespace is used.
|
||||
"""
|
||||
eio_sid = self.manager.eio_sid_from_sid(sid, namespace or '/')
|
||||
return self.environ.get(eio_sid)
|
||||
|
||||
def _get_event_handler(self, event, namespace, args):
|
||||
# Return the appropriate application event handler
|
||||
#
|
||||
# Resolution priority:
|
||||
# - self.handlers[namespace][event]
|
||||
# - self.handlers[namespace]["*"]
|
||||
# - self.handlers["*"][event]
|
||||
# - self.handlers["*"]["*"]
|
||||
handler = None
|
||||
if namespace in self.handlers:
|
||||
if event in self.handlers[namespace]:
|
||||
handler = self.handlers[namespace][event]
|
||||
elif event not in self.reserved_events and \
|
||||
'*' in self.handlers[namespace]:
|
||||
handler = self.handlers[namespace]['*']
|
||||
args = (event, *args)
|
||||
if handler is None and '*' in self.handlers:
|
||||
if event in self.handlers['*']:
|
||||
handler = self.handlers['*'][event]
|
||||
args = (namespace, *args)
|
||||
elif event not in self.reserved_events and \
|
||||
'*' in self.handlers['*']:
|
||||
handler = self.handlers['*']['*']
|
||||
args = (event, namespace, *args)
|
||||
return handler, args
|
||||
|
||||
def _get_namespace_handler(self, namespace, args):
|
||||
# Return the appropriate application event handler.
|
||||
#
|
||||
# Resolution priority:
|
||||
# - self.namespace_handlers[namespace]
|
||||
# - self.namespace_handlers["*"]
|
||||
handler = None
|
||||
if namespace in self.namespace_handlers:
|
||||
handler = self.namespace_handlers[namespace]
|
||||
if handler is None and '*' in self.namespace_handlers:
|
||||
handler = self.namespace_handlers['*']
|
||||
args = (namespace, *args)
|
||||
return handler, args
|
||||
|
||||
def _handle_eio_connect(self): # pragma: no cover
|
||||
raise NotImplementedError()
|
||||
|
||||
def _handle_eio_message(self, data): # pragma: no cover
|
||||
raise NotImplementedError()
|
||||
|
||||
def _handle_eio_disconnect(self): # pragma: no cover
|
||||
raise NotImplementedError()
|
||||
|
||||
def _engineio_server_class(self): # pragma: no cover
|
||||
raise NotImplementedError('Must be implemented in subclasses')
|
||||
542
venv/lib/python3.12/site-packages/socketio/client.py
Normal file
542
venv/lib/python3.12/site-packages/socketio/client.py
Normal file
@ -0,0 +1,542 @@
|
||||
import random
|
||||
|
||||
import engineio
|
||||
|
||||
from . import base_client
|
||||
from . import exceptions
|
||||
from . import packet
|
||||
|
||||
|
||||
class Client(base_client.BaseClient):
|
||||
"""A Socket.IO client.
|
||||
|
||||
This class implements a fully compliant Socket.IO web client with support
|
||||
for websocket and long-polling transports.
|
||||
|
||||
:param reconnection: ``True`` if the client should automatically attempt to
|
||||
reconnect to the server after an interruption, or
|
||||
``False`` to not reconnect. The default is ``True``.
|
||||
:param reconnection_attempts: How many reconnection attempts to issue
|
||||
before giving up, or 0 for infinite attempts.
|
||||
The default is 0.
|
||||
:param reconnection_delay: How long to wait in seconds before the first
|
||||
reconnection attempt. Each successive attempt
|
||||
doubles this delay.
|
||||
:param reconnection_delay_max: The maximum delay between reconnection
|
||||
attempts.
|
||||
:param randomization_factor: Randomization amount for each delay between
|
||||
reconnection attempts. The default is 0.5,
|
||||
which means that each delay is randomly
|
||||
adjusted by +/- 50%.
|
||||
:param logger: To enable logging set to ``True`` or pass a logger object to
|
||||
use. To disable logging set to ``False``. The default is
|
||||
``False``. Note that fatal errors are logged even when
|
||||
``logger`` is ``False``.
|
||||
:param serializer: The serialization method to use when transmitting
|
||||
packets. Valid values are ``'default'``, ``'pickle'``,
|
||||
``'msgpack'`` and ``'cbor'``. Alternatively, a subclass
|
||||
of the :class:`Packet` class with custom implementations
|
||||
of the ``encode()`` and ``decode()`` methods can be
|
||||
provided. Client and server must use compatible
|
||||
serializers.
|
||||
:param json: An alternative json module to use for encoding and decoding
|
||||
packets. Custom json modules must have ``dumps`` and ``loads``
|
||||
functions that are compatible with the standard library
|
||||
versions.
|
||||
:param handle_sigint: Set to ``True`` to automatically handle disconnection
|
||||
when the process is interrupted, or to ``False`` to
|
||||
leave interrupt handling to the calling application.
|
||||
Interrupt handling can only be enabled when the
|
||||
client instance is created in the main thread.
|
||||
|
||||
The Engine.IO configuration supports the following settings:
|
||||
|
||||
:param request_timeout: A timeout in seconds for requests. The default is
|
||||
5 seconds.
|
||||
:param http_session: an initialized ``requests.Session`` object to be used
|
||||
when sending requests to the server. Use it if you
|
||||
need to add special client options such as proxy
|
||||
servers, SSL certificates, custom CA bundle, etc.
|
||||
:param ssl_verify: ``True`` to verify SSL certificates, or ``False`` to
|
||||
skip SSL certificate verification, allowing
|
||||
connections to servers with self signed certificates.
|
||||
The default is ``True``.
|
||||
:param websocket_extra_options: Dictionary containing additional keyword
|
||||
arguments passed to
|
||||
``websocket.create_connection()``.
|
||||
:param engineio_logger: To enable Engine.IO logging set to ``True`` or pass
|
||||
a logger object to use. To disable logging set to
|
||||
``False``. The default is ``False``. Note that
|
||||
fatal errors are logged even when
|
||||
``engineio_logger`` is ``False``.
|
||||
"""
|
||||
def connect(self, url, headers={}, auth=None, transports=None,
|
||||
namespaces=None, socketio_path='socket.io', wait=True,
|
||||
wait_timeout=1, retry=False):
|
||||
"""Connect to a Socket.IO server.
|
||||
|
||||
:param url: The URL of the Socket.IO server. It can include custom
|
||||
query string parameters if required by the server. If a
|
||||
function is provided, the client will invoke it to obtain
|
||||
the URL each time a connection or reconnection is
|
||||
attempted.
|
||||
:param headers: A dictionary with custom headers to send with the
|
||||
connection request. If a function is provided, the
|
||||
client will invoke it to obtain the headers dictionary
|
||||
each time a connection or reconnection is attempted.
|
||||
:param auth: Authentication data passed to the server with the
|
||||
connection request, normally a dictionary with one or
|
||||
more string key/value pairs. If a function is provided,
|
||||
the client will invoke it to obtain the authentication
|
||||
data each time a connection or reconnection is attempted.
|
||||
:param transports: The list of allowed transports. Valid transports
|
||||
are ``'polling'`` and ``'websocket'``. If not
|
||||
given, the polling transport is connected first,
|
||||
then an upgrade to websocket is attempted.
|
||||
:param namespaces: The namespaces to connect as a string or list of
|
||||
strings. If not given, the namespaces that have
|
||||
registered event handlers are connected.
|
||||
:param socketio_path: The endpoint where the Socket.IO server is
|
||||
installed. The default value is appropriate for
|
||||
most cases.
|
||||
:param wait: if set to ``True`` (the default) the call only returns
|
||||
when all the namespaces are connected. If set to
|
||||
``False``, the call returns as soon as the Engine.IO
|
||||
transport is connected, and the namespaces will connect
|
||||
in the background.
|
||||
:param wait_timeout: How long the client should wait for the
|
||||
connection. The default is 1 second. This
|
||||
argument is only considered when ``wait`` is set
|
||||
to ``True``.
|
||||
:param retry: Apply the reconnection logic if the initial connection
|
||||
attempt fails. The default is ``False``.
|
||||
|
||||
Example usage::
|
||||
|
||||
sio = socketio.Client()
|
||||
sio.connect('http://localhost:5000')
|
||||
"""
|
||||
if self.connected:
|
||||
raise exceptions.ConnectionError('Already connected')
|
||||
|
||||
self.connection_url = url
|
||||
self.connection_headers = headers
|
||||
self.connection_auth = auth
|
||||
self.connection_transports = transports
|
||||
self.connection_namespaces = namespaces
|
||||
self.socketio_path = socketio_path
|
||||
|
||||
if namespaces is None:
|
||||
namespaces = list(set(self.handlers.keys()).union(
|
||||
set(self.namespace_handlers.keys())))
|
||||
if '*' in namespaces:
|
||||
namespaces.remove('*')
|
||||
if len(namespaces) == 0:
|
||||
namespaces = ['/']
|
||||
elif isinstance(namespaces, str):
|
||||
namespaces = [namespaces]
|
||||
self.connection_namespaces = namespaces
|
||||
self.namespaces = {}
|
||||
if self._connect_event is None:
|
||||
self._connect_event = self.eio.create_event()
|
||||
else:
|
||||
self._connect_event.clear()
|
||||
real_url = self._get_real_value(self.connection_url)
|
||||
real_headers = self._get_real_value(self.connection_headers)
|
||||
try:
|
||||
self.eio.connect(real_url, headers=real_headers,
|
||||
transports=transports,
|
||||
engineio_path=socketio_path)
|
||||
except engineio.exceptions.ConnectionError as exc:
|
||||
for n in self.connection_namespaces:
|
||||
self._trigger_event(
|
||||
'connect_error', n,
|
||||
exc.args[1] if len(exc.args) > 1 else exc.args[0])
|
||||
if retry: # pragma: no cover
|
||||
self._handle_reconnect()
|
||||
if self.eio.state == 'connected':
|
||||
return
|
||||
raise exceptions.ConnectionError(exc.args[0]) from None
|
||||
|
||||
if wait:
|
||||
while self._connect_event.wait(timeout=wait_timeout):
|
||||
self._connect_event.clear()
|
||||
if set(self.namespaces) == set(self.connection_namespaces):
|
||||
break
|
||||
if set(self.namespaces) != set(self.connection_namespaces):
|
||||
self.disconnect()
|
||||
raise exceptions.ConnectionError(
|
||||
'One or more namespaces failed to connect')
|
||||
|
||||
self.connected = True
|
||||
|
||||
def wait(self):
|
||||
"""Wait until the connection with the server ends.
|
||||
|
||||
Client applications can use this function to block the main thread
|
||||
during the life of the connection.
|
||||
"""
|
||||
while True:
|
||||
self.eio.wait()
|
||||
self.sleep(1) # give the reconnect task time to start up
|
||||
if not self._reconnect_task:
|
||||
break
|
||||
self._reconnect_task.join()
|
||||
if self.eio.state != 'connected':
|
||||
break
|
||||
|
||||
def emit(self, event, data=None, namespace=None, callback=None):
|
||||
"""Emit a custom event to the server.
|
||||
|
||||
:param event: The event name. It can be any string. The event names
|
||||
``'connect'``, ``'message'`` and ``'disconnect'`` are
|
||||
reserved and should not be used.
|
||||
:param data: The data to send to the server. Data can be of
|
||||
type ``str``, ``bytes``, ``list`` or ``dict``. To send
|
||||
multiple arguments, use a tuple where each element is of
|
||||
one of the types indicated above.
|
||||
:param namespace: The Socket.IO namespace for the event. If this
|
||||
argument is omitted the event is emitted to the
|
||||
default namespace.
|
||||
:param callback: If given, this function will be called to acknowledge
|
||||
the server has received the message. The arguments
|
||||
that will be passed to the function are those provided
|
||||
by the server.
|
||||
|
||||
Note: this method is not thread safe. If multiple threads are emitting
|
||||
at the same time on the same client connection, messages composed of
|
||||
multiple packets may end up being sent in an incorrect sequence. Use
|
||||
standard concurrency solutions (such as a Lock object) to prevent this
|
||||
situation.
|
||||
"""
|
||||
namespace = namespace or '/'
|
||||
if namespace not in self.namespaces:
|
||||
raise exceptions.BadNamespaceError(
|
||||
namespace + ' is not a connected namespace.')
|
||||
self.logger.info('Emitting event "%s" [%s]', event, namespace)
|
||||
if callback is not None:
|
||||
id = self._generate_ack_id(namespace, callback)
|
||||
else:
|
||||
id = None
|
||||
# tuples are expanded to multiple arguments, everything else is sent
|
||||
# as a single argument
|
||||
if isinstance(data, tuple):
|
||||
data = list(data)
|
||||
elif data is not None:
|
||||
data = [data]
|
||||
else:
|
||||
data = []
|
||||
self._send_packet(self.packet_class(packet.EVENT, namespace=namespace,
|
||||
data=[event] + data, id=id))
|
||||
|
||||
def send(self, data, namespace=None, callback=None):
|
||||
"""Send a message to the server.
|
||||
|
||||
This function emits an event with the name ``'message'``. Use
|
||||
:func:`emit` to issue custom event names.
|
||||
|
||||
:param data: The data to send to the server. Data can be of
|
||||
type ``str``, ``bytes``, ``list`` or ``dict``. To send
|
||||
multiple arguments, use a tuple where each element is of
|
||||
one of the types indicated above.
|
||||
:param namespace: The Socket.IO namespace for the event. If this
|
||||
argument is omitted the event is emitted to the
|
||||
default namespace.
|
||||
:param callback: If given, this function will be called to acknowledge
|
||||
the server has received the message. The arguments
|
||||
that will be passed to the function are those provided
|
||||
by the server.
|
||||
"""
|
||||
self.emit('message', data=data, namespace=namespace,
|
||||
callback=callback)
|
||||
|
||||
def call(self, event, data=None, namespace=None, timeout=60):
|
||||
"""Emit a custom event to the server and wait for the response.
|
||||
|
||||
This method issues an emit with a callback and waits for the callback
|
||||
to be invoked before returning. If the callback isn't invoked before
|
||||
the timeout, then a ``TimeoutError`` exception is raised. If the
|
||||
Socket.IO connection drops during the wait, this method still waits
|
||||
until the specified timeout.
|
||||
|
||||
:param event: The event name. It can be any string. The event names
|
||||
``'connect'``, ``'message'`` and ``'disconnect'`` are
|
||||
reserved and should not be used.
|
||||
:param data: The data to send to the server. Data can be of
|
||||
type ``str``, ``bytes``, ``list`` or ``dict``. To send
|
||||
multiple arguments, use a tuple where each element is of
|
||||
one of the types indicated above.
|
||||
:param namespace: The Socket.IO namespace for the event. If this
|
||||
argument is omitted the event is emitted to the
|
||||
default namespace.
|
||||
:param timeout: The waiting timeout. If the timeout is reached before
|
||||
the server acknowledges the event, then a
|
||||
``TimeoutError`` exception is raised.
|
||||
|
||||
Note: this method is not thread safe. If multiple threads are emitting
|
||||
at the same time on the same client connection, messages composed of
|
||||
multiple packets may end up being sent in an incorrect sequence. Use
|
||||
standard concurrency solutions (such as a Lock object) to prevent this
|
||||
situation.
|
||||
"""
|
||||
callback_event = self.eio.create_event()
|
||||
callback_args = []
|
||||
|
||||
def event_callback(*args):
|
||||
callback_args.append(args)
|
||||
callback_event.set()
|
||||
|
||||
self.emit(event, data=data, namespace=namespace,
|
||||
callback=event_callback)
|
||||
if not callback_event.wait(timeout=timeout):
|
||||
raise exceptions.TimeoutError()
|
||||
return callback_args[0] if len(callback_args[0]) > 1 \
|
||||
else callback_args[0][0] if len(callback_args[0]) == 1 \
|
||||
else None
|
||||
|
||||
def disconnect(self):
|
||||
"""Disconnect from the server."""
|
||||
# here we just request the disconnection
|
||||
# later in _handle_eio_disconnect we invoke the disconnect handler
|
||||
for n in self.namespaces:
|
||||
self._send_packet(self.packet_class(
|
||||
packet.DISCONNECT, namespace=n))
|
||||
self.eio.disconnect(abort=True)
|
||||
|
||||
def shutdown(self):
|
||||
"""Stop the client.
|
||||
|
||||
If the client is connected to a server, it is disconnected. If the
|
||||
client is attempting to reconnect to server, the reconnection attempts
|
||||
are stopped. If the client is not connected to a server and is not
|
||||
attempting to reconnect, then this function does nothing.
|
||||
"""
|
||||
if self.connected:
|
||||
self.disconnect()
|
||||
elif self._reconnect_task: # pragma: no branch
|
||||
self._reconnect_abort.set()
|
||||
self._reconnect_task.join()
|
||||
|
||||
def start_background_task(self, target, *args, **kwargs):
|
||||
"""Start a background task using the appropriate async model.
|
||||
|
||||
This is a utility function that applications can use to start a
|
||||
background task using the method that is compatible with the
|
||||
selected async mode.
|
||||
|
||||
:param target: the target function to execute.
|
||||
:param args: arguments to pass to the function.
|
||||
:param kwargs: keyword arguments to pass to the function.
|
||||
|
||||
This function returns an object that represents the background task,
|
||||
on which the ``join()`` methond can be invoked to wait for the task to
|
||||
complete.
|
||||
"""
|
||||
return self.eio.start_background_task(target, *args, **kwargs)
|
||||
|
||||
def sleep(self, seconds=0):
|
||||
"""Sleep for the requested amount of time using the appropriate async
|
||||
model.
|
||||
|
||||
This is a utility function that applications can use to put a task to
|
||||
sleep without having to worry about using the correct call for the
|
||||
selected async mode.
|
||||
"""
|
||||
return self.eio.sleep(seconds)
|
||||
|
||||
def _get_real_value(self, value):
|
||||
"""Return the actual value, for parameters that can also be given as
|
||||
callables."""
|
||||
if not callable(value):
|
||||
return value
|
||||
return value()
|
||||
|
||||
def _send_packet(self, pkt):
|
||||
"""Send a Socket.IO packet to the server."""
|
||||
encoded_packet = pkt.encode()
|
||||
if isinstance(encoded_packet, list):
|
||||
for ep in encoded_packet:
|
||||
self.eio.send(ep)
|
||||
else:
|
||||
self.eio.send(encoded_packet)
|
||||
|
||||
def _handle_connect(self, namespace, data):
|
||||
namespace = namespace or '/'
|
||||
if namespace not in self.namespaces:
|
||||
self.logger.info('Namespace {} is connected'.format(namespace))
|
||||
self.namespaces[namespace] = (data or {}).get('sid', self.sid)
|
||||
self._trigger_event('connect', namespace=namespace)
|
||||
self._connect_event.set()
|
||||
|
||||
def _handle_disconnect(self, namespace):
|
||||
if not self.connected:
|
||||
return
|
||||
namespace = namespace or '/'
|
||||
self._trigger_event('disconnect', namespace=namespace)
|
||||
self._trigger_event('__disconnect_final', namespace=namespace)
|
||||
if namespace in self.namespaces:
|
||||
del self.namespaces[namespace]
|
||||
if not self.namespaces:
|
||||
self.connected = False
|
||||
self.eio.disconnect(abort=True)
|
||||
|
||||
def _handle_event(self, namespace, id, data):
|
||||
namespace = namespace or '/'
|
||||
self.logger.info('Received event "%s" [%s]', data[0], namespace)
|
||||
r = self._trigger_event(data[0], namespace, *data[1:])
|
||||
if id is not None:
|
||||
# send ACK packet with the response returned by the handler
|
||||
# tuples are expanded as multiple arguments
|
||||
if r is None:
|
||||
data = []
|
||||
elif isinstance(r, tuple):
|
||||
data = list(r)
|
||||
else:
|
||||
data = [r]
|
||||
self._send_packet(self.packet_class(
|
||||
packet.ACK, namespace=namespace, id=id, data=data))
|
||||
|
||||
def _handle_ack(self, namespace, id, data):
|
||||
namespace = namespace or '/'
|
||||
self.logger.info('Received ack [%s]', namespace)
|
||||
callback = None
|
||||
try:
|
||||
callback = self.callbacks[namespace][id]
|
||||
except KeyError:
|
||||
# if we get an unknown callback we just ignore it
|
||||
self.logger.warning('Unknown callback received, ignoring.')
|
||||
else:
|
||||
del self.callbacks[namespace][id]
|
||||
if callback is not None:
|
||||
callback(*data)
|
||||
|
||||
def _handle_error(self, namespace, data):
|
||||
namespace = namespace or '/'
|
||||
self.logger.info('Connection to namespace {} was rejected'.format(
|
||||
namespace))
|
||||
if data is None:
|
||||
data = tuple()
|
||||
elif not isinstance(data, (tuple, list)):
|
||||
data = (data,)
|
||||
self._trigger_event('connect_error', namespace, *data)
|
||||
self._connect_event.set()
|
||||
if namespace in self.namespaces:
|
||||
del self.namespaces[namespace]
|
||||
if namespace == '/':
|
||||
self.namespaces = {}
|
||||
self.connected = False
|
||||
|
||||
def _trigger_event(self, event, namespace, *args):
|
||||
"""Invoke an application event handler."""
|
||||
# first see if we have an explicit handler for the event
|
||||
handler, args = self._get_event_handler(event, namespace, args)
|
||||
if handler:
|
||||
return handler(*args)
|
||||
|
||||
# or else, forward the event to a namespace handler if one exists
|
||||
handler, args = self._get_namespace_handler(namespace, args)
|
||||
if handler:
|
||||
return handler.trigger_event(event, *args)
|
||||
|
||||
def _handle_reconnect(self):
|
||||
if self._reconnect_abort is None: # pragma: no cover
|
||||
self._reconnect_abort = self.eio.create_event()
|
||||
self._reconnect_abort.clear()
|
||||
base_client.reconnecting_clients.append(self)
|
||||
attempt_count = 0
|
||||
current_delay = self.reconnection_delay
|
||||
while True:
|
||||
delay = current_delay
|
||||
current_delay *= 2
|
||||
if delay > self.reconnection_delay_max:
|
||||
delay = self.reconnection_delay_max
|
||||
delay += self.randomization_factor * (2 * random.random() - 1)
|
||||
self.logger.info(
|
||||
'Connection failed, new attempt in {:.02f} seconds'.format(
|
||||
delay))
|
||||
if self._reconnect_abort.wait(delay):
|
||||
self.logger.info('Reconnect task aborted')
|
||||
for n in self.connection_namespaces:
|
||||
self._trigger_event('__disconnect_final', namespace=n)
|
||||
break
|
||||
attempt_count += 1
|
||||
try:
|
||||
self.connect(self.connection_url,
|
||||
headers=self.connection_headers,
|
||||
auth=self.connection_auth,
|
||||
transports=self.connection_transports,
|
||||
namespaces=self.connection_namespaces,
|
||||
socketio_path=self.socketio_path,
|
||||
retry=False)
|
||||
except (exceptions.ConnectionError, ValueError):
|
||||
pass
|
||||
else:
|
||||
self.logger.info('Reconnection successful')
|
||||
self._reconnect_task = None
|
||||
break
|
||||
if self.reconnection_attempts and \
|
||||
attempt_count >= self.reconnection_attempts:
|
||||
self.logger.info(
|
||||
'Maximum reconnection attempts reached, giving up')
|
||||
for n in self.connection_namespaces:
|
||||
self._trigger_event('__disconnect_final', namespace=n)
|
||||
break
|
||||
base_client.reconnecting_clients.remove(self)
|
||||
|
||||
def _handle_eio_connect(self):
|
||||
"""Handle the Engine.IO connection event."""
|
||||
self.logger.info('Engine.IO connection established')
|
||||
self.sid = self.eio.sid
|
||||
real_auth = self._get_real_value(self.connection_auth) or {}
|
||||
for n in self.connection_namespaces:
|
||||
self._send_packet(self.packet_class(
|
||||
packet.CONNECT, data=real_auth, namespace=n))
|
||||
|
||||
def _handle_eio_message(self, data):
|
||||
"""Dispatch Engine.IO messages."""
|
||||
if self._binary_packet:
|
||||
pkt = self._binary_packet
|
||||
if pkt.add_attachment(data):
|
||||
self._binary_packet = None
|
||||
if pkt.packet_type == packet.BINARY_EVENT:
|
||||
self._handle_event(pkt.namespace, pkt.id, pkt.data)
|
||||
else:
|
||||
self._handle_ack(pkt.namespace, pkt.id, pkt.data)
|
||||
else:
|
||||
pkt = self.packet_class(encoded_packet=data)
|
||||
if pkt.packet_type == packet.CONNECT:
|
||||
self._handle_connect(pkt.namespace, pkt.data)
|
||||
elif pkt.packet_type == packet.DISCONNECT:
|
||||
self._handle_disconnect(pkt.namespace)
|
||||
elif pkt.packet_type == packet.EVENT:
|
||||
self._handle_event(pkt.namespace, pkt.id, pkt.data)
|
||||
elif pkt.packet_type == packet.ACK:
|
||||
self._handle_ack(pkt.namespace, pkt.id, pkt.data)
|
||||
elif pkt.packet_type == packet.BINARY_EVENT or \
|
||||
pkt.packet_type == packet.BINARY_ACK:
|
||||
self._binary_packet = pkt
|
||||
elif pkt.packet_type == packet.CONNECT_ERROR:
|
||||
self._handle_error(pkt.namespace, pkt.data)
|
||||
else:
|
||||
raise ValueError('Unknown packet type.')
|
||||
|
||||
def _handle_eio_disconnect(self):
|
||||
"""Handle the Engine.IO disconnection event."""
|
||||
self.logger.info('Engine.IO connection dropped')
|
||||
will_reconnect = self.reconnection and self.eio.state == 'connected'
|
||||
if self.connected:
|
||||
for n in self.namespaces:
|
||||
self._trigger_event('disconnect', namespace=n)
|
||||
if not will_reconnect:
|
||||
self._trigger_event('__disconnect_final', namespace=n)
|
||||
self.namespaces = {}
|
||||
self.connected = False
|
||||
self.callbacks = {}
|
||||
self._binary_packet = None
|
||||
self.sid = None
|
||||
if will_reconnect:
|
||||
self._reconnect_task = self.start_background_task(
|
||||
self._handle_reconnect)
|
||||
|
||||
def _engineio_client_class(self):
|
||||
return engineio.Client
|
||||
38
venv/lib/python3.12/site-packages/socketio/exceptions.py
Normal file
38
venv/lib/python3.12/site-packages/socketio/exceptions.py
Normal file
@ -0,0 +1,38 @@
|
||||
class SocketIOError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ConnectionError(SocketIOError):
|
||||
pass
|
||||
|
||||
|
||||
class ConnectionRefusedError(ConnectionError):
|
||||
"""Connection refused exception.
|
||||
|
||||
This exception can be raised from a connect handler when the connection
|
||||
is not accepted. The positional arguments provided with the exception are
|
||||
returned with the error packet to the client.
|
||||
"""
|
||||
def __init__(self, *args):
|
||||
if len(args) == 0:
|
||||
self.error_args = {'message': 'Connection rejected by server'}
|
||||
elif len(args) == 1:
|
||||
self.error_args = {'message': str(args[0])}
|
||||
else:
|
||||
self.error_args = {'message': str(args[0])}
|
||||
if len(args) == 2:
|
||||
self.error_args['data'] = args[1]
|
||||
else:
|
||||
self.error_args['data'] = args[1:]
|
||||
|
||||
|
||||
class TimeoutError(SocketIOError):
|
||||
pass
|
||||
|
||||
|
||||
class BadNamespaceError(SocketIOError):
|
||||
pass
|
||||
|
||||
|
||||
class DisconnectedError(SocketIOError):
|
||||
pass
|
||||
66
venv/lib/python3.12/site-packages/socketio/kafka_manager.py
Normal file
66
venv/lib/python3.12/site-packages/socketio/kafka_manager.py
Normal file
@ -0,0 +1,66 @@
|
||||
import logging
|
||||
import pickle
|
||||
|
||||
try:
|
||||
import kafka
|
||||
except ImportError:
|
||||
kafka = None
|
||||
|
||||
from .pubsub_manager import PubSubManager
|
||||
|
||||
logger = logging.getLogger('socketio')
|
||||
|
||||
|
||||
class KafkaManager(PubSubManager): # pragma: no cover
|
||||
"""Kafka based client manager.
|
||||
|
||||
This class implements a Kafka backend for event sharing across multiple
|
||||
processes.
|
||||
|
||||
To use a Kafka backend, initialize the :class:`Server` instance as
|
||||
follows::
|
||||
|
||||
url = 'kafka://hostname:port'
|
||||
server = socketio.Server(client_manager=socketio.KafkaManager(url))
|
||||
|
||||
:param url: The connection URL for the Kafka server. For a default Kafka
|
||||
store running on the same host, use ``kafka://``. For a highly
|
||||
available deployment of Kafka, pass a list with all the
|
||||
connection URLs available in your cluster.
|
||||
:param channel: The channel name (topic) on which the server sends and
|
||||
receives notifications. Must be the same in all the
|
||||
servers.
|
||||
:param write_only: If set to ``True``, only initialize to emit events. The
|
||||
default of ``False`` initializes the class for emitting
|
||||
and receiving.
|
||||
"""
|
||||
name = 'kafka'
|
||||
|
||||
def __init__(self, url='kafka://localhost:9092', channel='socketio',
|
||||
write_only=False):
|
||||
if kafka is None:
|
||||
raise RuntimeError('kafka-python package is not installed '
|
||||
'(Run "pip install kafka-python" in your '
|
||||
'virtualenv).')
|
||||
|
||||
super().__init__(channel=channel, write_only=write_only)
|
||||
|
||||
urls = [url] if isinstance(url, str) else url
|
||||
self.kafka_urls = [url[8:] if url != 'kafka://' else 'localhost:9092'
|
||||
for url in urls]
|
||||
self.producer = kafka.KafkaProducer(bootstrap_servers=self.kafka_urls)
|
||||
self.consumer = kafka.KafkaConsumer(self.channel,
|
||||
bootstrap_servers=self.kafka_urls)
|
||||
|
||||
def _publish(self, data):
|
||||
self.producer.send(self.channel, value=pickle.dumps(data))
|
||||
self.producer.flush()
|
||||
|
||||
def _kafka_listen(self):
|
||||
for message in self.consumer:
|
||||
yield message
|
||||
|
||||
def _listen(self):
|
||||
for message in self._kafka_listen():
|
||||
if message.topic == self.channel:
|
||||
yield pickle.loads(message.value)
|
||||
134
venv/lib/python3.12/site-packages/socketio/kombu_manager.py
Normal file
134
venv/lib/python3.12/site-packages/socketio/kombu_manager.py
Normal file
@ -0,0 +1,134 @@
|
||||
import pickle
|
||||
import time
|
||||
import uuid
|
||||
|
||||
try:
|
||||
import kombu
|
||||
except ImportError:
|
||||
kombu = None
|
||||
|
||||
from .pubsub_manager import PubSubManager
|
||||
|
||||
|
||||
class KombuManager(PubSubManager): # pragma: no cover
|
||||
"""Client manager that uses kombu for inter-process messaging.
|
||||
|
||||
This class implements a client manager backend for event sharing across
|
||||
multiple processes, using RabbitMQ, Redis or any other messaging mechanism
|
||||
supported by `kombu <http://kombu.readthedocs.org/en/latest/>`_.
|
||||
|
||||
To use a kombu backend, initialize the :class:`Server` instance as
|
||||
follows::
|
||||
|
||||
url = 'amqp://user:password@hostname:port//'
|
||||
server = socketio.Server(client_manager=socketio.KombuManager(url))
|
||||
|
||||
:param url: The connection URL for the backend messaging queue. Example
|
||||
connection URLs are ``'amqp://guest:guest@localhost:5672//'``
|
||||
and ``'redis://localhost:6379/'`` for RabbitMQ and Redis
|
||||
respectively. Consult the `kombu documentation
|
||||
<http://kombu.readthedocs.org/en/latest/userguide\
|
||||
/connections.html#urls>`_ for more on how to construct
|
||||
connection URLs.
|
||||
:param channel: The channel name on which the server sends and receives
|
||||
notifications. Must be the same in all the servers.
|
||||
:param write_only: If set to ``True``, only initialize to emit events. The
|
||||
default of ``False`` initializes the class for emitting
|
||||
and receiving.
|
||||
:param connection_options: additional keyword arguments to be passed to
|
||||
``kombu.Connection()``.
|
||||
:param exchange_options: additional keyword arguments to be passed to
|
||||
``kombu.Exchange()``.
|
||||
:param queue_options: additional keyword arguments to be passed to
|
||||
``kombu.Queue()``.
|
||||
:param producer_options: additional keyword arguments to be passed to
|
||||
``kombu.Producer()``.
|
||||
"""
|
||||
name = 'kombu'
|
||||
|
||||
def __init__(self, url='amqp://guest:guest@localhost:5672//',
|
||||
channel='socketio', write_only=False, logger=None,
|
||||
connection_options=None, exchange_options=None,
|
||||
queue_options=None, producer_options=None):
|
||||
if kombu is None:
|
||||
raise RuntimeError('Kombu package is not installed '
|
||||
'(Run "pip install kombu" in your '
|
||||
'virtualenv).')
|
||||
super().__init__(channel=channel, write_only=write_only, logger=logger)
|
||||
self.url = url
|
||||
self.connection_options = connection_options or {}
|
||||
self.exchange_options = exchange_options or {}
|
||||
self.queue_options = queue_options or {}
|
||||
self.producer_options = producer_options or {}
|
||||
self.publisher_connection = self._connection()
|
||||
|
||||
def initialize(self):
|
||||
super().initialize()
|
||||
|
||||
monkey_patched = True
|
||||
if self.server.async_mode == 'eventlet':
|
||||
from eventlet.patcher import is_monkey_patched
|
||||
monkey_patched = is_monkey_patched('socket')
|
||||
elif 'gevent' in self.server.async_mode:
|
||||
from gevent.monkey import is_module_patched
|
||||
monkey_patched = is_module_patched('socket')
|
||||
if not monkey_patched:
|
||||
raise RuntimeError(
|
||||
'Kombu requires a monkey patched socket library to work '
|
||||
'with ' + self.server.async_mode)
|
||||
|
||||
def _connection(self):
|
||||
return kombu.Connection(self.url, **self.connection_options)
|
||||
|
||||
def _exchange(self):
|
||||
options = {'type': 'fanout', 'durable': False}
|
||||
options.update(self.exchange_options)
|
||||
return kombu.Exchange(self.channel, **options)
|
||||
|
||||
def _queue(self):
|
||||
queue_name = 'flask-socketio.' + str(uuid.uuid4())
|
||||
options = {'durable': False, 'queue_arguments': {'x-expires': 300000}}
|
||||
options.update(self.queue_options)
|
||||
return kombu.Queue(queue_name, self._exchange(), **options)
|
||||
|
||||
def _producer_publish(self, connection):
|
||||
producer = connection.Producer(exchange=self._exchange(),
|
||||
**self.producer_options)
|
||||
return connection.ensure(producer, producer.publish)
|
||||
|
||||
def _publish(self, data):
|
||||
retry = True
|
||||
while True:
|
||||
try:
|
||||
producer_publish = self._producer_publish(
|
||||
self.publisher_connection)
|
||||
producer_publish(pickle.dumps(data))
|
||||
break
|
||||
except (OSError, kombu.exceptions.KombuError):
|
||||
if retry:
|
||||
self._get_logger().error('Cannot publish to rabbitmq... '
|
||||
'retrying')
|
||||
retry = False
|
||||
else:
|
||||
self._get_logger().error(
|
||||
'Cannot publish to rabbitmq... giving up')
|
||||
break
|
||||
|
||||
def _listen(self):
|
||||
reader_queue = self._queue()
|
||||
retry_sleep = 1
|
||||
while True:
|
||||
try:
|
||||
with self._connection() as connection:
|
||||
with connection.SimpleQueue(reader_queue) as queue:
|
||||
while True:
|
||||
message = queue.get(block=True)
|
||||
message.ack()
|
||||
yield message.payload
|
||||
retry_sleep = 1
|
||||
except (OSError, kombu.exceptions.KombuError):
|
||||
self._get_logger().error(
|
||||
'Cannot receive from rabbitmq... '
|
||||
'retrying in {} secs'.format(retry_sleep))
|
||||
time.sleep(retry_sleep)
|
||||
retry_sleep = min(retry_sleep * 2, 60)
|
||||
93
venv/lib/python3.12/site-packages/socketio/manager.py
Normal file
93
venv/lib/python3.12/site-packages/socketio/manager.py
Normal file
@ -0,0 +1,93 @@
|
||||
import logging
|
||||
|
||||
from engineio import packet as eio_packet
|
||||
from . import base_manager
|
||||
from . import packet
|
||||
|
||||
default_logger = logging.getLogger('socketio')
|
||||
|
||||
|
||||
class Manager(base_manager.BaseManager):
|
||||
"""Manage client connections.
|
||||
|
||||
This class keeps track of all the clients and the rooms they are in, to
|
||||
support the broadcasting of messages. The data used by this class is
|
||||
stored in a memory structure, making it appropriate only for single process
|
||||
services. More sophisticated storage backends can be implemented by
|
||||
subclasses.
|
||||
"""
|
||||
def can_disconnect(self, sid, namespace):
|
||||
return self.is_connected(sid, namespace)
|
||||
|
||||
def emit(self, event, data, namespace, room=None, skip_sid=None,
|
||||
callback=None, to=None, **kwargs):
|
||||
"""Emit a message to a single client, a room, or all the clients
|
||||
connected to the namespace."""
|
||||
room = to or room
|
||||
if namespace not in self.rooms:
|
||||
return
|
||||
if isinstance(data, tuple):
|
||||
# tuples are expanded to multiple arguments, everything else is
|
||||
# sent as a single argument
|
||||
data = list(data)
|
||||
elif data is not None:
|
||||
data = [data]
|
||||
else:
|
||||
data = []
|
||||
if not isinstance(skip_sid, list):
|
||||
skip_sid = [skip_sid]
|
||||
if not callback:
|
||||
# when callbacks aren't used the packets sent to each recipient are
|
||||
# identical, so they can be generated once and reused
|
||||
pkt = self.server.packet_class(
|
||||
packet.EVENT, namespace=namespace, data=[event] + data)
|
||||
encoded_packet = pkt.encode()
|
||||
if not isinstance(encoded_packet, list):
|
||||
encoded_packet = [encoded_packet]
|
||||
eio_pkt = [eio_packet.Packet(eio_packet.MESSAGE, p)
|
||||
for p in encoded_packet]
|
||||
for sid, eio_sid in self.get_participants(namespace, room):
|
||||
if sid not in skip_sid:
|
||||
for p in eio_pkt:
|
||||
self.server._send_eio_packet(eio_sid, p)
|
||||
else:
|
||||
# callbacks are used, so each recipient must be sent a packet that
|
||||
# contains a unique callback id
|
||||
# note that callbacks when addressing a group of people are
|
||||
# implemented but not tested or supported
|
||||
for sid, eio_sid in self.get_participants(namespace, room):
|
||||
if sid not in skip_sid: # pragma: no branch
|
||||
id = self._generate_ack_id(sid, callback)
|
||||
pkt = self.server.packet_class(
|
||||
packet.EVENT, namespace=namespace, data=[event] + data,
|
||||
id=id)
|
||||
self.server._send_packet(eio_sid, pkt)
|
||||
|
||||
def disconnect(self, sid, namespace, **kwargs):
|
||||
"""Register a client disconnect from a namespace."""
|
||||
return self.basic_disconnect(sid, namespace)
|
||||
|
||||
def enter_room(self, sid, namespace, room, eio_sid=None):
|
||||
"""Add a client to a room."""
|
||||
return self.basic_enter_room(sid, namespace, room, eio_sid=eio_sid)
|
||||
|
||||
def leave_room(self, sid, namespace, room):
|
||||
"""Remove a client from a room."""
|
||||
return self.basic_leave_room(sid, namespace, room)
|
||||
|
||||
def close_room(self, room, namespace):
|
||||
"""Remove all participants from a room."""
|
||||
return self.basic_close_room(room, namespace)
|
||||
|
||||
def trigger_callback(self, sid, id, data):
|
||||
"""Invoke an application callback."""
|
||||
callback = None
|
||||
try:
|
||||
callback = self.callbacks[sid][id]
|
||||
except KeyError:
|
||||
# if we get an unknown callback we just ignore it
|
||||
self._get_logger().warning('Unknown callback received, ignoring.')
|
||||
else:
|
||||
del self.callbacks[sid][id]
|
||||
if callback is not None:
|
||||
callback(*data)
|
||||
40
venv/lib/python3.12/site-packages/socketio/middleware.py
Normal file
40
venv/lib/python3.12/site-packages/socketio/middleware.py
Normal file
@ -0,0 +1,40 @@
|
||||
import engineio
|
||||
|
||||
|
||||
class WSGIApp(engineio.WSGIApp):
|
||||
"""WSGI middleware for Socket.IO.
|
||||
|
||||
This middleware dispatches traffic to a Socket.IO application. It can also
|
||||
serve a list of static files to the client, or forward unrelated HTTP
|
||||
traffic to another WSGI application.
|
||||
|
||||
:param socketio_app: The Socket.IO server. Must be an instance of the
|
||||
``socketio.Server`` class.
|
||||
:param wsgi_app: The WSGI app that receives all other traffic.
|
||||
:param static_files: A dictionary with static file mapping rules. See the
|
||||
documentation for details on this argument.
|
||||
:param socketio_path: The endpoint where the Socket.IO application should
|
||||
be installed. The default value is appropriate for
|
||||
most cases.
|
||||
|
||||
Example usage::
|
||||
|
||||
import socketio
|
||||
import eventlet
|
||||
from . import wsgi_app
|
||||
|
||||
sio = socketio.Server()
|
||||
app = socketio.WSGIApp(sio, wsgi_app)
|
||||
eventlet.wsgi.server(eventlet.listen(('', 8000)), app)
|
||||
"""
|
||||
def __init__(self, socketio_app, wsgi_app=None, static_files=None,
|
||||
socketio_path='socket.io'):
|
||||
super().__init__(socketio_app, wsgi_app, static_files=static_files,
|
||||
engineio_path=socketio_path)
|
||||
|
||||
|
||||
class Middleware(WSGIApp):
|
||||
"""This class has been renamed to WSGIApp and is now deprecated."""
|
||||
def __init__(self, socketio_app, wsgi_app=None,
|
||||
socketio_path='socket.io'):
|
||||
super().__init__(socketio_app, wsgi_app, socketio_path=socketio_path)
|
||||
18
venv/lib/python3.12/site-packages/socketio/msgpack_packet.py
Normal file
18
venv/lib/python3.12/site-packages/socketio/msgpack_packet.py
Normal file
@ -0,0 +1,18 @@
|
||||
import msgpack
|
||||
from . import packet
|
||||
|
||||
|
||||
class MsgPackPacket(packet.Packet):
|
||||
uses_binary_events = False
|
||||
|
||||
def encode(self):
|
||||
"""Encode the packet for transmission."""
|
||||
return msgpack.dumps(self._to_dict())
|
||||
|
||||
def decode(self, encoded_packet):
|
||||
"""Decode a transmitted package."""
|
||||
decoded = msgpack.loads(encoded_packet)
|
||||
self.packet_type = decoded['type']
|
||||
self.data = decoded.get('data')
|
||||
self.id = decoded.get('id')
|
||||
self.namespace = decoded['nsp']
|
||||
198
venv/lib/python3.12/site-packages/socketio/namespace.py
Normal file
198
venv/lib/python3.12/site-packages/socketio/namespace.py
Normal file
@ -0,0 +1,198 @@
|
||||
from . import base_namespace
|
||||
|
||||
|
||||
class Namespace(base_namespace.BaseServerNamespace):
|
||||
"""Base class for server-side class-based namespaces.
|
||||
|
||||
A class-based namespace is a class that contains all the event handlers
|
||||
for a Socket.IO namespace. The event handlers are methods of the class
|
||||
with the prefix ``on_``, such as ``on_connect``, ``on_disconnect``,
|
||||
``on_message``, ``on_json``, and so on.
|
||||
|
||||
:param namespace: The Socket.IO namespace to be used with all the event
|
||||
handlers defined in this class. If this argument is
|
||||
omitted, the default namespace is used.
|
||||
"""
|
||||
def trigger_event(self, event, *args):
|
||||
"""Dispatch an event to the proper handler method.
|
||||
|
||||
In the most common usage, this method is not overloaded by subclasses,
|
||||
as it performs the routing of events to methods. However, this
|
||||
method can be overridden if special dispatching rules are needed, or if
|
||||
having a single method that catches all events is desired.
|
||||
"""
|
||||
handler_name = 'on_' + (event or '')
|
||||
if hasattr(self, handler_name):
|
||||
return getattr(self, handler_name)(*args)
|
||||
|
||||
def emit(self, event, data=None, to=None, room=None, skip_sid=None,
|
||||
namespace=None, callback=None, ignore_queue=False):
|
||||
"""Emit a custom event to one or more connected clients.
|
||||
|
||||
The only difference with the :func:`socketio.Server.emit` method is
|
||||
that when the ``namespace`` argument is not given the namespace
|
||||
associated with the class is used.
|
||||
"""
|
||||
return self.server.emit(event, data=data, to=to, room=room,
|
||||
skip_sid=skip_sid,
|
||||
namespace=namespace or self.namespace,
|
||||
callback=callback, ignore_queue=ignore_queue)
|
||||
|
||||
def send(self, data, to=None, room=None, skip_sid=None, namespace=None,
|
||||
callback=None, ignore_queue=False):
|
||||
"""Send a message to one or more connected clients.
|
||||
|
||||
The only difference with the :func:`socketio.Server.send` method is
|
||||
that when the ``namespace`` argument is not given the namespace
|
||||
associated with the class is used.
|
||||
"""
|
||||
return self.server.send(data, to=to, room=room, skip_sid=skip_sid,
|
||||
namespace=namespace or self.namespace,
|
||||
callback=callback, ignore_queue=ignore_queue)
|
||||
|
||||
def call(self, event, data=None, to=None, sid=None, namespace=None,
|
||||
timeout=None, ignore_queue=False):
|
||||
"""Emit a custom event to a client and wait for the response.
|
||||
|
||||
The only difference with the :func:`socketio.Server.call` method is
|
||||
that when the ``namespace`` argument is not given the namespace
|
||||
associated with the class is used.
|
||||
"""
|
||||
return self.server.call(event, data=data, to=to, sid=sid,
|
||||
namespace=namespace or self.namespace,
|
||||
timeout=timeout, ignore_queue=ignore_queue)
|
||||
|
||||
def enter_room(self, sid, room, namespace=None):
|
||||
"""Enter a room.
|
||||
|
||||
The only difference with the :func:`socketio.Server.enter_room` method
|
||||
is that when the ``namespace`` argument is not given the namespace
|
||||
associated with the class is used.
|
||||
"""
|
||||
return self.server.enter_room(sid, room,
|
||||
namespace=namespace or self.namespace)
|
||||
|
||||
def leave_room(self, sid, room, namespace=None):
|
||||
"""Leave a room.
|
||||
|
||||
The only difference with the :func:`socketio.Server.leave_room` method
|
||||
is that when the ``namespace`` argument is not given the namespace
|
||||
associated with the class is used.
|
||||
"""
|
||||
return self.server.leave_room(sid, room,
|
||||
namespace=namespace or self.namespace)
|
||||
|
||||
def close_room(self, room, namespace=None):
|
||||
"""Close a room.
|
||||
|
||||
The only difference with the :func:`socketio.Server.close_room` method
|
||||
is that when the ``namespace`` argument is not given the namespace
|
||||
associated with the class is used.
|
||||
"""
|
||||
return self.server.close_room(room,
|
||||
namespace=namespace or self.namespace)
|
||||
|
||||
def get_session(self, sid, namespace=None):
|
||||
"""Return the user session for a client.
|
||||
|
||||
The only difference with the :func:`socketio.Server.get_session`
|
||||
method is that when the ``namespace`` argument is not given the
|
||||
namespace associated with the class is used.
|
||||
"""
|
||||
return self.server.get_session(
|
||||
sid, namespace=namespace or self.namespace)
|
||||
|
||||
def save_session(self, sid, session, namespace=None):
|
||||
"""Store the user session for a client.
|
||||
|
||||
The only difference with the :func:`socketio.Server.save_session`
|
||||
method is that when the ``namespace`` argument is not given the
|
||||
namespace associated with the class is used.
|
||||
"""
|
||||
return self.server.save_session(
|
||||
sid, session, namespace=namespace or self.namespace)
|
||||
|
||||
def session(self, sid, namespace=None):
|
||||
"""Return the user session for a client with context manager syntax.
|
||||
|
||||
The only difference with the :func:`socketio.Server.session` method is
|
||||
that when the ``namespace`` argument is not given the namespace
|
||||
associated with the class is used.
|
||||
"""
|
||||
return self.server.session(sid, namespace=namespace or self.namespace)
|
||||
|
||||
def disconnect(self, sid, namespace=None):
|
||||
"""Disconnect a client.
|
||||
|
||||
The only difference with the :func:`socketio.Server.disconnect` method
|
||||
is that when the ``namespace`` argument is not given the namespace
|
||||
associated with the class is used.
|
||||
"""
|
||||
return self.server.disconnect(sid,
|
||||
namespace=namespace or self.namespace)
|
||||
|
||||
|
||||
class ClientNamespace(base_namespace.BaseClientNamespace):
|
||||
"""Base class for client-side class-based namespaces.
|
||||
|
||||
A class-based namespace is a class that contains all the event handlers
|
||||
for a Socket.IO namespace. The event handlers are methods of the class
|
||||
with the prefix ``on_``, such as ``on_connect``, ``on_disconnect``,
|
||||
``on_message``, ``on_json``, and so on.
|
||||
|
||||
:param namespace: The Socket.IO namespace to be used with all the event
|
||||
handlers defined in this class. If this argument is
|
||||
omitted, the default namespace is used.
|
||||
"""
|
||||
def trigger_event(self, event, *args):
|
||||
"""Dispatch an event to the proper handler method.
|
||||
|
||||
In the most common usage, this method is not overloaded by subclasses,
|
||||
as it performs the routing of events to methods. However, this
|
||||
method can be overridden if special dispatching rules are needed, or if
|
||||
having a single method that catches all events is desired.
|
||||
"""
|
||||
handler_name = 'on_' + (event or '')
|
||||
if hasattr(self, handler_name):
|
||||
return getattr(self, handler_name)(*args)
|
||||
|
||||
def emit(self, event, data=None, namespace=None, callback=None):
|
||||
"""Emit a custom event to the server.
|
||||
|
||||
The only difference with the :func:`socketio.Client.emit` method is
|
||||
that when the ``namespace`` argument is not given the namespace
|
||||
associated with the class is used.
|
||||
"""
|
||||
return self.client.emit(event, data=data,
|
||||
namespace=namespace or self.namespace,
|
||||
callback=callback)
|
||||
|
||||
def send(self, data, room=None, namespace=None, callback=None):
|
||||
"""Send a message to the server.
|
||||
|
||||
The only difference with the :func:`socketio.Client.send` method is
|
||||
that when the ``namespace`` argument is not given the namespace
|
||||
associated with the class is used.
|
||||
"""
|
||||
return self.client.send(data, namespace=namespace or self.namespace,
|
||||
callback=callback)
|
||||
|
||||
def call(self, event, data=None, namespace=None, timeout=None):
|
||||
"""Emit a custom event to the server and wait for the response.
|
||||
|
||||
The only difference with the :func:`socketio.Client.call` method is
|
||||
that when the ``namespace`` argument is not given the namespace
|
||||
associated with the class is used.
|
||||
"""
|
||||
return self.client.call(event, data=data,
|
||||
namespace=namespace or self.namespace,
|
||||
timeout=timeout)
|
||||
|
||||
def disconnect(self):
|
||||
"""Disconnect from the server.
|
||||
|
||||
The only difference with the :func:`socketio.Client.disconnect` method
|
||||
is that when the ``namespace`` argument is not given the namespace
|
||||
associated with the class is used.
|
||||
"""
|
||||
return self.client.disconnect()
|
||||
190
venv/lib/python3.12/site-packages/socketio/packet.py
Normal file
190
venv/lib/python3.12/site-packages/socketio/packet.py
Normal file
@ -0,0 +1,190 @@
|
||||
import functools
|
||||
from engineio import json as _json
|
||||
|
||||
(CONNECT, DISCONNECT, EVENT, ACK, CONNECT_ERROR, BINARY_EVENT, BINARY_ACK) = \
|
||||
(0, 1, 2, 3, 4, 5, 6)
|
||||
packet_names = ['CONNECT', 'DISCONNECT', 'EVENT', 'ACK', 'CONNECT_ERROR',
|
||||
'BINARY_EVENT', 'BINARY_ACK']
|
||||
|
||||
|
||||
class Packet(object):
|
||||
"""Socket.IO packet."""
|
||||
|
||||
# the format of the Socket.IO packet is as follows:
|
||||
#
|
||||
# packet type: 1 byte, values 0-6
|
||||
# num_attachments: ASCII encoded, only if num_attachments != 0
|
||||
# '-': only if num_attachments != 0
|
||||
# namespace, followed by a ',': only if namespace != '/'
|
||||
# id: ASCII encoded, only if id is not None
|
||||
# data: JSON dump of data payload
|
||||
|
||||
uses_binary_events = True
|
||||
json = _json
|
||||
|
||||
def __init__(self, packet_type=EVENT, data=None, namespace=None, id=None,
|
||||
binary=None, encoded_packet=None):
|
||||
self.packet_type = packet_type
|
||||
self.data = data
|
||||
self.namespace = namespace
|
||||
self.id = id
|
||||
if self.uses_binary_events and \
|
||||
(binary or (binary is None and self._data_is_binary(
|
||||
self.data))):
|
||||
if self.packet_type == EVENT:
|
||||
self.packet_type = BINARY_EVENT
|
||||
elif self.packet_type == ACK:
|
||||
self.packet_type = BINARY_ACK
|
||||
else:
|
||||
raise ValueError('Packet does not support binary payload.')
|
||||
self.attachment_count = 0
|
||||
self.attachments = []
|
||||
if encoded_packet:
|
||||
self.attachment_count = self.decode(encoded_packet) or 0
|
||||
|
||||
def encode(self):
|
||||
"""Encode the packet for transmission.
|
||||
|
||||
If the packet contains binary elements, this function returns a list
|
||||
of packets where the first is the original packet with placeholders for
|
||||
the binary components and the remaining ones the binary attachments.
|
||||
"""
|
||||
encoded_packet = str(self.packet_type)
|
||||
if self.packet_type == BINARY_EVENT or self.packet_type == BINARY_ACK:
|
||||
data, attachments = self._deconstruct_binary(self.data)
|
||||
encoded_packet += str(len(attachments)) + '-'
|
||||
else:
|
||||
data = self.data
|
||||
attachments = None
|
||||
if self.namespace is not None and self.namespace != '/':
|
||||
encoded_packet += self.namespace + ','
|
||||
if self.id is not None:
|
||||
encoded_packet += str(self.id)
|
||||
if data is not None:
|
||||
encoded_packet += self.json.dumps(data, separators=(',', ':'))
|
||||
if attachments is not None:
|
||||
encoded_packet = [encoded_packet] + attachments
|
||||
return encoded_packet
|
||||
|
||||
def decode(self, encoded_packet):
|
||||
"""Decode a transmitted package.
|
||||
|
||||
The return value indicates how many binary attachment packets are
|
||||
necessary to fully decode the packet.
|
||||
"""
|
||||
ep = encoded_packet
|
||||
try:
|
||||
self.packet_type = int(ep[0:1])
|
||||
except TypeError:
|
||||
self.packet_type = ep
|
||||
ep = ''
|
||||
self.namespace = None
|
||||
self.data = None
|
||||
ep = ep[1:]
|
||||
dash = ep.find('-')
|
||||
attachment_count = 0
|
||||
if dash > 0 and ep[0:dash].isdigit():
|
||||
if dash > 10:
|
||||
raise ValueError('too many attachments')
|
||||
attachment_count = int(ep[0:dash])
|
||||
ep = ep[dash + 1:]
|
||||
if ep and ep[0:1] == '/':
|
||||
sep = ep.find(',')
|
||||
if sep == -1:
|
||||
self.namespace = ep
|
||||
ep = ''
|
||||
else:
|
||||
self.namespace = ep[0:sep]
|
||||
ep = ep[sep + 1:]
|
||||
q = self.namespace.find('?')
|
||||
if q != -1:
|
||||
self.namespace = self.namespace[0:q]
|
||||
if ep and ep[0].isdigit():
|
||||
i = 1
|
||||
end = len(ep)
|
||||
while i < end:
|
||||
if not ep[i].isdigit() or i >= 100:
|
||||
break
|
||||
i += 1
|
||||
self.id = int(ep[:i])
|
||||
ep = ep[i:]
|
||||
if len(ep) > 0 and ep[0].isdigit():
|
||||
raise ValueError('id field is too long')
|
||||
if ep:
|
||||
self.data = self.json.loads(ep)
|
||||
return attachment_count
|
||||
|
||||
def add_attachment(self, attachment):
|
||||
if self.attachment_count <= len(self.attachments):
|
||||
raise ValueError('Unexpected binary attachment')
|
||||
self.attachments.append(attachment)
|
||||
if self.attachment_count == len(self.attachments):
|
||||
self.reconstruct_binary(self.attachments)
|
||||
return True
|
||||
return False
|
||||
|
||||
def reconstruct_binary(self, attachments):
|
||||
"""Reconstruct a decoded packet using the given list of binary
|
||||
attachments.
|
||||
"""
|
||||
self.data = self._reconstruct_binary_internal(self.data,
|
||||
self.attachments)
|
||||
|
||||
def _reconstruct_binary_internal(self, data, attachments):
|
||||
if isinstance(data, list):
|
||||
return [self._reconstruct_binary_internal(item, attachments)
|
||||
for item in data]
|
||||
elif isinstance(data, dict):
|
||||
if data.get('_placeholder') and 'num' in data:
|
||||
return attachments[data['num']]
|
||||
else:
|
||||
return {key: self._reconstruct_binary_internal(value,
|
||||
attachments)
|
||||
for key, value in data.items()}
|
||||
else:
|
||||
return data
|
||||
|
||||
def _deconstruct_binary(self, data):
|
||||
"""Extract binary components in the packet."""
|
||||
attachments = []
|
||||
data = self._deconstruct_binary_internal(data, attachments)
|
||||
return data, attachments
|
||||
|
||||
def _deconstruct_binary_internal(self, data, attachments):
|
||||
if isinstance(data, bytes):
|
||||
attachments.append(data)
|
||||
return {'_placeholder': True, 'num': len(attachments) - 1}
|
||||
elif isinstance(data, list):
|
||||
return [self._deconstruct_binary_internal(item, attachments)
|
||||
for item in data]
|
||||
elif isinstance(data, dict):
|
||||
return {key: self._deconstruct_binary_internal(value, attachments)
|
||||
for key, value in data.items()}
|
||||
else:
|
||||
return data
|
||||
|
||||
def _data_is_binary(self, data):
|
||||
"""Check if the data contains binary components."""
|
||||
if isinstance(data, bytes):
|
||||
return True
|
||||
elif isinstance(data, list):
|
||||
return functools.reduce(
|
||||
lambda a, b: a or b, [self._data_is_binary(item)
|
||||
for item in data], False)
|
||||
elif isinstance(data, dict):
|
||||
return functools.reduce(
|
||||
lambda a, b: a or b, [self._data_is_binary(item)
|
||||
for item in data.values()],
|
||||
False)
|
||||
else:
|
||||
return False
|
||||
|
||||
def _to_dict(self):
|
||||
d = {
|
||||
'type': self.packet_type,
|
||||
'data': self.data,
|
||||
'nsp': self.namespace,
|
||||
}
|
||||
if self.id is not None:
|
||||
d['id'] = self.id
|
||||
return d
|
||||
233
venv/lib/python3.12/site-packages/socketio/pubsub_manager.py
Normal file
233
venv/lib/python3.12/site-packages/socketio/pubsub_manager.py
Normal file
@ -0,0 +1,233 @@
|
||||
from functools import partial
|
||||
import uuid
|
||||
|
||||
from engineio import json
|
||||
import pickle
|
||||
|
||||
from .manager import Manager
|
||||
|
||||
|
||||
class PubSubManager(Manager):
|
||||
"""Manage a client list attached to a pub/sub backend.
|
||||
|
||||
This is a base class that enables multiple servers to share the list of
|
||||
clients, with the servers communicating events through a pub/sub backend.
|
||||
The use of a pub/sub backend also allows any client connected to the
|
||||
backend to emit events addressed to Socket.IO clients.
|
||||
|
||||
The actual backends must be implemented by subclasses, this class only
|
||||
provides a pub/sub generic framework.
|
||||
|
||||
:param channel: The channel name on which the server sends and receives
|
||||
notifications.
|
||||
"""
|
||||
name = 'pubsub'
|
||||
|
||||
def __init__(self, channel='socketio', write_only=False, logger=None):
|
||||
super().__init__()
|
||||
self.channel = channel
|
||||
self.write_only = write_only
|
||||
self.host_id = uuid.uuid4().hex
|
||||
self.logger = logger
|
||||
|
||||
def initialize(self):
|
||||
super().initialize()
|
||||
if not self.write_only:
|
||||
self.thread = self.server.start_background_task(self._thread)
|
||||
self._get_logger().info(self.name + ' backend initialized.')
|
||||
|
||||
def emit(self, event, data, namespace=None, room=None, skip_sid=None,
|
||||
callback=None, to=None, **kwargs):
|
||||
"""Emit a message to a single client, a room, or all the clients
|
||||
connected to the namespace.
|
||||
|
||||
This method takes care or propagating the message to all the servers
|
||||
that are connected through the message queue.
|
||||
|
||||
The parameters are the same as in :meth:`.Server.emit`.
|
||||
"""
|
||||
room = to or room
|
||||
if kwargs.get('ignore_queue'):
|
||||
return super().emit(
|
||||
event, data, namespace=namespace, room=room, skip_sid=skip_sid,
|
||||
callback=callback)
|
||||
namespace = namespace or '/'
|
||||
if callback is not None:
|
||||
if self.server is None:
|
||||
raise RuntimeError('Callbacks can only be issued from the '
|
||||
'context of a server.')
|
||||
if room is None:
|
||||
raise ValueError('Cannot use callback without a room set.')
|
||||
id = self._generate_ack_id(room, callback)
|
||||
callback = (room, namespace, id)
|
||||
else:
|
||||
callback = None
|
||||
message = {'method': 'emit', 'event': event, 'data': data,
|
||||
'namespace': namespace, 'room': room,
|
||||
'skip_sid': skip_sid, 'callback': callback,
|
||||
'host_id': self.host_id}
|
||||
self._handle_emit(message) # handle in this host
|
||||
self._publish(message) # notify other hosts
|
||||
|
||||
def can_disconnect(self, sid, namespace):
|
||||
if self.is_connected(sid, namespace):
|
||||
# client is in this server, so we can disconnect directly
|
||||
return super().can_disconnect(sid, namespace)
|
||||
else:
|
||||
# client is in another server, so we post request to the queue
|
||||
message = {'method': 'disconnect', 'sid': sid,
|
||||
'namespace': namespace or '/', 'host_id': self.host_id}
|
||||
self._handle_disconnect(message) # handle in this host
|
||||
self._publish(message) # notify other hosts
|
||||
|
||||
def disconnect(self, sid, namespace=None, **kwargs):
|
||||
if kwargs.get('ignore_queue'):
|
||||
return super().disconnect(sid, namespace=namespace)
|
||||
message = {'method': 'disconnect', 'sid': sid,
|
||||
'namespace': namespace or '/', 'host_id': self.host_id}
|
||||
self._handle_disconnect(message) # handle in this host
|
||||
self._publish(message) # notify other hosts
|
||||
|
||||
def enter_room(self, sid, namespace, room, eio_sid=None):
|
||||
if self.is_connected(sid, namespace):
|
||||
# client is in this server, so we can add to the room directly
|
||||
return super().enter_room(sid, namespace, room, eio_sid=eio_sid)
|
||||
else:
|
||||
message = {'method': 'enter_room', 'sid': sid, 'room': room,
|
||||
'namespace': namespace or '/', 'host_id': self.host_id}
|
||||
self._publish(message) # notify other hosts
|
||||
|
||||
def leave_room(self, sid, namespace, room):
|
||||
if self.is_connected(sid, namespace):
|
||||
# client is in this server, so we can remove from the room directly
|
||||
return super().leave_room(sid, namespace, room)
|
||||
else:
|
||||
message = {'method': 'leave_room', 'sid': sid, 'room': room,
|
||||
'namespace': namespace or '/', 'host_id': self.host_id}
|
||||
self._publish(message) # notify other hosts
|
||||
|
||||
def close_room(self, room, namespace=None):
|
||||
message = {'method': 'close_room', 'room': room,
|
||||
'namespace': namespace or '/', 'host_id': self.host_id}
|
||||
self._handle_close_room(message) # handle in this host
|
||||
self._publish(message) # notify other hosts
|
||||
|
||||
def _publish(self, data):
|
||||
"""Publish a message on the Socket.IO channel.
|
||||
|
||||
This method needs to be implemented by the different subclasses that
|
||||
support pub/sub backends.
|
||||
"""
|
||||
raise NotImplementedError('This method must be implemented in a '
|
||||
'subclass.') # pragma: no cover
|
||||
|
||||
def _listen(self):
|
||||
"""Return the next message published on the Socket.IO channel,
|
||||
blocking until a message is available.
|
||||
|
||||
This method needs to be implemented by the different subclasses that
|
||||
support pub/sub backends.
|
||||
"""
|
||||
raise NotImplementedError('This method must be implemented in a '
|
||||
'subclass.') # pragma: no cover
|
||||
|
||||
def _handle_emit(self, message):
|
||||
# Events with callbacks are very tricky to handle across hosts
|
||||
# Here in the receiving end we set up a local callback that preserves
|
||||
# the callback host and id from the sender
|
||||
remote_callback = message.get('callback')
|
||||
remote_host_id = message.get('host_id')
|
||||
if remote_callback is not None and len(remote_callback) == 3:
|
||||
callback = partial(self._return_callback, remote_host_id,
|
||||
*remote_callback)
|
||||
else:
|
||||
callback = None
|
||||
super().emit(message['event'], message['data'],
|
||||
namespace=message.get('namespace'),
|
||||
room=message.get('room'),
|
||||
skip_sid=message.get('skip_sid'), callback=callback)
|
||||
|
||||
def _handle_callback(self, message):
|
||||
if self.host_id == message.get('host_id'):
|
||||
try:
|
||||
sid = message['sid']
|
||||
id = message['id']
|
||||
args = message['args']
|
||||
except KeyError:
|
||||
return
|
||||
self.trigger_callback(sid, id, args)
|
||||
|
||||
def _return_callback(self, host_id, sid, namespace, callback_id, *args):
|
||||
# When an event callback is received, the callback is returned back
|
||||
# to the sender, which is identified by the host_id
|
||||
if host_id == self.host_id:
|
||||
self.trigger_callback(sid, callback_id, args)
|
||||
else:
|
||||
self._publish({'method': 'callback', 'host_id': host_id,
|
||||
'sid': sid, 'namespace': namespace,
|
||||
'id': callback_id, 'args': args})
|
||||
|
||||
def _handle_disconnect(self, message):
|
||||
self.server.disconnect(sid=message.get('sid'),
|
||||
namespace=message.get('namespace'),
|
||||
ignore_queue=True)
|
||||
|
||||
def _handle_enter_room(self, message):
|
||||
sid = message.get('sid')
|
||||
namespace = message.get('namespace')
|
||||
if self.is_connected(sid, namespace):
|
||||
super().enter_room(sid, namespace, message.get('room'))
|
||||
|
||||
def _handle_leave_room(self, message):
|
||||
sid = message.get('sid')
|
||||
namespace = message.get('namespace')
|
||||
if self.is_connected(sid, namespace):
|
||||
super().leave_room(sid, namespace, message.get('room'))
|
||||
|
||||
def _handle_close_room(self, message):
|
||||
super().close_room(room=message.get('room'),
|
||||
namespace=message.get('namespace'))
|
||||
|
||||
def _thread(self):
|
||||
while True:
|
||||
try:
|
||||
for message in self._listen():
|
||||
data = None
|
||||
if isinstance(message, dict):
|
||||
data = message
|
||||
else:
|
||||
if isinstance(message, bytes): # pragma: no cover
|
||||
try:
|
||||
data = pickle.loads(message)
|
||||
except:
|
||||
pass
|
||||
if data is None:
|
||||
try:
|
||||
data = json.loads(message)
|
||||
except:
|
||||
pass
|
||||
if data and 'method' in data:
|
||||
self._get_logger().debug('pubsub message: {}'.format(
|
||||
data['method']))
|
||||
try:
|
||||
if data['method'] == 'callback':
|
||||
self._handle_callback(data)
|
||||
elif data.get('host_id') != self.host_id:
|
||||
if data['method'] == 'emit':
|
||||
self._handle_emit(data)
|
||||
elif data['method'] == 'disconnect':
|
||||
self._handle_disconnect(data)
|
||||
elif data['method'] == 'enter_room':
|
||||
self._handle_enter_room(data)
|
||||
elif data['method'] == 'leave_room':
|
||||
self._handle_leave_room(data)
|
||||
elif data['method'] == 'close_room':
|
||||
self._handle_close_room(data)
|
||||
except Exception:
|
||||
self.server.logger.exception(
|
||||
'Handler error in pubsub listening thread')
|
||||
self.server.logger.error('pubsub listen() exited unexpectedly')
|
||||
break # loop should never exit except in unit tests!
|
||||
except Exception: # pragma: no cover
|
||||
self.server.logger.exception('Unexpected Error in pubsub '
|
||||
'listening thread')
|
||||
115
venv/lib/python3.12/site-packages/socketio/redis_manager.py
Normal file
115
venv/lib/python3.12/site-packages/socketio/redis_manager.py
Normal file
@ -0,0 +1,115 @@
|
||||
import logging
|
||||
import pickle
|
||||
import time
|
||||
|
||||
try:
|
||||
import redis
|
||||
except ImportError:
|
||||
redis = None
|
||||
|
||||
from .pubsub_manager import PubSubManager
|
||||
|
||||
logger = logging.getLogger('socketio')
|
||||
|
||||
|
||||
class RedisManager(PubSubManager): # pragma: no cover
|
||||
"""Redis based client manager.
|
||||
|
||||
This class implements a Redis backend for event sharing across multiple
|
||||
processes. Only kept here as one more example of how to build a custom
|
||||
backend, since the kombu backend is perfectly adequate to support a Redis
|
||||
message queue.
|
||||
|
||||
To use a Redis backend, initialize the :class:`Server` instance as
|
||||
follows::
|
||||
|
||||
url = 'redis://hostname:port/0'
|
||||
server = socketio.Server(client_manager=socketio.RedisManager(url))
|
||||
|
||||
:param url: The connection URL for the Redis server. For a default Redis
|
||||
store running on the same host, use ``redis://``. To use an
|
||||
SSL connection, use ``rediss://``.
|
||||
:param channel: The channel name on which the server sends and receives
|
||||
notifications. Must be the same in all the servers.
|
||||
:param write_only: If set to ``True``, only initialize to emit events. The
|
||||
default of ``False`` initializes the class for emitting
|
||||
and receiving.
|
||||
:param redis_options: additional keyword arguments to be passed to
|
||||
``Redis.from_url()``.
|
||||
"""
|
||||
name = 'redis'
|
||||
|
||||
def __init__(self, url='redis://localhost:6379/0', channel='socketio',
|
||||
write_only=False, logger=None, redis_options=None):
|
||||
if redis is None:
|
||||
raise RuntimeError('Redis package is not installed '
|
||||
'(Run "pip install redis" in your '
|
||||
'virtualenv).')
|
||||
self.redis_url = url
|
||||
self.redis_options = redis_options or {}
|
||||
self._redis_connect()
|
||||
super().__init__(channel=channel, write_only=write_only, logger=logger)
|
||||
|
||||
def initialize(self):
|
||||
super().initialize()
|
||||
|
||||
monkey_patched = True
|
||||
if self.server.async_mode == 'eventlet':
|
||||
from eventlet.patcher import is_monkey_patched
|
||||
monkey_patched = is_monkey_patched('socket')
|
||||
elif 'gevent' in self.server.async_mode:
|
||||
from gevent.monkey import is_module_patched
|
||||
monkey_patched = is_module_patched('socket')
|
||||
if not monkey_patched:
|
||||
raise RuntimeError(
|
||||
'Redis requires a monkey patched socket library to work '
|
||||
'with ' + self.server.async_mode)
|
||||
|
||||
def _redis_connect(self):
|
||||
self.redis = redis.Redis.from_url(self.redis_url,
|
||||
**self.redis_options)
|
||||
self.pubsub = self.redis.pubsub(ignore_subscribe_messages=True)
|
||||
|
||||
def _publish(self, data):
|
||||
retry = True
|
||||
while True:
|
||||
try:
|
||||
if not retry:
|
||||
self._redis_connect()
|
||||
return self.redis.publish(self.channel, pickle.dumps(data))
|
||||
except redis.exceptions.RedisError:
|
||||
if retry:
|
||||
logger.error('Cannot publish to redis... retrying')
|
||||
retry = False
|
||||
else:
|
||||
logger.error('Cannot publish to redis... giving up')
|
||||
break
|
||||
|
||||
def _redis_listen_with_retries(self):
|
||||
retry_sleep = 1
|
||||
connect = False
|
||||
while True:
|
||||
try:
|
||||
if connect:
|
||||
self._redis_connect()
|
||||
self.pubsub.subscribe(self.channel)
|
||||
retry_sleep = 1
|
||||
for message in self.pubsub.listen():
|
||||
yield message
|
||||
except redis.exceptions.RedisError:
|
||||
logger.error('Cannot receive from redis... '
|
||||
'retrying in {} secs'.format(retry_sleep))
|
||||
connect = True
|
||||
time.sleep(retry_sleep)
|
||||
retry_sleep *= 2
|
||||
if retry_sleep > 60:
|
||||
retry_sleep = 60
|
||||
|
||||
def _listen(self):
|
||||
channel = self.channel.encode('utf-8')
|
||||
self.pubsub.subscribe(self.channel)
|
||||
for message in self._redis_listen_with_retries():
|
||||
if message['channel'] == channel and \
|
||||
message['type'] == 'message' and 'data' in message:
|
||||
yield message['data']
|
||||
self.pubsub.unsubscribe(self.channel)
|
||||
666
venv/lib/python3.12/site-packages/socketio/server.py
Normal file
666
venv/lib/python3.12/site-packages/socketio/server.py
Normal file
@ -0,0 +1,666 @@
|
||||
import logging
|
||||
|
||||
import engineio
|
||||
|
||||
from . import base_server
|
||||
from . import exceptions
|
||||
from . import packet
|
||||
|
||||
default_logger = logging.getLogger('socketio.server')
|
||||
|
||||
|
||||
class Server(base_server.BaseServer):
|
||||
"""A Socket.IO server.
|
||||
|
||||
This class implements a fully compliant Socket.IO web server with support
|
||||
for websocket and long-polling transports.
|
||||
|
||||
:param client_manager: The client manager instance that will manage the
|
||||
client list. When this is omitted, the client list
|
||||
is stored in an in-memory structure, so the use of
|
||||
multiple connected servers is not possible.
|
||||
:param logger: To enable logging set to ``True`` or pass a logger object to
|
||||
use. To disable logging set to ``False``. The default is
|
||||
``False``. Note that fatal errors are logged even when
|
||||
``logger`` is ``False``.
|
||||
:param serializer: The serialization method to use when transmitting
|
||||
packets. Valid values are ``'default'``, ``'pickle'``,
|
||||
``'msgpack'`` and ``'cbor'``. Alternatively, a subclass
|
||||
of the :class:`Packet` class with custom implementations
|
||||
of the ``encode()`` and ``decode()`` methods can be
|
||||
provided. Client and server must use compatible
|
||||
serializers.
|
||||
:param json: An alternative json module to use for encoding and decoding
|
||||
packets. Custom json modules must have ``dumps`` and ``loads``
|
||||
functions that are compatible with the standard library
|
||||
versions.
|
||||
:param async_handlers: If set to ``True``, event handlers for a client are
|
||||
executed in separate threads. To run handlers for a
|
||||
client synchronously, set to ``False``. The default
|
||||
is ``True``.
|
||||
:param always_connect: When set to ``False``, new connections are
|
||||
provisory until the connect handler returns
|
||||
something other than ``False``, at which point they
|
||||
are accepted. When set to ``True``, connections are
|
||||
immediately accepted, and then if the connect
|
||||
handler returns ``False`` a disconnect is issued.
|
||||
Set to ``True`` if you need to emit events from the
|
||||
connect handler and your client is confused when it
|
||||
receives events before the connection acceptance.
|
||||
In any other case use the default of ``False``.
|
||||
:param namespaces: a list of namespaces that are accepted, in addition to
|
||||
any namespaces for which handlers have been defined. The
|
||||
default is `['/']`, which always accepts connections to
|
||||
the default namespace. Set to `'*'` to accept all
|
||||
namespaces.
|
||||
:param kwargs: Connection parameters for the underlying Engine.IO server.
|
||||
|
||||
The Engine.IO configuration supports the following settings:
|
||||
|
||||
:param async_mode: The asynchronous model to use. See the Deployment
|
||||
section in the documentation for a description of the
|
||||
available options. Valid async modes are
|
||||
``'threading'``, ``'eventlet'``, ``'gevent'`` and
|
||||
``'gevent_uwsgi'``. If this argument is not given,
|
||||
``'eventlet'`` is tried first, then ``'gevent_uwsgi'``,
|
||||
then ``'gevent'``, and finally ``'threading'``.
|
||||
The first async mode that has all its dependencies
|
||||
installed is then one that is chosen.
|
||||
:param ping_interval: The interval in seconds at which the server pings
|
||||
the client. The default is 25 seconds. For advanced
|
||||
control, a two element tuple can be given, where
|
||||
the first number is the ping interval and the second
|
||||
is a grace period added by the server.
|
||||
:param ping_timeout: The time in seconds that the client waits for the
|
||||
server to respond before disconnecting. The default
|
||||
is 20 seconds.
|
||||
:param max_http_buffer_size: The maximum size that is accepted for incoming
|
||||
messages. The default is 1,000,000 bytes. In
|
||||
spite of its name, the value set in this
|
||||
argument is enforced for HTTP long-polling and
|
||||
WebSocket connections.
|
||||
:param allow_upgrades: Whether to allow transport upgrades or not. The
|
||||
default is ``True``.
|
||||
:param http_compression: Whether to compress packages when using the
|
||||
polling transport. The default is ``True``.
|
||||
:param compression_threshold: Only compress messages when their byte size
|
||||
is greater than this value. The default is
|
||||
1024 bytes.
|
||||
:param cookie: If set to a string, it is the name of the HTTP cookie the
|
||||
server sends back tot he client containing the client
|
||||
session id. If set to a dictionary, the ``'name'`` key
|
||||
contains the cookie name and other keys define cookie
|
||||
attributes, where the value of each attribute can be a
|
||||
string, a callable with no arguments, or a boolean. If set
|
||||
to ``None`` (the default), a cookie is not sent to the
|
||||
client.
|
||||
:param cors_allowed_origins: Origin or list of origins that are allowed to
|
||||
connect to this server. Only the same origin
|
||||
is allowed by default. Set this argument to
|
||||
``'*'`` to allow all origins, or to ``[]`` to
|
||||
disable CORS handling.
|
||||
:param cors_credentials: Whether credentials (cookies, authentication) are
|
||||
allowed in requests to this server. The default is
|
||||
``True``.
|
||||
:param monitor_clients: If set to ``True``, a background task will ensure
|
||||
inactive clients are closed. Set to ``False`` to
|
||||
disable the monitoring task (not recommended). The
|
||||
default is ``True``.
|
||||
:param transports: The list of allowed transports. Valid transports
|
||||
are ``'polling'`` and ``'websocket'``. Defaults to
|
||||
``['polling', 'websocket']``.
|
||||
:param engineio_logger: To enable Engine.IO logging set to ``True`` or pass
|
||||
a logger object to use. To disable logging set to
|
||||
``False``. The default is ``False``. Note that
|
||||
fatal errors are logged even when
|
||||
``engineio_logger`` is ``False``.
|
||||
"""
|
||||
def emit(self, event, data=None, to=None, room=None, skip_sid=None,
|
||||
namespace=None, callback=None, ignore_queue=False):
|
||||
"""Emit a custom event to one or more connected clients.
|
||||
|
||||
:param event: The event name. It can be any string. The event names
|
||||
``'connect'``, ``'message'`` and ``'disconnect'`` are
|
||||
reserved and should not be used.
|
||||
:param data: The data to send to the client or clients. Data can be of
|
||||
type ``str``, ``bytes``, ``list`` or ``dict``. To send
|
||||
multiple arguments, use a tuple where each element is of
|
||||
one of the types indicated above.
|
||||
:param to: The recipient of the message. This can be set to the
|
||||
session ID of a client to address only that client, to any
|
||||
custom room created by the application to address all
|
||||
the clients in that room, or to a list of custom room
|
||||
names. If this argument is omitted the event is broadcasted
|
||||
to all connected clients.
|
||||
:param room: Alias for the ``to`` parameter.
|
||||
:param skip_sid: The session ID of a client to skip when broadcasting
|
||||
to a room or to all clients. This can be used to
|
||||
prevent a message from being sent to the sender. To
|
||||
skip multiple sids, pass a list.
|
||||
:param namespace: The Socket.IO namespace for the event. If this
|
||||
argument is omitted the event is emitted to the
|
||||
default namespace.
|
||||
:param callback: If given, this function will be called to acknowledge
|
||||
the client has received the message. The arguments
|
||||
that will be passed to the function are those provided
|
||||
by the client. Callback functions can only be used
|
||||
when addressing an individual client.
|
||||
:param ignore_queue: Only used when a message queue is configured. If
|
||||
set to ``True``, the event is emitted to the
|
||||
clients directly, without going through the queue.
|
||||
This is more efficient, but only works when a
|
||||
single server process is used. It is recommended
|
||||
to always leave this parameter with its default
|
||||
value of ``False``.
|
||||
|
||||
Note: this method is not thread safe. If multiple threads are emitting
|
||||
at the same time to the same client, then messages composed of
|
||||
multiple packets may end up being sent in an incorrect sequence. Use
|
||||
standard concurrency solutions (such as a Lock object) to prevent this
|
||||
situation.
|
||||
"""
|
||||
namespace = namespace or '/'
|
||||
room = to or room
|
||||
self.logger.info('emitting event "%s" to %s [%s]', event,
|
||||
room or 'all', namespace)
|
||||
self.manager.emit(event, data, namespace, room=room,
|
||||
skip_sid=skip_sid, callback=callback,
|
||||
ignore_queue=ignore_queue)
|
||||
|
||||
def send(self, data, to=None, room=None, skip_sid=None, namespace=None,
|
||||
callback=None, ignore_queue=False):
|
||||
"""Send a message to one or more connected clients.
|
||||
|
||||
This function emits an event with the name ``'message'``. Use
|
||||
:func:`emit` to issue custom event names.
|
||||
|
||||
:param data: The data to send to the client or clients. Data can be of
|
||||
type ``str``, ``bytes``, ``list`` or ``dict``. To send
|
||||
multiple arguments, use a tuple where each element is of
|
||||
one of the types indicated above.
|
||||
:param to: The recipient of the message. This can be set to the
|
||||
session ID of a client to address only that client, to any
|
||||
any custom room created by the application to address all
|
||||
the clients in that room, or to a list of custom room
|
||||
names. If this argument is omitted the event is broadcasted
|
||||
to all connected clients.
|
||||
:param room: Alias for the ``to`` parameter.
|
||||
:param skip_sid: The session ID of a client to skip when broadcasting
|
||||
to a room or to all clients. This can be used to
|
||||
prevent a message from being sent to the sender. To
|
||||
skip multiple sids, pass a list.
|
||||
:param namespace: The Socket.IO namespace for the event. If this
|
||||
argument is omitted the event is emitted to the
|
||||
default namespace.
|
||||
:param callback: If given, this function will be called to acknowledge
|
||||
the client has received the message. The arguments
|
||||
that will be passed to the function are those provided
|
||||
by the client. Callback functions can only be used
|
||||
when addressing an individual client.
|
||||
:param ignore_queue: Only used when a message queue is configured. If
|
||||
set to ``True``, the event is emitted to the
|
||||
clients directly, without going through the queue.
|
||||
This is more efficient, but only works when a
|
||||
single server process is used. It is recommended
|
||||
to always leave this parameter with its default
|
||||
value of ``False``.
|
||||
"""
|
||||
self.emit('message', data=data, to=to, room=room, skip_sid=skip_sid,
|
||||
namespace=namespace, callback=callback,
|
||||
ignore_queue=ignore_queue)
|
||||
|
||||
def call(self, event, data=None, to=None, sid=None, namespace=None,
|
||||
timeout=60, ignore_queue=False):
|
||||
"""Emit a custom event to a client and wait for the response.
|
||||
|
||||
This method issues an emit with a callback and waits for the callback
|
||||
to be invoked before returning. If the callback isn't invoked before
|
||||
the timeout, then a ``TimeoutError`` exception is raised. If the
|
||||
Socket.IO connection drops during the wait, this method still waits
|
||||
until the specified timeout.
|
||||
|
||||
:param event: The event name. It can be any string. The event names
|
||||
``'connect'``, ``'message'`` and ``'disconnect'`` are
|
||||
reserved and should not be used.
|
||||
:param data: The data to send to the client or clients. Data can be of
|
||||
type ``str``, ``bytes``, ``list`` or ``dict``. To send
|
||||
multiple arguments, use a tuple where each element is of
|
||||
one of the types indicated above.
|
||||
:param to: The session ID of the recipient client.
|
||||
:param sid: Alias for the ``to`` parameter.
|
||||
:param namespace: The Socket.IO namespace for the event. If this
|
||||
argument is omitted the event is emitted to the
|
||||
default namespace.
|
||||
:param timeout: The waiting timeout. If the timeout is reached before
|
||||
the client acknowledges the event, then a
|
||||
``TimeoutError`` exception is raised.
|
||||
:param ignore_queue: Only used when a message queue is configured. If
|
||||
set to ``True``, the event is emitted to the
|
||||
client directly, without going through the queue.
|
||||
This is more efficient, but only works when a
|
||||
single server process is used. It is recommended
|
||||
to always leave this parameter with its default
|
||||
value of ``False``.
|
||||
|
||||
Note: this method is not thread safe. If multiple threads are emitting
|
||||
at the same time to the same client, then messages composed of
|
||||
multiple packets may end up being sent in an incorrect sequence. Use
|
||||
standard concurrency solutions (such as a Lock object) to prevent this
|
||||
situation.
|
||||
"""
|
||||
if to is None and sid is None:
|
||||
raise ValueError('Cannot use call() to broadcast.')
|
||||
if not self.async_handlers:
|
||||
raise RuntimeError(
|
||||
'Cannot use call() when async_handlers is False.')
|
||||
callback_event = self.eio.create_event()
|
||||
callback_args = []
|
||||
|
||||
def event_callback(*args):
|
||||
callback_args.append(args)
|
||||
callback_event.set()
|
||||
|
||||
self.emit(event, data=data, room=to or sid, namespace=namespace,
|
||||
callback=event_callback, ignore_queue=ignore_queue)
|
||||
if not callback_event.wait(timeout=timeout):
|
||||
raise exceptions.TimeoutError()
|
||||
return callback_args[0] if len(callback_args[0]) > 1 \
|
||||
else callback_args[0][0] if len(callback_args[0]) == 1 \
|
||||
else None
|
||||
|
||||
def enter_room(self, sid, room, namespace=None):
|
||||
"""Enter a room.
|
||||
|
||||
This function adds the client to a room. The :func:`emit` and
|
||||
:func:`send` functions can optionally broadcast events to all the
|
||||
clients in a room.
|
||||
|
||||
:param sid: Session ID of the client.
|
||||
:param room: Room name. If the room does not exist it is created.
|
||||
:param namespace: The Socket.IO namespace for the event. If this
|
||||
argument is omitted the default namespace is used.
|
||||
"""
|
||||
namespace = namespace or '/'
|
||||
self.logger.info('%s is entering room %s [%s]', sid, room, namespace)
|
||||
self.manager.enter_room(sid, namespace, room)
|
||||
|
||||
def leave_room(self, sid, room, namespace=None):
|
||||
"""Leave a room.
|
||||
|
||||
This function removes the client from a room.
|
||||
|
||||
:param sid: Session ID of the client.
|
||||
:param room: Room name.
|
||||
:param namespace: The Socket.IO namespace for the event. If this
|
||||
argument is omitted the default namespace is used.
|
||||
"""
|
||||
namespace = namespace or '/'
|
||||
self.logger.info('%s is leaving room %s [%s]', sid, room, namespace)
|
||||
self.manager.leave_room(sid, namespace, room)
|
||||
|
||||
def close_room(self, room, namespace=None):
|
||||
"""Close a room.
|
||||
|
||||
This function removes all the clients from the given room.
|
||||
|
||||
:param room: Room name.
|
||||
:param namespace: The Socket.IO namespace for the event. If this
|
||||
argument is omitted the default namespace is used.
|
||||
"""
|
||||
namespace = namespace or '/'
|
||||
self.logger.info('room %s is closing [%s]', room, namespace)
|
||||
self.manager.close_room(room, namespace)
|
||||
|
||||
def get_session(self, sid, namespace=None):
|
||||
"""Return the user session for a client.
|
||||
|
||||
:param sid: The session id of the client.
|
||||
:param namespace: The Socket.IO namespace. If this argument is omitted
|
||||
the default namespace is used.
|
||||
|
||||
The return value is a dictionary. Modifications made to this
|
||||
dictionary are not guaranteed to be preserved unless
|
||||
``save_session()`` is called, or when the ``session`` context manager
|
||||
is used.
|
||||
"""
|
||||
namespace = namespace or '/'
|
||||
eio_sid = self.manager.eio_sid_from_sid(sid, namespace)
|
||||
eio_session = self.eio.get_session(eio_sid)
|
||||
return eio_session.setdefault(namespace, {})
|
||||
|
||||
def save_session(self, sid, session, namespace=None):
|
||||
"""Store the user session for a client.
|
||||
|
||||
:param sid: The session id of the client.
|
||||
:param session: The session dictionary.
|
||||
:param namespace: The Socket.IO namespace. If this argument is omitted
|
||||
the default namespace is used.
|
||||
"""
|
||||
namespace = namespace or '/'
|
||||
eio_sid = self.manager.eio_sid_from_sid(sid, namespace)
|
||||
eio_session = self.eio.get_session(eio_sid)
|
||||
eio_session[namespace] = session
|
||||
|
||||
def session(self, sid, namespace=None):
|
||||
"""Return the user session for a client with context manager syntax.
|
||||
|
||||
:param sid: The session id of the client.
|
||||
|
||||
This is a context manager that returns the user session dictionary for
|
||||
the client. Any changes that are made to this dictionary inside the
|
||||
context manager block are saved back to the session. Example usage::
|
||||
|
||||
@sio.on('connect')
|
||||
def on_connect(sid, environ):
|
||||
username = authenticate_user(environ)
|
||||
if not username:
|
||||
return False
|
||||
with sio.session(sid) as session:
|
||||
session['username'] = username
|
||||
|
||||
@sio.on('message')
|
||||
def on_message(sid, msg):
|
||||
with sio.session(sid) as session:
|
||||
print('received message from ', session['username'])
|
||||
"""
|
||||
class _session_context_manager(object):
|
||||
def __init__(self, server, sid, namespace):
|
||||
self.server = server
|
||||
self.sid = sid
|
||||
self.namespace = namespace
|
||||
self.session = None
|
||||
|
||||
def __enter__(self):
|
||||
self.session = self.server.get_session(sid,
|
||||
namespace=namespace)
|
||||
return self.session
|
||||
|
||||
def __exit__(self, *args):
|
||||
self.server.save_session(sid, self.session,
|
||||
namespace=namespace)
|
||||
|
||||
return _session_context_manager(self, sid, namespace)
|
||||
|
||||
def disconnect(self, sid, namespace=None, ignore_queue=False):
|
||||
"""Disconnect a client.
|
||||
|
||||
:param sid: Session ID of the client.
|
||||
:param namespace: The Socket.IO namespace to disconnect. If this
|
||||
argument is omitted the default namespace is used.
|
||||
:param ignore_queue: Only used when a message queue is configured. If
|
||||
set to ``True``, the disconnect is processed
|
||||
locally, without broadcasting on the queue. It is
|
||||
recommended to always leave this parameter with
|
||||
its default value of ``False``.
|
||||
"""
|
||||
namespace = namespace or '/'
|
||||
if ignore_queue:
|
||||
delete_it = self.manager.is_connected(sid, namespace)
|
||||
else:
|
||||
delete_it = self.manager.can_disconnect(sid, namespace)
|
||||
if delete_it:
|
||||
self.logger.info('Disconnecting %s [%s]', sid, namespace)
|
||||
eio_sid = self.manager.pre_disconnect(sid, namespace=namespace)
|
||||
self._send_packet(eio_sid, self.packet_class(
|
||||
packet.DISCONNECT, namespace=namespace))
|
||||
self._trigger_event('disconnect', namespace, sid)
|
||||
self.manager.disconnect(sid, namespace=namespace,
|
||||
ignore_queue=True)
|
||||
|
||||
def shutdown(self):
|
||||
"""Stop Socket.IO background tasks.
|
||||
|
||||
This method stops all background activity initiated by the Socket.IO
|
||||
server. It must be called before shutting down the web server.
|
||||
"""
|
||||
self.logger.info('Socket.IO is shutting down')
|
||||
self.eio.shutdown()
|
||||
|
||||
def handle_request(self, environ, start_response):
|
||||
"""Handle an HTTP request from the client.
|
||||
|
||||
This is the entry point of the Socket.IO application, using the same
|
||||
interface as a WSGI application. For the typical usage, this function
|
||||
is invoked by the :class:`Middleware` instance, but it can be invoked
|
||||
directly when the middleware is not used.
|
||||
|
||||
:param environ: The WSGI environment.
|
||||
:param start_response: The WSGI ``start_response`` function.
|
||||
|
||||
This function returns the HTTP response body to deliver to the client
|
||||
as a byte sequence.
|
||||
"""
|
||||
return self.eio.handle_request(environ, start_response)
|
||||
|
||||
def start_background_task(self, target, *args, **kwargs):
|
||||
"""Start a background task using the appropriate async model.
|
||||
|
||||
This is a utility function that applications can use to start a
|
||||
background task using the method that is compatible with the
|
||||
selected async mode.
|
||||
|
||||
:param target: the target function to execute.
|
||||
:param args: arguments to pass to the function.
|
||||
:param kwargs: keyword arguments to pass to the function.
|
||||
|
||||
This function returns an object that represents the background task,
|
||||
on which the ``join()`` methond can be invoked to wait for the task to
|
||||
complete.
|
||||
"""
|
||||
return self.eio.start_background_task(target, *args, **kwargs)
|
||||
|
||||
def sleep(self, seconds=0):
|
||||
"""Sleep for the requested amount of time using the appropriate async
|
||||
model.
|
||||
|
||||
This is a utility function that applications can use to put a task to
|
||||
sleep without having to worry about using the correct call for the
|
||||
selected async mode.
|
||||
"""
|
||||
return self.eio.sleep(seconds)
|
||||
|
||||
def instrument(self, auth=None, mode='development', read_only=False,
|
||||
server_id=None, namespace='/admin',
|
||||
server_stats_interval=2):
|
||||
"""Instrument the Socket.IO server for monitoring with the `Socket.IO
|
||||
Admin UI <https://socket.io/docs/v4/admin-ui/>`_.
|
||||
|
||||
:param auth: Authentication credentials for Admin UI access. Set to a
|
||||
dictionary with the expected login (usually ``username``
|
||||
and ``password``) or a list of dictionaries if more than
|
||||
one set of credentials need to be available. For more
|
||||
complex authentication methods, set to a callable that
|
||||
receives the authentication dictionary as an argument and
|
||||
returns ``True`` if the user is allowed or ``False``
|
||||
otherwise. To disable authentication, set this argument to
|
||||
``False`` (not recommended, never do this on a production
|
||||
server).
|
||||
:param mode: The reporting mode. The default is ``'development'``,
|
||||
which is best used while debugging, as it may have a
|
||||
significant performance effect. Set to ``'production'`` to
|
||||
reduce the amount of information that is reported to the
|
||||
admin UI.
|
||||
:param read_only: If set to ``True``, the admin interface will be
|
||||
read-only, with no option to modify room assignments
|
||||
or disconnect clients. The default is ``False``.
|
||||
:param server_id: The server name to use for this server. If this
|
||||
argument is omitted, the server generates its own
|
||||
name.
|
||||
:param namespace: The Socket.IO namespace to use for the admin
|
||||
interface. The default is ``/admin``.
|
||||
:param server_stats_interval: The interval in seconds at which the
|
||||
server emits a summary of it stats to all
|
||||
connected admins.
|
||||
"""
|
||||
from .admin import InstrumentedServer
|
||||
return InstrumentedServer(
|
||||
self, auth=auth, mode=mode, read_only=read_only,
|
||||
server_id=server_id, namespace=namespace,
|
||||
server_stats_interval=server_stats_interval)
|
||||
|
||||
def _send_packet(self, eio_sid, pkt):
|
||||
"""Send a Socket.IO packet to a client."""
|
||||
encoded_packet = pkt.encode()
|
||||
if isinstance(encoded_packet, list):
|
||||
for ep in encoded_packet:
|
||||
self.eio.send(eio_sid, ep)
|
||||
else:
|
||||
self.eio.send(eio_sid, encoded_packet)
|
||||
|
||||
def _send_eio_packet(self, eio_sid, eio_pkt):
|
||||
"""Send a raw Engine.IO packet to a client."""
|
||||
self.eio.send_packet(eio_sid, eio_pkt)
|
||||
|
||||
def _handle_connect(self, eio_sid, namespace, data):
|
||||
"""Handle a client connection request."""
|
||||
namespace = namespace or '/'
|
||||
sid = None
|
||||
if namespace in self.handlers or namespace in self.namespace_handlers \
|
||||
or self.namespaces == '*' or namespace in self.namespaces:
|
||||
sid = self.manager.connect(eio_sid, namespace)
|
||||
if sid is None:
|
||||
self._send_packet(eio_sid, self.packet_class(
|
||||
packet.CONNECT_ERROR, data='Unable to connect',
|
||||
namespace=namespace))
|
||||
return
|
||||
|
||||
if self.always_connect:
|
||||
self._send_packet(eio_sid, self.packet_class(
|
||||
packet.CONNECT, {'sid': sid}, namespace=namespace))
|
||||
fail_reason = exceptions.ConnectionRefusedError().error_args
|
||||
try:
|
||||
if data:
|
||||
success = self._trigger_event(
|
||||
'connect', namespace, sid, self.environ[eio_sid], data)
|
||||
else:
|
||||
try:
|
||||
success = self._trigger_event(
|
||||
'connect', namespace, sid, self.environ[eio_sid])
|
||||
except TypeError:
|
||||
success = self._trigger_event(
|
||||
'connect', namespace, sid, self.environ[eio_sid], None)
|
||||
except exceptions.ConnectionRefusedError as exc:
|
||||
fail_reason = exc.error_args
|
||||
success = False
|
||||
|
||||
if success is False:
|
||||
if self.always_connect:
|
||||
self.manager.pre_disconnect(sid, namespace)
|
||||
self._send_packet(eio_sid, self.packet_class(
|
||||
packet.DISCONNECT, data=fail_reason, namespace=namespace))
|
||||
else:
|
||||
self._send_packet(eio_sid, self.packet_class(
|
||||
packet.CONNECT_ERROR, data=fail_reason,
|
||||
namespace=namespace))
|
||||
self.manager.disconnect(sid, namespace, ignore_queue=True)
|
||||
elif not self.always_connect:
|
||||
self._send_packet(eio_sid, self.packet_class(
|
||||
packet.CONNECT, {'sid': sid}, namespace=namespace))
|
||||
|
||||
def _handle_disconnect(self, eio_sid, namespace):
|
||||
"""Handle a client disconnect."""
|
||||
namespace = namespace or '/'
|
||||
sid = self.manager.sid_from_eio_sid(eio_sid, namespace)
|
||||
if not self.manager.is_connected(sid, namespace): # pragma: no cover
|
||||
return
|
||||
self.manager.pre_disconnect(sid, namespace=namespace)
|
||||
self._trigger_event('disconnect', namespace, sid)
|
||||
self.manager.disconnect(sid, namespace, ignore_queue=True)
|
||||
|
||||
def _handle_event(self, eio_sid, namespace, id, data):
|
||||
"""Handle an incoming client event."""
|
||||
namespace = namespace or '/'
|
||||
sid = self.manager.sid_from_eio_sid(eio_sid, namespace)
|
||||
self.logger.info('received event "%s" from %s [%s]', data[0], sid,
|
||||
namespace)
|
||||
if not self.manager.is_connected(sid, namespace):
|
||||
self.logger.warning('%s is not connected to namespace %s',
|
||||
sid, namespace)
|
||||
return
|
||||
if self.async_handlers:
|
||||
self.start_background_task(self._handle_event_internal, self, sid,
|
||||
eio_sid, data, namespace, id)
|
||||
else:
|
||||
self._handle_event_internal(self, sid, eio_sid, data, namespace,
|
||||
id)
|
||||
|
||||
def _handle_event_internal(self, server, sid, eio_sid, data, namespace,
|
||||
id):
|
||||
r = server._trigger_event(data[0], namespace, sid, *data[1:])
|
||||
if r != self.not_handled and id is not None:
|
||||
# send ACK packet with the response returned by the handler
|
||||
# tuples are expanded as multiple arguments
|
||||
if r is None:
|
||||
data = []
|
||||
elif isinstance(r, tuple):
|
||||
data = list(r)
|
||||
else:
|
||||
data = [r]
|
||||
server._send_packet(eio_sid, self.packet_class(
|
||||
packet.ACK, namespace=namespace, id=id, data=data))
|
||||
|
||||
def _handle_ack(self, eio_sid, namespace, id, data):
|
||||
"""Handle ACK packets from the client."""
|
||||
namespace = namespace or '/'
|
||||
sid = self.manager.sid_from_eio_sid(eio_sid, namespace)
|
||||
self.logger.info('received ack from %s [%s]', sid, namespace)
|
||||
self.manager.trigger_callback(sid, id, data)
|
||||
|
||||
def _trigger_event(self, event, namespace, *args):
|
||||
"""Invoke an application event handler."""
|
||||
# first see if we have an explicit handler for the event
|
||||
handler, args = self._get_event_handler(event, namespace, args)
|
||||
if handler:
|
||||
return handler(*args)
|
||||
# or else, forward the event to a namespace handler if one exists
|
||||
handler, args = self._get_namespace_handler(namespace, args)
|
||||
if handler:
|
||||
return handler.trigger_event(event, *args)
|
||||
else:
|
||||
return self.not_handled
|
||||
|
||||
def _handle_eio_connect(self, eio_sid, environ):
|
||||
"""Handle the Engine.IO connection event."""
|
||||
if not self.manager_initialized:
|
||||
self.manager_initialized = True
|
||||
self.manager.initialize()
|
||||
self.environ[eio_sid] = environ
|
||||
|
||||
def _handle_eio_message(self, eio_sid, data):
|
||||
"""Dispatch Engine.IO messages."""
|
||||
if eio_sid in self._binary_packet:
|
||||
pkt = self._binary_packet[eio_sid]
|
||||
if pkt.add_attachment(data):
|
||||
del self._binary_packet[eio_sid]
|
||||
if pkt.packet_type == packet.BINARY_EVENT:
|
||||
self._handle_event(eio_sid, pkt.namespace, pkt.id,
|
||||
pkt.data)
|
||||
else:
|
||||
self._handle_ack(eio_sid, pkt.namespace, pkt.id, pkt.data)
|
||||
else:
|
||||
pkt = self.packet_class(encoded_packet=data)
|
||||
if pkt.packet_type == packet.CONNECT:
|
||||
self._handle_connect(eio_sid, pkt.namespace, pkt.data)
|
||||
elif pkt.packet_type == packet.DISCONNECT:
|
||||
self._handle_disconnect(eio_sid, pkt.namespace)
|
||||
elif pkt.packet_type == packet.EVENT:
|
||||
self._handle_event(eio_sid, pkt.namespace, pkt.id, pkt.data)
|
||||
elif pkt.packet_type == packet.ACK:
|
||||
self._handle_ack(eio_sid, pkt.namespace, pkt.id, pkt.data)
|
||||
elif pkt.packet_type == packet.BINARY_EVENT or \
|
||||
pkt.packet_type == packet.BINARY_ACK:
|
||||
self._binary_packet[eio_sid] = pkt
|
||||
elif pkt.packet_type == packet.CONNECT_ERROR:
|
||||
raise ValueError('Unexpected CONNECT_ERROR packet.')
|
||||
else:
|
||||
raise ValueError('Unknown packet type.')
|
||||
|
||||
def _handle_eio_disconnect(self, eio_sid):
|
||||
"""Handle Engine.IO disconnect event."""
|
||||
for n in list(self.manager.get_namespaces()).copy():
|
||||
self._handle_disconnect(eio_sid, n)
|
||||
if eio_sid in self.environ:
|
||||
del self.environ[eio_sid]
|
||||
|
||||
def _engineio_server_class(self):
|
||||
return engineio.Server
|
||||
191
venv/lib/python3.12/site-packages/socketio/simple_client.py
Normal file
191
venv/lib/python3.12/site-packages/socketio/simple_client.py
Normal file
@ -0,0 +1,191 @@
|
||||
from threading import Event
|
||||
from socketio import Client
|
||||
from socketio.exceptions import SocketIOError, TimeoutError, DisconnectedError
|
||||
|
||||
|
||||
class SimpleClient:
|
||||
"""A Socket.IO client.
|
||||
|
||||
This class implements a simple, yet fully compliant Socket.IO web client
|
||||
with support for websocket and long-polling transports.
|
||||
|
||||
The positional and keyword arguments given in the constructor are passed
|
||||
to the underlying :func:`socketio.Client` object.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.client_args = args
|
||||
self.client_kwargs = kwargs
|
||||
self.client = None
|
||||
self.namespace = '/'
|
||||
self.connected_event = Event()
|
||||
self.connected = False
|
||||
self.input_event = Event()
|
||||
self.input_buffer = []
|
||||
|
||||
def connect(self, url, headers={}, auth=None, transports=None,
|
||||
namespace='/', socketio_path='socket.io', wait_timeout=5):
|
||||
"""Connect to a Socket.IO server.
|
||||
|
||||
:param url: The URL of the Socket.IO server. It can include custom
|
||||
query string parameters if required by the server. If a
|
||||
function is provided, the client will invoke it to obtain
|
||||
the URL each time a connection or reconnection is
|
||||
attempted.
|
||||
:param headers: A dictionary with custom headers to send with the
|
||||
connection request. If a function is provided, the
|
||||
client will invoke it to obtain the headers dictionary
|
||||
each time a connection or reconnection is attempted.
|
||||
:param auth: Authentication data passed to the server with the
|
||||
connection request, normally a dictionary with one or
|
||||
more string key/value pairs. If a function is provided,
|
||||
the client will invoke it to obtain the authentication
|
||||
data each time a connection or reconnection is attempted.
|
||||
:param transports: The list of allowed transports. Valid transports
|
||||
are ``'polling'`` and ``'websocket'``. If not
|
||||
given, the polling transport is connected first,
|
||||
then an upgrade to websocket is attempted.
|
||||
:param namespace: The namespace to connect to as a string. If not
|
||||
given, the default namespace ``/`` is used.
|
||||
:param socketio_path: The endpoint where the Socket.IO server is
|
||||
installed. The default value is appropriate for
|
||||
most cases.
|
||||
:param wait_timeout: How long the client should wait for the
|
||||
connection to be established. The default is 5
|
||||
seconds.
|
||||
"""
|
||||
if self.connected:
|
||||
raise RuntimeError('Already connected')
|
||||
self.namespace = namespace
|
||||
self.input_buffer = []
|
||||
self.input_event.clear()
|
||||
self.client = Client(*self.client_args, **self.client_kwargs)
|
||||
|
||||
@self.client.event(namespace=self.namespace)
|
||||
def connect(): # pragma: no cover
|
||||
self.connected = True
|
||||
self.connected_event.set()
|
||||
|
||||
@self.client.event(namespace=self.namespace)
|
||||
def disconnect(): # pragma: no cover
|
||||
self.connected_event.clear()
|
||||
|
||||
@self.client.event(namespace=self.namespace)
|
||||
def __disconnect_final(): # pragma: no cover
|
||||
self.connected = False
|
||||
self.connected_event.set()
|
||||
|
||||
@self.client.on('*', namespace=self.namespace)
|
||||
def on_event(event, *args): # pragma: no cover
|
||||
self.input_buffer.append([event, *args])
|
||||
self.input_event.set()
|
||||
|
||||
self.client.connect(url, headers=headers, auth=auth,
|
||||
transports=transports, namespaces=[namespace],
|
||||
socketio_path=socketio_path,
|
||||
wait_timeout=wait_timeout)
|
||||
|
||||
@property
|
||||
def sid(self):
|
||||
"""The session ID received from the server.
|
||||
|
||||
The session ID is not guaranteed to remain constant throughout the life
|
||||
of the connection, as reconnections can cause it to change.
|
||||
"""
|
||||
return self.client.get_sid(self.namespace) if self.client else None
|
||||
|
||||
@property
|
||||
def transport(self):
|
||||
"""The name of the transport currently in use.
|
||||
|
||||
The transport is returned as a string and can be one of ``polling``
|
||||
and ``websocket``.
|
||||
"""
|
||||
return self.client.transport if self.client else ''
|
||||
|
||||
def emit(self, event, data=None):
|
||||
"""Emit an event to the server.
|
||||
|
||||
:param event: The event name. It can be any string. The event names
|
||||
``'connect'``, ``'message'`` and ``'disconnect'`` are
|
||||
reserved and should not be used.
|
||||
:param data: The data to send to the server. Data can be of
|
||||
type ``str``, ``bytes``, ``list`` or ``dict``. To send
|
||||
multiple arguments, use a tuple where each element is of
|
||||
one of the types indicated above.
|
||||
|
||||
This method schedules the event to be sent out and returns, without
|
||||
actually waiting for its delivery. In cases where the client needs to
|
||||
ensure that the event was received, :func:`socketio.SimpleClient.call`
|
||||
should be used instead.
|
||||
"""
|
||||
while True:
|
||||
self.connected_event.wait()
|
||||
if not self.connected:
|
||||
raise DisconnectedError()
|
||||
try:
|
||||
return self.client.emit(event, data, namespace=self.namespace)
|
||||
except SocketIOError:
|
||||
pass
|
||||
|
||||
def call(self, event, data=None, timeout=60):
|
||||
"""Emit an event to the server and wait for a response.
|
||||
|
||||
This method issues an emit and waits for the server to provide a
|
||||
response or acknowledgement. If the response does not arrive before the
|
||||
timeout, then a ``TimeoutError`` exception is raised.
|
||||
|
||||
:param event: The event name. It can be any string. The event names
|
||||
``'connect'``, ``'message'`` and ``'disconnect'`` are
|
||||
reserved and should not be used.
|
||||
:param data: The data to send to the server. Data can be of
|
||||
type ``str``, ``bytes``, ``list`` or ``dict``. To send
|
||||
multiple arguments, use a tuple where each element is of
|
||||
one of the types indicated above.
|
||||
:param timeout: The waiting timeout. If the timeout is reached before
|
||||
the server acknowledges the event, then a
|
||||
``TimeoutError`` exception is raised.
|
||||
"""
|
||||
while True:
|
||||
self.connected_event.wait()
|
||||
if not self.connected:
|
||||
raise DisconnectedError()
|
||||
try:
|
||||
return self.client.call(event, data, namespace=self.namespace,
|
||||
timeout=timeout)
|
||||
except SocketIOError:
|
||||
pass
|
||||
|
||||
def receive(self, timeout=None):
|
||||
"""Wait for an event from the server.
|
||||
|
||||
:param timeout: The waiting timeout. If the timeout is reached before
|
||||
the server acknowledges the event, then a
|
||||
``TimeoutError`` exception is raised.
|
||||
|
||||
The return value is a list with the event name as the first element. If
|
||||
the server included arguments with the event, they are returned as
|
||||
additional list elements.
|
||||
"""
|
||||
while not self.input_buffer:
|
||||
if not self.connected_event.wait(
|
||||
timeout=timeout): # pragma: no cover
|
||||
raise TimeoutError()
|
||||
if not self.connected:
|
||||
raise DisconnectedError()
|
||||
if not self.input_event.wait(timeout=timeout):
|
||||
raise TimeoutError()
|
||||
self.input_event.clear()
|
||||
return self.input_buffer.pop(0)
|
||||
|
||||
def disconnect(self):
|
||||
"""Disconnect from the server."""
|
||||
if self.connected:
|
||||
self.client.disconnect()
|
||||
self.client = None
|
||||
self.connected = False
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.disconnect()
|
||||
9
venv/lib/python3.12/site-packages/socketio/tornado.py
Normal file
9
venv/lib/python3.12/site-packages/socketio/tornado.py
Normal file
@ -0,0 +1,9 @@
|
||||
try:
|
||||
from engineio.async_drivers.tornado import get_tornado_handler as \
|
||||
get_engineio_handler
|
||||
except ImportError: # pragma: no cover
|
||||
get_engineio_handler = None
|
||||
|
||||
|
||||
def get_tornado_handler(socketio_server): # pragma: no cover
|
||||
return get_engineio_handler(socketio_server.eio)
|
||||
105
venv/lib/python3.12/site-packages/socketio/zmq_manager.py
Normal file
105
venv/lib/python3.12/site-packages/socketio/zmq_manager.py
Normal file
@ -0,0 +1,105 @@
|
||||
import pickle
|
||||
import re
|
||||
|
||||
from .pubsub_manager import PubSubManager
|
||||
|
||||
|
||||
class ZmqManager(PubSubManager): # pragma: no cover
|
||||
"""zmq based client manager.
|
||||
|
||||
NOTE: this zmq implementation should be considered experimental at this
|
||||
time. At this time, eventlet is required to use zmq.
|
||||
|
||||
This class implements a zmq backend for event sharing across multiple
|
||||
processes. To use a zmq backend, initialize the :class:`Server` instance as
|
||||
follows::
|
||||
|
||||
url = 'zmq+tcp://hostname:port1+port2'
|
||||
server = socketio.Server(client_manager=socketio.ZmqManager(url))
|
||||
|
||||
:param url: The connection URL for the zmq message broker,
|
||||
which will need to be provided and running.
|
||||
:param channel: The channel name on which the server sends and receives
|
||||
notifications. Must be the same in all the servers.
|
||||
:param write_only: If set to ``True``, only initialize to emit events. The
|
||||
default of ``False`` initializes the class for emitting
|
||||
and receiving.
|
||||
|
||||
A zmq message broker must be running for the zmq_manager to work.
|
||||
you can write your own or adapt one from the following simple broker
|
||||
below::
|
||||
|
||||
import zmq
|
||||
|
||||
receiver = zmq.Context().socket(zmq.PULL)
|
||||
receiver.bind("tcp://*:5555")
|
||||
|
||||
publisher = zmq.Context().socket(zmq.PUB)
|
||||
publisher.bind("tcp://*:5556")
|
||||
|
||||
while True:
|
||||
publisher.send(receiver.recv())
|
||||
"""
|
||||
name = 'zmq'
|
||||
|
||||
def __init__(self, url='zmq+tcp://localhost:5555+5556',
|
||||
channel='socketio',
|
||||
write_only=False,
|
||||
logger=None):
|
||||
try:
|
||||
from eventlet.green import zmq
|
||||
except ImportError:
|
||||
raise RuntimeError('zmq package is not installed '
|
||||
'(Run "pip install pyzmq" in your '
|
||||
'virtualenv).')
|
||||
|
||||
r = re.compile(r':\d+\+\d+$')
|
||||
if not (url.startswith('zmq+tcp://') and r.search(url)):
|
||||
raise RuntimeError('unexpected connection string: ' + url)
|
||||
|
||||
url = url.replace('zmq+', '')
|
||||
(sink_url, sub_port) = url.split('+')
|
||||
sink_port = sink_url.split(':')[-1]
|
||||
sub_url = sink_url.replace(sink_port, sub_port)
|
||||
|
||||
sink = zmq.Context().socket(zmq.PUSH)
|
||||
sink.connect(sink_url)
|
||||
|
||||
sub = zmq.Context().socket(zmq.SUB)
|
||||
sub.setsockopt_string(zmq.SUBSCRIBE, u'')
|
||||
sub.connect(sub_url)
|
||||
|
||||
self.sink = sink
|
||||
self.sub = sub
|
||||
self.channel = channel
|
||||
super().__init__(channel=channel, write_only=write_only, logger=logger)
|
||||
|
||||
def _publish(self, data):
|
||||
pickled_data = pickle.dumps(
|
||||
{
|
||||
'type': 'message',
|
||||
'channel': self.channel,
|
||||
'data': data
|
||||
}
|
||||
)
|
||||
return self.sink.send(pickled_data)
|
||||
|
||||
def zmq_listen(self):
|
||||
while True:
|
||||
response = self.sub.recv()
|
||||
if response is not None:
|
||||
yield response
|
||||
|
||||
def _listen(self):
|
||||
for message in self.zmq_listen():
|
||||
if isinstance(message, bytes):
|
||||
try:
|
||||
message = pickle.loads(message)
|
||||
except Exception:
|
||||
pass
|
||||
if isinstance(message, dict) and \
|
||||
message['type'] == 'message' and \
|
||||
message['channel'] == self.channel and \
|
||||
'data' in message:
|
||||
yield message['data']
|
||||
return
|
||||
Reference in New Issue
Block a user