././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744472442.0610871 python_socketio-5.13.0/0000775000175000017500000000000014776504572014432 5ustar00miguelmiguel././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704499078.0 python_socketio-5.13.0/LICENSE0000644000175000017500000000207214546113606015423 0ustar00miguelmiguelThe MIT License (MIT) Copyright (c) 2015 Miguel Grinberg Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704499102.0 python_socketio-5.13.0/MANIFEST.in0000644000175000017500000000020614546113636016154 0ustar00miguelmiguelinclude README.md LICENSE tox.ini recursive-include docs * recursive-exclude docs/_build * recursive-include tests * exclude **/*.pyc ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744472442.0600872 python_socketio-5.13.0/PKG-INFO0000644000175000017500000000623314776504572015531 0ustar00miguelmiguelMetadata-Version: 2.4 Name: python-socketio Version: 5.13.0 Summary: Socket.IO server and client for Python Author-email: Miguel Grinberg Project-URL: Homepage, https://github.com/miguelgrinberg/python-socketio Project-URL: Bug Tracker, https://github.com/miguelgrinberg/python-socketio/issues Classifier: Environment :: Web Environment Classifier: Intended Audience :: Developers Classifier: Programming Language :: Python :: 3 Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Requires-Python: >=3.8 Description-Content-Type: text/markdown License-File: LICENSE Requires-Dist: bidict>=0.21.0 Requires-Dist: python-engineio>=4.11.0 Provides-Extra: client Requires-Dist: requests>=2.21.0; extra == "client" Requires-Dist: websocket-client>=0.54.0; extra == "client" Provides-Extra: asyncio-client Requires-Dist: aiohttp>=3.4; extra == "asyncio-client" Provides-Extra: docs Requires-Dist: sphinx; extra == "docs" Dynamic: license-file python-socketio =============== [![Build status](https://github.com/miguelgrinberg/python-socketio/workflows/build/badge.svg)](https://github.com/miguelgrinberg/python-socketio/actions) [![codecov](https://codecov.io/gh/miguelgrinberg/python-socketio/branch/main/graph/badge.svg)](https://codecov.io/gh/miguelgrinberg/python-socketio) Python implementation of the `Socket.IO` realtime client and server. Sponsors -------- The following organizations are funding this project: ![Socket.IO](https://images.opencollective.com/socketio/050e5eb/logo/64.png)
[Socket.IO](https://socket.io) | [Add your company here!](https://github.com/sponsors/miguelgrinberg)| -|- Many individual sponsors also support this project through small ongoing contributions. Why not [join them](https://github.com/sponsors/miguelgrinberg)? Version compatibility --------------------- The Socket.IO protocol has been through a number of revisions, and some of these introduced backward incompatible changes, which means that the client and the server must use compatible versions for everything to work. If you are using the Python client and server, the easiest way to ensure compatibility is to use the same version of this package for the client and the server. If you are using this package with a different client or server, then you must ensure the versions are compatible. The version compatibility chart below maps versions of this package to versions of the JavaScript reference implementation and the versions of the Socket.IO and Engine.IO protocols. JavaScript Socket.IO version | Socket.IO protocol revision | Engine.IO protocol revision | python-socketio version -|-|-|- 0.9.x | 1, 2 | 1, 2 | Not supported 1.x and 2.x | 3, 4 | 3 | 4.x 3.x and 4.x | 5 | 4 | 5.x Resources --------- - [Documentation](http://python-socketio.readthedocs.io/) - [PyPI](https://pypi.python.org/pypi/python-socketio) - [Change Log](https://github.com/miguelgrinberg/python-socketio/blob/main/CHANGES.md) - Questions? See the [questions](https://stackoverflow.com/questions/tagged/python-socketio) others have asked on Stack Overflow, or [ask](https://stackoverflow.com/questions/ask?tags=python+python-socketio) your own question. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1731704143.0 python_socketio-5.13.0/README.md0000644000175000017500000000425014715732517015703 0ustar00miguelmiguelpython-socketio =============== [![Build status](https://github.com/miguelgrinberg/python-socketio/workflows/build/badge.svg)](https://github.com/miguelgrinberg/python-socketio/actions) [![codecov](https://codecov.io/gh/miguelgrinberg/python-socketio/branch/main/graph/badge.svg)](https://codecov.io/gh/miguelgrinberg/python-socketio) Python implementation of the `Socket.IO` realtime client and server. Sponsors -------- The following organizations are funding this project: ![Socket.IO](https://images.opencollective.com/socketio/050e5eb/logo/64.png)
[Socket.IO](https://socket.io) | [Add your company here!](https://github.com/sponsors/miguelgrinberg)| -|- Many individual sponsors also support this project through small ongoing contributions. Why not [join them](https://github.com/sponsors/miguelgrinberg)? Version compatibility --------------------- The Socket.IO protocol has been through a number of revisions, and some of these introduced backward incompatible changes, which means that the client and the server must use compatible versions for everything to work. If you are using the Python client and server, the easiest way to ensure compatibility is to use the same version of this package for the client and the server. If you are using this package with a different client or server, then you must ensure the versions are compatible. The version compatibility chart below maps versions of this package to versions of the JavaScript reference implementation and the versions of the Socket.IO and Engine.IO protocols. JavaScript Socket.IO version | Socket.IO protocol revision | Engine.IO protocol revision | python-socketio version -|-|-|- 0.9.x | 1, 2 | 1, 2 | Not supported 1.x and 2.x | 3, 4 | 3 | 4.x 3.x and 4.x | 5 | 4 | 5.x Resources --------- - [Documentation](http://python-socketio.readthedocs.io/) - [PyPI](https://pypi.python.org/pypi/python-socketio) - [Change Log](https://github.com/miguelgrinberg/python-socketio/blob/main/CHANGES.md) - Questions? See the [questions](https://stackoverflow.com/questions/tagged/python-socketio) others have asked on Stack Overflow, or [ask](https://stackoverflow.com/questions/ask?tags=python+python-socketio) your own question. ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1744472442.056087 python_socketio-5.13.0/docs/0000775000175000017500000000000014776504572015362 5ustar00miguelmiguel././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704499080.0 python_socketio-5.13.0/docs/Makefile0000644000175000017500000000110414546113610016774 0ustar00miguelmiguel# Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1744472442.056087 python_socketio-5.13.0/docs/_static/0000775000175000017500000000000014776504572017010 5ustar00miguelmiguel././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704499080.0 python_socketio-5.13.0/docs/_static/README.md0000644000175000017500000000006314546113610020244 0ustar00miguelmiguelPlace static files used by the documentation here. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704499102.0 python_socketio-5.13.0/docs/_static/custom.css0000644000175000017500000000013614546113636021022 0ustar00miguelmigueldiv.sphinxsidebar { max-height: calc(100% - 30px); overflow-y: auto; overflow-x: hidden; } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704499102.0 python_socketio-5.13.0/docs/api.rst0000644000175000017500000000264514546113636016662 0ustar00miguelmiguelAPI Reference ============= .. toctree:: :maxdepth: 3 .. module:: socketio .. autoclass:: SimpleClient :members: :inherited-members: .. autoclass:: AsyncSimpleClient :members: :inherited-members: .. autoclass:: Client :members: :inherited-members: .. autoclass:: AsyncClient :members: :inherited-members: .. autoclass:: Server :members: :inherited-members: .. autoclass:: AsyncServer :members: :inherited-members: .. autoclass:: socketio.exceptions.ConnectionRefusedError :members: .. autoclass:: WSGIApp :members: .. autoclass:: ASGIApp :members: .. autoclass:: Middleware :members: .. autoclass:: ClientNamespace :members: :inherited-members: .. autoclass:: Namespace :members: :inherited-members: .. autoclass:: AsyncClientNamespace :members: :inherited-members: .. autoclass:: AsyncNamespace :members: :inherited-members: .. autoclass:: Manager :members: :inherited-members: .. autoclass:: PubSubManager :members: :inherited-members: .. autoclass:: KombuManager :members: :inherited-members: .. autoclass:: RedisManager :members: :inherited-members: .. autoclass:: KafkaManager :members: :inherited-members: .. autoclass:: AsyncManager :members: :inherited-members: .. autoclass:: AsyncRedisManager :members: :inherited-members: .. autoclass:: AsyncAioPikaManager :members: :inherited-members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1734543553.0 python_socketio-5.13.0/docs/client.rst0000664000175000017500000005256614730604301017365 0ustar00miguelmiguelThe Socket.IO Clients ===================== This package contains two Socket.IO clients: - a "simple" client, which provides a straightforward API that is sufficient for most applications - an "event-driven" client, which provides access to all the features of the Socket.IO protocol Each of these clients comes in two variants: one for the standard Python library, and another for asynchronous applications built with the ``asyncio`` package. Installation ------------ To install the standard Python client along with its dependencies, use the following command:: pip install "python-socketio[client]" If instead you plan on using the ``asyncio`` client, then use this:: pip install "python-socketio[asyncio_client]" Using the Simple Client ----------------------- The advantage of the simple client is that it abstracts away the logic required to maintain a Socket.IO connection. This client handles disconnections and reconnections in a completely transparent way, without adding any complexity to the application. Creating a Client Instance ~~~~~~~~~~~~~~~~~~~~~~~~~~ The easiest way to create a Socket.IO client is to use the context manager interface:: import socketio # standard Python with socketio.SimpleClient() as sio: # ... connect to a server and use the client # ... no need to manually disconnect! # asyncio async with socketio.AsyncSimpleClient() as sio: # ... connect to a server and use the client # ... no need to manually disconnect! With this usage the context manager will ensure that the client is properly disconnected before exiting the ``with`` or ``async with`` block. If preferred, a client can be manually instantiated:: import socketio # standard Python sio = socketio.SimpleClient() # asyncio sio = socketio.AsyncSimpleClient() Connecting to a Server ~~~~~~~~~~~~~~~~~~~~~~ The connection to a server is established by calling the ``connect()`` method:: sio.connect('http://localhost:5000') In the case of the ``asyncio`` client, the method is a coroutine:: await sio.connect('http://localhost:5000') By default the client first connects to the server using the long-polling transport, and then attempts to upgrade the connection to use WebSocket. To connect directly using WebSocket, use the ``transports`` argument:: sio.connect('http://localhost:5000', transports=['websocket']) Upon connection, the server assigns the client a unique session identifier. The application can find this identifier in the ``sid`` attribute:: print('my sid is', sio.sid) The Socket.IO transport that is used in the connection can be obtained from the ``transport`` attribute:: print('my transport is', sio.transport) The transport is given as a string, and can be either ``'websocket'`` or ``'polling'``. TLS/SSL Support ^^^^^^^^^^^^^^^ The client supports TLS/SSL connections. To enable it, use a ``https://`` connection URL:: sio.connect('https://example.com') Or when using ``asyncio``:: await sio.connect('https://example.com') The client verifies server certificates by default. Consult the documentation for the event-driven client for information on how to customize this behavior. Emitting Events ~~~~~~~~~~~~~~~ The client can emit an event to the server using the ``emit()`` method:: sio.emit('my message', {'foo': 'bar'}) Or in the case of ``asyncio``, as a coroutine:: await sio.emit('my message', {'foo': 'bar'}) The arguments provided to the method are the name of the event to emit and the optional data that is passed on to the server. The data can be of type ``str``, ``bytes``, ``dict``, ``list`` or ``tuple``. When sending a ``list`` or a ``tuple``, the elements in it need to be of any allowed types except ``tuple``. When a tuple is used, the elements of the tuple will be passed as individual arguments to the server-side event handler function. Receiving Events ~~~~~~~~~~~~~~~~ The client can wait for the server to emit an event with the ``receive()`` method:: event = sio.receive() print(f'received event: "{event[0]}" with arguments {event[1:]}') When using ``asyncio``, this method needs to be awaited:: event = await sio.receive() print(f'received event: "{event[0]}" with arguments {event[1:]}') The return value of ``receive()`` is a list. The first element of this list is the event name, while the remaining elements are the arguments passed by the server. With the usage shown above, the ``receive()`` method will return only when an event is received from the server. An optional timeout in seconds can be passed to prevent the client from waiting forever:: from socketio.exceptions import TimeoutError try: event = sio.receive(timeout=5) except TimeoutError: print('timed out waiting for event') else: print('received event:', event) Or with ``asyncio``:: from socketio.exceptions import TimeoutError try: event = await sio.receive(timeout=5) except TimeoutError: print('timed out waiting for event') else: print('received event:', event) Disconnecting from the Server ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ At any time the client can request to be disconnected from the server by invoking the ``disconnect()`` method:: sio.disconnect() For the ``asyncio`` client this is a coroutine:: await sio.disconnect() Debugging and Troubleshooting ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To help you debug issues, the client can be configured to output logs to the terminal:: import socketio # standard Python sio = socketio.Client(logger=True, engineio_logger=True) # asyncio sio = socketio.AsyncClient(logger=True, engineio_logger=True) The ``logger`` argument controls logging related to the Socket.IO protocol, while ``engineio_logger`` controls logs that originate in the low-level Engine.IO transport. These arguments can be set to ``True`` to output logs to ``stderr``, or to an object compatible with Python's ``logging`` package where the logs should be emitted to. A value of ``False`` disables logging. Logging can help identify the cause of connection problems, unexpected disconnections and other issues. Using the Event-Driven Client ----------------------------- Creating a Client Instance ~~~~~~~~~~~~~~~~~~~~~~~~~~ To instantiate an Socket.IO client, simply create an instance of the appropriate client class:: import socketio # standard Python sio = socketio.Client() # asyncio sio = socketio.AsyncClient() Defining Event Handlers ~~~~~~~~~~~~~~~~~~~~~~~ The Socket.IO protocol is event based. When a server wants to communicate with a client it *emits* an event. Each event has a name, and a list of arguments. The client registers event handler functions with the :func:`socketio.Client.event` or :func:`socketio.Client.on` decorators:: @sio.event def message(data): print('I received a message!') @sio.on('my message') def on_message(data): print('I received a message!') In the first example the event name is obtained from the name of the handler function. The second example is slightly more verbose, but it allows the event name to be different than the function name or to include characters that are illegal in function names, such as spaces. For the ``asyncio`` client, event handlers can be regular functions as above, or can also be coroutines:: @sio.event async def message(data): print('I received a message!') If the server includes arguments with an event, those are passed to the handler function as arguments. Catch-All Event and Namespace Handlers ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A "catch-all" event handler is invoked for any events that do not have an event handler. You can define a catch-all handler using ``'*'`` as event name:: @sio.on('*') def any_event(event, sid, data): pass Asyncio servers can also use a coroutine:: @sio.on('*') async def any_event(event, sid, data): pass A catch-all event handler receives the event name as a first argument. The remaining arguments are the same as for a regular event handler. The ``connect`` and ``disconnect`` events have to be defined explicitly and are not invoked on a catch-all event handler. Similarily, a "catch-all" namespace handler is invoked for any connected namespaces that do not have an explicitly defined event handler. As with catch-all events, ``'*'`` is used in place of a namespace:: @sio.on('my_event', namespace='*') def my_event_any_namespace(namespace, sid, data): pass For these events, the namespace is passed as first argument, followed by the regular arguments of the event. Lastly, it is also possible to define a "catch-all" handler for all events on all namespaces:: @sio.on('*', namespace='*') def any_event_any_namespace(event, namespace, sid, data): pass Event handlers with catch-all events and namespaces receive the event name and the namespace as first and second arguments. Connect, Connect Error and Disconnect Event Handlers ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The ``connect``, ``connect_error`` and ``disconnect`` events are special; they are invoked automatically when a client connects or disconnects from the server:: @sio.event def connect(): print("I'm connected!") @sio.event def connect_error(data): print("The connection failed!") @sio.event def disconnect(reason): print("I'm disconnected! reason:", reason) The ``connect_error`` handler is invoked when a connection attempt fails. If the server provides arguments, these are passed on to the handler. The server can use an argument to provide information to the client regarding the connection failure. The ``disconnect`` handler is invoked for application initiated disconnects, server initiated disconnects, or accidental disconnects, for example due to networking failures. In the case of an accidental disconnection, the client is going to attempt to reconnect immediately after invoking the disconnect handler. As soon as the connection is re-established the connect handler will be invoked once again. The handler receives a ``reason`` argument which provides the cause of the disconnection:: @sio.event def disconnect(reason): if reason == sio.reason.CLIENT_DISCONNECT: print('the client disconnected') elif reason == sio.reason.SERVER_DISCONNECT: print('the server disconnected the client') else: print('disconnect reason:', reason) See the The :attr:`socketio.Client.reason` attribute for a list of possible disconnection reasons. The ``connect``, ``connect_error`` and ``disconnect`` events have to be defined explicitly and are not invoked on a catch-all event handler. Connecting to a Server ~~~~~~~~~~~~~~~~~~~~~~ The connection to a server is established by calling the ``connect()`` method:: sio.connect('http://localhost:5000') In the case of the ``asyncio`` client, the method is a coroutine:: await sio.connect('http://localhost:5000') Upon connection, the server assigns the client a unique session identifier. The application can find this identifier in the ``sid`` attribute:: print('my sid is', sio.sid) The Socket.IO transport that is used in the connection can be obtained from the ``transport`` attribute:: print('my transport is', sio.transport) The transport is given as a string, and can be either ``'websocket'`` or ``'polling'``. TLS/SSL Support ^^^^^^^^^^^^^^^ The client supports TLS/SSL connections. To enable it, use a ``https://`` connection URL:: sio.connect('https://example.com') Or when using ``asyncio``:: await sio.connect('https://example.com') The client will verify the server certificate by default. To disable certificate verification, or to use other less common options such as client certificates, the client must be initialized with a custom HTTP session object that is configured with the desired TLS/SSL options. The following example disables server certificate verification, which can be useful when connecting to a server that uses a self-signed certificate:: http_session = requests.Session() http_session.verify = False sio = socketio.Client(http_session=http_session) sio.connect('https://example.com') And when using ``asyncio``:: connector = aiohttp.TCPConnector(ssl=False) http_session = aiohttp.ClientSession(connector=connector) sio = socketio.AsyncClient(http_session=http_session) await sio.connect('https://example.com') Instead of disabling certificate verification, you can provide a custom certificate authority bundle to verify the certificate against:: http_session = requests.Session() http_session.verify = '/path/to/ca.pem' sio = socketio.Client(http_session=http_session) sio.connect('https://example.com') And for ``asyncio``:: ssl_context = ssl.create_default_context() ssl_context.load_verify_locations('/path/to/ca.pem') connector = aiohttp.TCPConnector(ssl=ssl_context) http_session = aiohttp.ClientSession(connector=connector) sio = socketio.AsyncClient(http_session=http_session) await sio.connect('https://example.com') Below you can see how to use a client certificate to authenticate against the server:: http_session = requests.Session() http_session.cert = ('/path/to/client/cert.pem', '/path/to/client/key.pem') sio = socketio.Client(http_session=http_session) sio.connect('https://example.com') And for ``asyncio``:: ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) ssl_context.load_cert_chain('/path/to/client/cert.pem', '/path/to/client/key.pem') connector = aiohttp.TCPConnector(ssl=ssl_context) http_session = aiohttp.ClientSession(connector=connector) sio = socketio.AsyncClient(http_session=http_session) await sio.connect('https://example.com') Emitting Events ~~~~~~~~~~~~~~~ The client can emit an event to the server using the ``emit()`` method:: sio.emit('my message', {'foo': 'bar'}) Or in the case of ``asyncio``, as a coroutine:: await sio.emit('my message', {'foo': 'bar'}) The arguments provided to the method are the name of the event to emit and the optional data that is passed on to the server. The data can be of type ``str``, ``bytes``, ``dict``, ``list`` or ``tuple``. When sending a ``list`` or a ``tuple``, the elements in it need to be of any allowed types except ``tuple``. When a tuple is used, the elements of the tuple will be passed as individual arguments to the server-side event handler function. The ``emit()`` method can be invoked inside an event handler as a response to a server event, or in any other part of the application, including in background tasks. Event Callbacks ~~~~~~~~~~~~~~~ When a server emits an event to a client, it can optionally provide a callback function, to be invoked as a way of acknowledgment that the server has processed the event. While this is entirely managed by the server, the client can provide a list of return values that are to be passed on to the callback function set up by the server. This is achieved simply by returning the desired values from the handler function:: @sio.event def my_event(sid, data): # handle the message return "OK", 123 Likewise, the client can request a callback function to be invoked after the server has processed an event. The :func:`socketio.Server.emit` method has an optional ``callback`` argument that can be set to a callable. If this argument is given, the callable will be invoked after the server has processed the event, and any values returned by the server handler will be passed as arguments to this function. Namespaces ~~~~~~~~~~ The Socket.IO protocol supports multiple logical connections, all multiplexed on the same physical connection. Clients can open multiple connections by specifying a different *namespace* on each. Namespaces use a path syntax starting with a forward slash. A list of namespaces can be given by the client in the ``connect()`` call. For example, this example creates two logical connections, the default one plus a second connection under the ``/chat`` namespace:: sio.connect('http://localhost:5000', namespaces=['/chat']) To define event handlers on a namespace, the ``namespace`` argument must be added to the corresponding decorator:: @sio.event(namespace='/chat') def my_custom_event(sid, data): pass @sio.on('connect', namespace='/chat') def on_connect(): print("I'm connected to the /chat namespace!") Likewise, the client can emit an event to the server on a namespace by providing its in the ``emit()`` call:: sio.emit('my message', {'foo': 'bar'}, namespace='/chat') If the ``namespaces`` argument of the ``connect()`` call isn't given, any namespaces used in event handlers are automatically connected. Class-Based Namespaces ~~~~~~~~~~~~~~~~~~~~~~ As an alternative to the decorator-based event handlers, the event handlers that belong to a namespace can be created as methods of a subclass of :class:`socketio.ClientNamespace`:: class MyCustomNamespace(socketio.ClientNamespace): def on_connect(self): pass def on_disconnect(self, reason): pass def on_my_event(self, data): self.emit('my_response', data) sio.register_namespace(MyCustomNamespace('/chat')) For asyncio based servers, namespaces must inherit from :class:`socketio.AsyncClientNamespace`, and can define event handlers as coroutines if desired:: class MyCustomNamespace(socketio.AsyncClientNamespace): def on_connect(self): pass def on_disconnect(self, reason): pass async def on_my_event(self, data): await self.emit('my_response', data) sio.register_namespace(MyCustomNamespace('/chat')) A catch-all class-based namespace handler can be defined by passing ``'*'`` as the namespace during registration:: sio.register_namespace(MyCustomNamespace('*')) When class-based namespaces are used, any events received by the client are dispatched to a method named as the event name with the ``on_`` prefix. For example, event ``my_event`` will be handled by a method named ``on_my_event``. If an event is received for which there is no corresponding method defined in the namespace class, then the event is ignored. All event names used in class-based namespaces must use characters that are legal in method names. As a convenience to methods defined in a class-based namespace, the namespace instance includes versions of several of the methods in the :class:`socketio.Client` and :class:`socketio.AsyncClient` classes that default to the proper namespace when the ``namespace`` argument is not given. In the case that an event has a handler in a class-based namespace, and also a decorator-based function handler, only the standalone function handler is invoked. Disconnecting from the Server ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ At any time the client can request to be disconnected from the server by invoking the ``disconnect()`` method:: sio.disconnect() For the ``asyncio`` client this is a coroutine:: await sio.disconnect() Managing Background Tasks ~~~~~~~~~~~~~~~~~~~~~~~~~ When a client connection to the server is established, a few background tasks will be spawned to keep the connection alive and handle incoming events. The application running on the main thread is free to do any work, as this is not going to prevent the functioning of the Socket.IO client. If the application does not have anything to do in the main thread and just wants to wait until the connection with the server ends, it can call the ``wait()`` method:: sio.wait() Or in the ``asyncio`` version:: await sio.wait() For the convenience of the application, a helper function is provided to start a custom background task:: def my_background_task(my_argument): # do some background work here! pass task = sio.start_background_task(my_background_task, 123) The arguments passed to this method are the background function and any positional or keyword arguments to invoke the function with. Here is the ``asyncio`` version:: async def my_background_task(my_argument): # do some background work here! pass task = sio.start_background_task(my_background_task, 123) Note that this function is not a coroutine, since it does not wait for the background function to end. The background function must be a coroutine. The ``sleep()`` method is a second convenience function that is provided for the benefit of applications working with background tasks of their own:: sio.sleep(2) Or for ``asyncio``:: await sio.sleep(2) The single argument passed to the method is the number of seconds to sleep for. Debugging and Troubleshooting ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To help you debug issues, the client can be configured to output logs to the terminal:: import socketio # standard Python sio = socketio.Client(logger=True, engineio_logger=True) # asyncio sio = socketio.AsyncClient(logger=True, engineio_logger=True) The ``logger`` argument controls logging related to the Socket.IO protocol, while ``engineio_logger`` controls logs that originate in the low-level Engine.IO transport. These arguments can be set to ``True`` to output logs to ``stderr``, or to an object compatible with Python's ``logging`` package where the logs should be emitted to. A value of ``False`` disables logging. Logging can help identify the cause of connection problems, unexpected disconnections and other issues. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1718073652.0 python_socketio-5.13.0/docs/conf.py0000644000175000017500000001264414631734464016661 0ustar00miguelmiguel# -*- coding: utf-8 -*- # # Configuration file for the Sphinx documentation builder. # # This file does only contain a selection of the most common options. For a # full list see the documentation: # http://www.sphinx-doc.org/en/master/config # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # # import os # import sys # sys.path.insert(0, os.path.abspath('.')) # -- Project information ----------------------------------------------------- project = 'python-socketio' copyright = '2018, Miguel Grinberg' author = 'Miguel Grinberg' # The short X.Y version version = '' # The full version, including alpha/beta/rc tags release = '' # -- General configuration --------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. # # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', ] autodoc_member_order = 'alphabetical' # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The master toctree document. master_doc = 'index' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = 'en' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] # The name of the Pygments (syntax highlighting) style to use. pygments_style = None # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = 'alabaster' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = { 'github_user': 'miguelgrinberg', 'github_repo': 'python-socketio', 'github_banner': True, 'github_button': True, 'github_type': 'star', 'fixed_sidebar': True, } # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Custom sidebar templates, must be a dictionary that maps document names # to template names. # # The default sidebars (for documents that don't match any pattern) are # defined by theme itself. Builtin themes are using these templates by # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', # 'searchbox.html']``. # # html_sidebars = {} # -- Options for HTMLHelp output --------------------------------------------- # Output file base name for HTML help builder. htmlhelp_basename = 'python-socketiodoc' # -- Options for LaTeX output ------------------------------------------------ latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # # 'preamble': '', # Latex figure (float) alignment # # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'python-socketio.tex', 'python-socketio Documentation', 'Miguel Grinberg', 'manual'), ] # -- Options for manual page output ------------------------------------------ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'python-socketio', 'python-socketio Documentation', [author], 1) ] # -- Options for Texinfo output ---------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'python-socketio', 'python-socketio Documentation', author, 'python-socketio', 'One line description of project.', 'Miscellaneous'), ] # -- Options for Epub output ------------------------------------------------- # Bibliographic Dublin Core info. epub_title = project # The unique identifier of the text. This can be a ISBN number # or the project homepage. # # epub_identifier = '' # A unique identification for the text. # # epub_uid = '' # A list of files that should not be packed into the epub file. epub_exclude_files = ['search.html'] # -- Extension configuration ------------------------------------------------- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704499081.0 python_socketio-5.13.0/docs/index.rst0000644000175000017500000000100114546113611017172 0ustar00miguelmiguel.. python-socketio documentation master file, created by sphinx-quickstart on Sun Nov 25 11:52:38 2018. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. python-socketio =============== This projects implements Socket.IO clients and servers that can run standalone or integrated with a variety of Python web frameworks. .. toctree:: :maxdepth: 3 intro client server api * :ref:`genindex` * :ref:`modindex` * :ref:`search` ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1735472798.0 python_socketio-5.13.0/docs/intro.rst0000664000175000017500000001647314734233236017250 0ustar00miguelmiguel.. socketio documentation master file, created by sphinx-quickstart on Sat Jun 13 23:41:23 2015. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Getting Started =============== What is Socket.IO? ------------------ Socket.IO is a transport protocol that enables real-time bidirectional event-based communication between clients (typically, though not always, web browsers) and a server. The official implementations of the client and server components are written in JavaScript. This package provides Python implementations of both, each with standard and asyncio variants. Version compatibility --------------------- The Socket.IO protocol has been through a number of revisions, and some of these introduced backward incompatible changes, which means that the client and the server must use compatible versions for everything to work. If you are using the Python client and server, the easiest way to ensure compatibility is to use the same version of this package for the client and the server. If you are using this package with a different client or server, then you must ensure the versions are compatible. The version compatibility chart below maps versions of this package to versions of the JavaScript reference implementation and the versions of the Socket.IO and Engine.IO protocols. +------------------------------+-----------------------------+-----------------------------+-------------------------+-------------------------+ | JavaScript Socket.IO version | Socket.IO protocol revision | Engine.IO protocol revision | python-socketio version | python-engineio version | +==============================+=============================+=============================+=========================+=========================+ | 0.9.x | 1, 2 | 1, 2 | Not supported | Not supported | +------------------------------+-----------------------------+-----------------------------+-------------------------+-------------------------+ | 1.x and 2.x | 3, 4 | 3 | 4.x | 3.x | +------------------------------+-----------------------------+-----------------------------+-------------------------+-------------------------+ | 3.x and 4.x | 5 | 4 | 5.x | 4.x | +------------------------------+-----------------------------+-----------------------------+-------------------------+-------------------------+ Client Examples --------------- The example that follows shows a simple Python client: .. code:: python import socketio sio = socketio.Client() @sio.event def connect(): print('connection established') @sio.event def my_message(data): print('message received with ', data) sio.emit('my response', {'response': 'my response'}) @sio.event def disconnect(): print('disconnected from server') sio.connect('http://localhost:5000') sio.wait() Below is a similar client, coded for ``asyncio`` (Python 3.5+ only): .. code:: python import asyncio import socketio sio = socketio.AsyncClient() @sio.event async def connect(): print('connection established') @sio.event async def my_message(data): print('message received with ', data) await sio.emit('my response', {'response': 'my response'}) @sio.event async def disconnect(): print('disconnected from server') async def main(): await sio.connect('http://localhost:5000') await sio.wait() if __name__ == '__main__': asyncio.run(main()) Client Features --------------- - Can connect to other Socket.IO servers that are compatible with the JavaScript Socket.IO reference server. - Compatible with Python 3.8+. - Two versions of the client, one for standard Python and another for asyncio. - Uses an event-based architecture implemented with decorators that hides the details of the protocol. - Implements HTTP long-polling and WebSocket transports. - Automatically reconnects to the server if the connection is dropped. Server Examples --------------- The following application is a basic server example that uses the Eventlet asynchronous server: .. code:: python import eventlet import socketio sio = socketio.Server() app = socketio.WSGIApp(sio, static_files={ '/': {'content_type': 'text/html', 'filename': 'index.html'} }) @sio.event def connect(sid, environ): print('connect ', sid) @sio.event def my_message(sid, data): print('message ', data) @sio.event def disconnect(sid): print('disconnect ', sid) if __name__ == '__main__': eventlet.wsgi.server(eventlet.listen(('', 5000)), app) Below is a similar application, coded for ``asyncio`` (Python 3.5+ only) and the Uvicorn web server: .. code:: python from aiohttp import web import socketio sio = socketio.AsyncServer() app = web.Application() sio.attach(app) async def index(request): """Serve the client-side application.""" with open('index.html') as f: return web.Response(text=f.read(), content_type='text/html') @sio.event def connect(sid, environ): print("connect ", sid) @sio.event async def chat_message(sid, data): print("message ", data) @sio.event def disconnect(sid): print('disconnect ', sid) app.router.add_static('/static', 'static') app.router.add_get('/', index) if __name__ == '__main__': web.run_app(app) Server Features --------------- - Can connect to servers running other Socket.IO clients that are compatible with the JavaScript reference client. - Compatible with Python 3.8+. - Two versions of the server, one for standard Python and another for asyncio. - Supports large number of clients even on modest hardware due to being asynchronous. - Can be hosted on any `WSGI `_ or `ASGI `_ web server including `Gunicorn `_, `Uvicorn `_, `eventlet `_ and `gevent `_. - Can be integrated with WSGI applications written in frameworks such as Flask, Django, etc. - Can be integrated with `aiohttp `_, `FastAPI `_, `sanic `_ and `tornado `_ ``asyncio`` applications. - Broadcasting of messages to all connected clients, or to subsets of them assigned to "rooms". - Optional support for multiple servers, connected through a messaging queue such as Redis or RabbitMQ. - Send messages to clients from external processes, such as Celery workers or auxiliary scripts. - Event-based architecture implemented with decorators that hides the details of the protocol. - Support for HTTP long-polling and WebSocket transports. - Support for XHR2 and XHR browsers. - Support for text and binary messages. - Support for gzip and deflate HTTP compression. - Configurable CORS responses, to avoid cross-origin problems with browsers. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704499080.0 python_socketio-5.13.0/docs/make.bat0000644000175000017500000000142314546113610016745 0ustar00miguelmiguel@ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=. set BUILDDIR=_build if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% :end popd ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744472116.0 python_socketio-5.13.0/docs/server.rst0000664000175000017500000012347314776504064017430 0ustar00miguelmiguelThe Socket.IO Server ==================== This package contains two Socket.IO servers: - The :func:`socketio.Server` class creates a server compatible with the Python standard library. - The :func:`socketio.AsyncServer` class creates a server compatible with the ``asyncio`` package. The methods in the two servers are the same, with the only difference that in the ``asyncio`` server most methods are implemented as coroutines. Installation ------------ To install the Socket.IO server along with its dependencies, use the following command:: pip install python-socketio Creating a Server Instance -------------------------- A Socket.IO server is an instance of class :class:`socketio.Server`:: import socketio # create a Socket.IO server sio = socketio.Server() For asyncio based servers, the :class:`socketio.AsyncServer` class provides the same functionality, but in a coroutine friendly format:: import socketio # create a Socket.IO server sio = socketio.AsyncServer() Running the Server ------------------ To run the Socket.IO application it is necessary to configure a web server to receive incoming requests from clients and forward them to the Socket.IO server instance. To simplify this task, several integrations are available, including support for the `WSGI `_ and `ASGI `_ standards. Running as a WSGI Application ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To configure the Socket.IO server as a WSGI application wrap the server instance with the :class:`socketio.WSGIApp` class:: # wrap with a WSGI application app = socketio.WSGIApp(sio) The resulting WSGI application can be executed with supported WSGI servers such as `Werkzeug `_ for development and `Gunicorn `_ for production. When combining Socket.IO with a web application written with a WSGI framework such as Flask or Django, the ``WSGIApp`` class can wrap both applications together and route traffic to them:: from mywebapp import app # a Flask, Django, etc. application app = socketio.WSGIApp(sio, app) Running as an ASGI Application ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To configure the Socket.IO server as an ASGI application wrap the server instance with the :class:`socketio.ASGIApp` class:: # wrap with ASGI application app = socketio.ASGIApp(sio) The resulting ASGI application can be executed with an ASGI compliant web server, for example `Uvicorn `_. Socket.IO can also be combined with a web application written with an ASGI web framework such as FastAPI. In that case, the ``ASGIApp`` class can wrap both applications together and route traffic to them:: from mywebapp import app # a FastAPI or other ASGI application app = socketio.ASGIApp(sio, app) Serving Static Files ~~~~~~~~~~~~~~~~~~~~ The Socket.IO server can be configured to serve static files to clients. This is particularly useful to deliver HTML, CSS and JavaScript files to clients when this package is used without a companion web framework. Static files are configured with a Python dictionary in which each key/value pair is a static file mapping rule. In its simplest form, this dictionary has one or more static file URLs as keys, and the corresponding files in the server as values:: static_files = { '/': 'latency.html', '/static/socket.io.js': 'static/socket.io.js', '/static/style.css': 'static/style.css', } With this example configuration, when the server receives a request for ``/`` (the root URL) it will return the contents of the file ``latency.html`` in the current directory, and will assign a content type based on the file extension, in this case ``text/html``. Files with the ``.html``, ``.css``, ``.js``, ``.json``, ``.jpg``, ``.png``, ``.gif`` and ``.txt`` file extensions are automatically recognized and assigned the correct content type. For files with other file extensions or with no file extension, the ``application/octet-stream`` content type is used as a default. If desired, an explicit content type for a static file can be given as follows:: static_files = { '/': {'filename': 'latency.html', 'content_type': 'text/plain'}, } It is also possible to configure an entire directory in a single rule, so that all the files in it are served as static files:: static_files = { '/static': './public', } In this example any files with URLs starting with ``/static`` will be served directly from the ``public`` folder in the current directory, so for example, the URL ``/static/index.html`` will return local file ``./public/index.html`` and the URL ``/static/css/styles.css`` will return local file ``./public/css/styles.css``. If a URL that ends in a ``/`` is requested, then a default filename of ``index.html`` is appended to it. In the previous example, a request for the ``/static/`` URL would return local file ``./public/index.html``. The default filename to serve for slash-ending URLs can be set in the static files dictionary with an empty key:: static_files = { '/static': './public', '': 'image.gif', } With this configuration, a request for ``/static/`` would return local file ``./public/image.gif``. A non-standard content type can also be specified if needed:: static_files = { '/static': './public', '': {'filename': 'image.gif', 'content_type': 'text/plain'}, } The static file configuration dictionary is given as the ``static_files`` argument to the ``socketio.WSGIApp`` or ``socketio.ASGIApp`` classes:: # for standard WSGI applications sio = socketio.Server() app = socketio.WSGIApp(sio, static_files=static_files) # for asyncio-based ASGI applications sio = socketio.AsyncServer() app = socketio.ASGIApp(sio, static_files=static_files) The routing precedence in these two classes is as follows: - First, the path is checked against the Socket.IO endpoint. - Next, the path is checked against the static file configuration, if present. - If the path did not match the Socket.IO endpoint or any static file, control is passed to the secondary application if configured, else a 404 error is returned. Note: static file serving is intended for development use only, and as such it lacks important features such as caching. Do not use in a production environment. Events ------ The Socket.IO protocol is event based. When a client wants to communicate with the server, or the server wants to communicate with one or more clients, they *emit* an event to the other party. Each event has a name, and an optional list of arguments. Listening to Events ~~~~~~~~~~~~~~~~~~~ To receive events from clients, the server application must register event handler functions. These functions are invoked when the corresponding events are emitted by clients. To register a handler for an event, the :func:`socketio.Server.event` or :func:`socketio.Server.on` decorators are used:: @sio.event def my_event(sid, data): pass @sio.on('my custom event') def another_event(sid, data): pass In the first example the event name is obtained from the name of the handler function. The second example is slightly more verbose, but it allows the event name to be different than the function name or to include characters that are illegal in function names, such as spaces. For asyncio servers, event handlers can optionally be given as coroutines:: @sio.event async def my_event(sid, data): pass The ``sid`` argument that is passed to all handlers is the Socket.IO session id, a unique identifier that Socket.IO assigns to each client connection. All the events sent by a given client will have the same ``sid`` value. Connect and Disconnect Events ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The ``connect`` and ``disconnect`` events are special; they are invoked automatically when a client connects or disconnects from the server:: @sio.event def connect(sid, environ, auth): print('connect ', sid) @sio.event def disconnect(sid, reason): print('disconnect ', sid, reason) The ``connect`` event is an ideal place to perform user authentication, and any necessary mapping between user entities in the application and the ``sid`` that was assigned to the client. In addition to the ``sid``, the connect handler receives ``environ`` as an argument, with the request information in standard WSGI format, including HTTP headers. The connect handler also receives the ``auth`` argument with any authentication details passed by the client, or ``None`` if the client did not pass any authentication. After inspecting the arguments, the connect event handler can return ``False`` to reject the connection with the client. Sometimes it is useful to pass data back to the client being rejected. In that case instead of returning ``False`` a :class:`socketio.exceptions.ConnectionRefusedError` exception can be raised, and all of its arguments will be sent to the client with the rejection message:: @sio.event def connect(sid, environ, auth): raise ConnectionRefusedError('authentication failed') The disconnect handler receives the ``sid`` assigned to the client and a ``reason``, which provides the cause of the disconnection:: @sio.event def disconnect(sid, reason): if reason == sio.reason.CLIENT_DISCONNECT: print('the client disconnected') elif reason == sio.reason.SERVER_DISCONNECT: print('the server disconnected the client') else: print('disconnect reason:', reason) See the The :attr:`socketio.Server.reason` attribute for a list of possible disconnection reasons. Catch-All Event Handlers ~~~~~~~~~~~~~~~~~~~~~~~~ A "catch-all" event handler is invoked for any events that do not have an event handler. You can define a catch-all handler using ``'*'`` as event name:: @sio.on('*') def any_event(event, sid, data): pass Asyncio servers can also use a coroutine:: @sio.on('*') async def any_event(event, sid, data): pass A catch-all event handler receives the event name as a first argument. The remaining arguments are the same as for a regular event handler. Note that the ``connect`` and ``disconnect`` events have to be defined explicitly and are not invoked on a catch-all event handler. Emitting Events to Clients ~~~~~~~~~~~~~~~~~~~~~~~~~~ Socket.IO is a bidirectional protocol, so at any time the server can send an event to its connected clients. The :func:`socketio.Server.emit` method is used for this task:: sio.emit('my event', {'data': 'foobar'}) The first argument is the event name, followed by an optional data payload of type ``str``, ``bytes``, ``list``, ``dict`` or ``tuple``. When sending a ``list``, ``dict`` or ``tuple``, the elements are also constrained to the same data types. When a ``tuple`` is sent, the elements of the tuple will be passed as multiple arguments to the client-side event handler function. The above example will send the event to all the clients are connected. Sometimes the server may want to send an event just to one particular client. This can be achieved by adding a ``to`` argument to the emit call, with the ``sid`` of the client:: sio.emit('my event', {'data': 'foobar'}, to=user_sid) The ``to`` argument is used to identify the client that should receive the event, and is set to the ``sid`` value assigned to that client's connection with the server. When ``to`` is omitted, the event is broadcasted to all connected clients. Acknowledging Events ~~~~~~~~~~~~~~~~~~~~ When a client sends an event to the server, it can optionally request to receive acknowledgment from the server. The sending of acknowledgements is automatically managed by the Socket.IO server, but the event handler function can provide a list of values that are to be passed on to the client with the acknowledgement simply by returning them:: @sio.event def my_event(sid, data): # handle the message return "OK", 123 # <-- client will have these as acknowledgement Requesting Client Acknowledgements ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Similar to how clients can request acknowledgements from the server, when the server is emitting to a single client it can also ask the client to acknowledge the event, and optionally return one or more values as a response. The Socket.IO server supports two ways of working with client acknowledgements. The most convenient method is to replace :func:`socketio.Server.emit` with :func:`socketio.Server.call`. The ``call()`` method will emit the event, and then wait until the client sends an acknowledgement, returning any values provided by the client:: response = sio.call('my event', {'data': 'foobar'}, to=user_sid) A much more primitive acknowledgement solution uses callback functions. The :func:`socketio.Server.emit` method has an optional ``callback`` argument that can be set to a callable. If this argument is given, the callable will be invoked after the client has processed the event, and any values returned by the client will be passed as arguments to this function:: def my_callback(): print("callback invoked!") sio.emit('my event', {'data': 'foobar'}, to=user_sid, callback=my_callback) Rooms ----- To make it easy for the server to emit events to groups of related clients, the application can put its clients into "rooms", and then address messages to these rooms. In previous examples, the ``to`` argument of the :func:`socketio.SocketIO.emit` method was used to designate a specific client as the recipient of the event. The ``to`` argument can also be given the name of a room, and then all the clients that are in that room will receive the event. The application can create as many rooms as needed and manage which clients are in them using the :func:`socketio.Server.enter_room` and :func:`socketio.Server.leave_room` methods. Clients can be in as many rooms as needed and can be moved between rooms when necessary. :: @sio.event def begin_chat(sid): sio.enter_room(sid, 'chat_users') @sio.event def exit_chat(sid): sio.leave_room(sid, 'chat_users') In chat applications it is often desired that an event is broadcasted to all the members of the room except one, which is the originator of the event such as a chat message. The :func:`socketio.Server.emit` method provides an optional ``skip_sid`` argument to indicate a client that should be skipped during the broadcast. :: @sio.event def my_message(sid, data): sio.emit('my reply', data, room='chat_users', skip_sid=sid) Namespaces ---------- The Socket.IO protocol supports multiple logical connections, all multiplexed on the same physical connection. Clients can open multiple connections by specifying a different *namespace* on each. A namespace is given by the client as a pathname following the hostname and port. For example, connecting to *http://example.com:8000/chat* would open a connection to the namespace */chat*. Each namespace works independently from the others, with separate session IDs (``sid``\ s), event handlers and rooms. Namespaces can be defined directly in the event handler functions, or they can also be created as classes. Decorator-Based Namespaces ~~~~~~~~~~~~~~~~~~~~~~~~~~ Decorator-based namespaces are regular event handlers that include the ``namespace`` argument in their decorator:: @sio.event(namespace='/chat') def my_custom_event(sid, data): pass @sio.on('my custom event', namespace='/chat') def my_custom_event(sid, data): pass When emitting an event, the ``namespace`` optional argument is used to specify which namespace to send it on. When the ``namespace`` argument is omitted, the default Socket.IO namespace, which is named ``/``, is used. It is important that applications that use multiple namespaces specify the correct namespace when setting up their event handlers and rooms using the optional ``namespace`` argument. This argument must also be specified when emitting events under a namespace. Most methods in the :class:`socketio.Server` class have the optional ``namespace`` argument. Class-Based Namespaces ~~~~~~~~~~~~~~~~~~~~~~ As an alternative to the decorator-based namespaces, the event handlers that belong to a namespace can be created as methods in a subclass of :class:`socketio.Namespace`:: class MyCustomNamespace(socketio.Namespace): def on_connect(self, sid, environ): pass def on_disconnect(self, sid, reason): pass def on_my_event(self, sid, data): self.emit('my_response', data) sio.register_namespace(MyCustomNamespace('/test')) For asyncio based servers, namespaces must inherit from :class:`socketio.AsyncNamespace`, and can define event handlers as coroutines if desired:: class MyCustomNamespace(socketio.AsyncNamespace): def on_connect(self, sid, environ): pass def on_disconnect(self, sid, reason): pass async def on_my_event(self, sid, data): await self.emit('my_response', data) sio.register_namespace(MyCustomNamespace('/test')) When class-based namespaces are used, any events received by the server are dispatched to a method named as the event name with the ``on_`` prefix. For example, event ``my_event`` will be handled by a method named ``on_my_event``. If an event is received for which there is no corresponding method defined in the namespace class, then the event is ignored. All event names used in class-based namespaces must use characters that are legal in method names. As a convenience to methods defined in a class-based namespace, the namespace instance includes versions of several of the methods in the :class:`socketio.Server` and :class:`socketio.AsyncServer` classes that default to the proper namespace when the ``namespace`` argument is not given. In the case that an event has a handler in a class-based namespace, and also a decorator-based function handler, only the standalone function handler is invoked. It is important to note that class-based namespaces are singletons. This means that a single instance of a namespace class is used for all clients, and consequently, a namespace instance cannot be used to store client specific information. Catch-All Namespaces ~~~~~~~~~~~~~~~~~~~~ Similarily to catch-all event handlers, a "catch-all" namespace can be used when defining event handlers for any connected namespaces that do not have an explicitly defined event handler. As with catch-all events, ``'*'`` is used in place of a namespace:: @sio.on('my_event', namespace='*') def my_event_any_namespace(namespace, sid, data): pass For these events, the namespace is passed as first argument, followed by the regular arguments of the event. A catch-all class-based namespace handler can be defined by passing ``'*'`` as the namespace during registration:: sio.register_namespace(MyCustomNamespace('*')) A "catch-all" handler for all events on all namespaces can be defined as follows:: @sio.on('*', namespace='*') def any_event_any_namespace(event, namespace, sid, data): pass Event handlers with catch-all events and namespaces receive the event name and the namespace as first and second arguments. User Sessions ------------- The server can maintain application-specific information in a user session dedicated to each connected client. Applications can use the user session to write any details about the user that need to be preserved throughout the life of the connection, such as usernames or user ids. The ``save_session()`` and ``get_session()`` methods are used to store and retrieve information in the user session:: @sio.event def connect(sid, environ): username = authenticate_user(environ) sio.save_session(sid, {'username': username}) @sio.event def message(sid, data): session = sio.get_session(sid) print('message from ', session['username']) For the ``asyncio`` server, these methods are coroutines:: @sio.event async def connect(sid, environ): username = authenticate_user(environ) await sio.save_session(sid, {'username': username}) @sio.event async def message(sid, data): session = await sio.get_session(sid) print('message from ', session['username']) The session can also be manipulated with the `session()` context manager:: @sio.event def connect(sid, environ): username = authenticate_user(environ) with sio.session(sid) as session: session['username'] = username @sio.event def message(sid, data): with sio.session(sid) as session: print('message from ', session['username']) For the ``asyncio`` server, an asynchronous context manager is used:: @sio.event async def connect(sid, environ): username = authenticate_user(environ) async with sio.session(sid) as session: session['username'] = username @sio.event async def message(sid, data): async with sio.session(sid) as session: print('message from ', session['username']) The ``get_session()``, ``save_session()`` and ``session()`` methods take an optional ``namespace`` argument. If this argument isn't provided, the session is attached to the default namespace. Note: the contents of the user session are destroyed when the client disconnects. In particular, user session contents are not preserved when a client reconnects after an unexpected disconnection from the server. Cross-Origin Controls --------------------- For security reasons, this server enforces a same-origin policy by default. In practical terms, this means the following: - If an incoming HTTP or WebSocket request includes the ``Origin`` header, this header must match the scheme and host of the connection URL. In case of a mismatch, a 400 status code response is returned and the connection is rejected. - No restrictions are imposed on incoming requests that do not include the ``Origin`` header. If necessary, the ``cors_allowed_origins`` option can be used to allow other origins. This argument can be set to a string to set a single allowed origin, or to a list to allow multiple origins. A special value of ``'*'`` can be used to instruct the server to allow all origins, but this should be done with care, as this could make the server vulnerable to Cross-Site Request Forgery (CSRF) attacks. Monitoring and Administration ----------------------------- The Socket.IO server can be configured to accept connections from the official `Socket.IO Admin UI `_. This tool provides real-time information about currently connected clients, rooms in use and events being emitted. It also allows an administrator to manually emit events, change room assignments and disconnect clients. The hosted version of this tool is available at `https://admin.socket.io `_. Given that enabling this feature can affect the performance of the server, it is disabled by default. To enable it, call the :func:`instrument() ` method. For example:: import os import socketio sio = socketio.Server(cors_allowed_origins=[ 'http://localhost:5000', 'https://admin.socket.io', ]) sio.instrument(auth={ 'username': 'admin', 'password': os.environ['ADMIN_PASSWORD'], }) This configures the server to accept connections from the hosted Admin UI client. Administrators can then open https://admin.socket.io in their web browsers and log in with username ``admin`` and the password given by the ``ADMIN_PASSWORD`` environment variable. To ensure the Admin UI front end is allowed to connect, CORS is also configured. Consult the reference documentation to learn about additional configuration options that are available. Debugging and Troubleshooting ----------------------------- To help you debug issues, the server can be configured to output logs to the terminal:: import socketio # standard Python sio = socketio.Server(logger=True, engineio_logger=True) # asyncio sio = socketio.AsyncServer(logger=True, engineio_logger=True) The ``logger`` argument controls logging related to the Socket.IO protocol, while ``engineio_logger`` controls logs that originate in the low-level Engine.IO transport. These arguments can be set to ``True`` to output logs to ``stderr``, or to an object compatible with Python's ``logging`` package where the logs should be emitted to. A value of ``False`` disables logging. Logging can help identify the cause of connection problems, 400 responses, bad performance and other issues. Concurrency and Web Server Integration -------------------------------------- The Socket.IO server can be configured with different concurrency models depending on the needs of the application and the web server that is used. The concurrency model is given by the ``async_mode`` argument in the server. For example:: sio = socketio.Server(async_mode='threading') The following sub-sections describe the available concurrency options for synchronous and asynchronous servers. Standard Modes ~~~~~~~~~~~~~~ - ``threading``: the server will use Python threads for concurrency and will run on any multi-threaded WSGI server. This is the default mode when no other concurrency libraries are installed. - ``gevent``: the server will use greenlets through the `gevent `_ library for concurrency. A web server that is compatible with ``gevent`` is required. - ``gevent_uwsgi``: a variation of the ``gevent`` mode that is designed to work with the `uWSGI `_ web server. - ``eventlet``: the server will use greenlets through the `eventlet `_ library for concurrency. A web server that is compatible with ``eventlet`` is required. Use of ``eventlet`` is not recommended due to this project being in maintenance mode. Asyncio Modes ~~~~~~~~~~~~~ The asynchronous options are all based on the `asyncio `_ package of the Python standard library, with minor variations depending on the web server platform that is used. - ``asgi``: use of any `ASGI `_ web server is required. - ``aiohttp``: use of the `aiohttp `_ web framework and server is required. - ``tornado``: use of the `Tornado `_ web framework and server is required. - ``sanic``: use of the `Sanic `_ web framework and server is required. When using Sanic, it is recommended to use the ``asgi`` mode instead. .. _deployment-strategies: Deployment Strategies --------------------- The following sections describe a variety of deployment strategies for Socket.IO servers. Gunicorn ~~~~~~~~ The simplest deployment strategy for the Socket.IO server is to use the popular `Gunicorn `_ web server in multi-threaded mode. The Socket.IO server must be wrapped by the :class:`socketio.WSGIApp` class, so that it is compatible with the WSGI protocol:: sio = socketio.Server(async_mode='threading') app = socketio.WSGIApp(sio) If desired, the ``socketio.WSGIApp`` class can forward any traffic that is not Socket.IO to another WSGI application, making it possible to deploy a standard WSGI web application built with frameworks such as Flask or Django and the Socket.IO server as a bundle:: sio = socketio.Server(async_mode='threading') app = socketio.WSGIApp(sio, other_wsgi_app) The example that follows shows how to start a Socket.IO application using Gunicorn's threaded worker class:: $ gunicorn --workers 1 --threads 100 --bind 127.0.0.1:5000 module:app With the above configuration the server will be able to handle close to 100 concurrent clients. It is also possible to use more than one worker process, but this has two additional requirements: - The clients must connect directly over WebSocket. The long-polling transport is incompatible with the way Gunicorn load balances requests among workers. To disable long-polling in the server, add ``transports=['websocket']`` in the server constructor. Clients will have a similar option to initiate the connection with WebSocket. - The :func:`socketio.Server` instances in each worker must be configured with a message queue to allow the workers to communicate with each other. See the :ref:`using-a-message-queue` section for more information. When using multiple workers, the approximate number of connections the server will be able to accept can be calculated as the number of workers multiplied by the number of threads per worker. Note that Gunicorn can also be used alongside ``uvicorn``, ``gevent`` and ``eventlet``. These options are discussed under the appropriate sections below. Uvicorn (and other ASGI web servers) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ When working with an asynchronous Socket.IO server, the easiest deployment strategy is to use an ASGI web server such as `Uvicorn `_. The ``socketio.ASGIApp`` class is an ASGI compatible application that can forward Socket.IO traffic to a ``socketio.AsyncServer`` instance:: sio = socketio.AsyncServer(async_mode='asgi') app = socketio.ASGIApp(sio) If desired, the ``socketio.ASGIApp`` class can forward any traffic that is not Socket.IO to another ASGI application, making it possible to deploy a standard ASGI web application built with a framework such as FastAPI and the Socket.IO server as a bundle:: sio = socketio.AsyncServer(async_mode='asgi') app = socketio.ASGIApp(sio, other_asgi_app) The following example starts the application with Uvicorn:: uvicorn --port 5000 module:app Uvicorn can also be used through its Gunicorn worker:: gunicorn --workers 1 --worker-class uvicorn.workers.UvicornWorker --bind 127.0.0.1:5000 See the Gunicorn section above for information on how to use Gunicorn with multiple workers. Hypercorn, Daphne, and other ASGI servers !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! To use an ASGI web server other than Uvicorn, configure the application for ASGI as shown above for Uvicorn, then follow the documentation of your chosen web server to start the application. Aiohttp ~~~~~~~ Another option for deploying an asynchronous Socket.IO server is to use the `Aiohttp `_ web framework and server. Instances of class ``socketio.AsyncServer`` will automatically use Aiohttp if the library is installed. To request its use explicitly, the ``async_mode`` option can be given in the constructor:: sio = socketio.AsyncServer(async_mode='aiohttp') A server configured for Aiohttp must be attached to an existing application:: app = web.Application() sio.attach(app) The Aiohttp application can define regular routes that will coexist with the Socket.IO server. A typical pattern is to add routes that serve a client application and any associated static files. The Aiohttp application is then executed in the usual manner:: if __name__ == '__main__': web.run_app(app) Gevent ~~~~~~ When a multi-threaded web server is unable to satisfy the concurrency and scalability requirements of the application, an option to try is `Gevent `_. Gevent is a coroutine-based concurrency library based on greenlets, which are significantly lighter than threads. Instances of class ``socketio.Server`` will automatically use Gevent if the library is installed. To request gevent to be selected explicitly, the ``async_mode`` option can be given in the constructor:: sio = socketio.Server(async_mode='gevent') The Socket.IO server must be wrapped by the :class:`socketio.WSGIApp` class, so that it is compatible with the WSGI protocol:: app = socketio.WSGIApp(sio) If desired, the ``socketio.WSGIApp`` class can forward any traffic that is not Socket.IO to another WSGI application, making it possible to deploy a standard WSGI web application built with frameworks such as Flask or Django and the Socket.IO server as a bundle:: sio = socketio.Server(async_mode='gevent') app = socketio.WSGIApp(sio, other_wsgi_app) A server configured for Gevent is deployed as a regular WSGI application using the provided ``socketio.WSGIApp``:: from gevent import pywsgi pywsgi.WSGIServer(('', 8000), app).serve_forever() Gevent with Gunicorn !!!!!!!!!!!!!!!!!!!! An alternative to running the gevent WSGI server as above is to use `Gunicorn `_ with its Gevent worker. The command to launch the application under Gunicorn and Gevent is shown below:: $ gunicorn -k gevent -w 1 -b 127.0.0.1:5000 module:app See the Gunicorn section above for information on how to use Gunicorn with multiple workers. Gevent provides a ``monkey_patch()`` function that replaces all the blocking functions in the standard library with equivalent asynchronous versions. While the Socket.IO server does not require monkey patching, other libraries such as database or message queue drivers are likely to require it. Gevent with uWSGI !!!!!!!!!!!!!!!!! When using the uWSGI server in combination with gevent, the Socket.IO server can take advantage of uWSGI's native WebSocket support. Instances of class ``socketio.Server`` will automatically use this option for asynchronous operations if both gevent and uWSGI are installed and eventlet is not installed. To request this asynchronous mode explicitly, the ``async_mode`` option can be given in the constructor:: # gevent with uWSGI sio = socketio.Server(async_mode='gevent_uwsgi') A complete explanation of the configuration and usage of the uWSGI server is beyond the scope of this documentation. The uWSGI server is a fairly complex package that provides a large and comprehensive set of options. It must be compiled with WebSocket and SSL support for the WebSocket transport to be available. As way of an introduction, the following command starts a uWSGI server for the ``latency.py`` example on port 5000:: $ uwsgi --http :5000 --gevent 1000 --http-websockets --master --wsgi-file latency.py --callable app Tornado ~~~~~~~ Instances of class ``socketio.AsyncServer`` will automatically use `Tornado `_ if the library is installed. To request its use explicitly, the ``async_mode`` option can be given in the constructor:: sio = socketio.AsyncServer(async_mode='tornado') A server configured for Tornado must include a request handler for Socket.IO:: app = tornado.web.Application( [ (r"/socket.io/", socketio.get_tornado_handler(sio)), ], # ... other application options ) The Tornado application can define other routes that will coexist with the Socket.IO server. A typical pattern is to add routes that serve a client application and any associated static files. The Tornado application is then executed in the usual manner:: app.listen(port) tornado.ioloop.IOLoop.current().start() Eventlet ~~~~~~~~ .. note:: Eventlet is not in active development anymore, and for that reason the current recommendation is to not use it for new projects. `Eventlet `_ is a high performance concurrent networking library for Python that uses coroutines, enabling code to be written in the same style used with the blocking standard library functions. An Socket.IO server deployed with eventlet has access to the long-polling and WebSocket transports. Instances of class ``socketio.Server`` will automatically use eventlet for asynchronous operations if the library is installed. To request its use explicitly, the ``async_mode`` option can be given in the constructor:: sio = socketio.Server(async_mode='eventlet') A server configured for eventlet is deployed as a regular WSGI application using the provided ``socketio.WSGIApp``:: import eventlet app = socketio.WSGIApp(sio) eventlet.wsgi.server(eventlet.listen(('', 8000)), app) Eventlet with Gunicorn !!!!!!!!!!!!!!!!!!!!!! An alternative to running the eventlet WSGI server as above is to use `gunicorn `_, a fully featured pure Python web server. The command to launch the application under gunicorn is shown below:: $ gunicorn -k eventlet -w 1 module:app See the Gunicorn section above for information on how to use Gunicorn with multiple workers. Eventlet provides a ``monkey_patch()`` function that replaces all the blocking functions in the standard library with equivalent asynchronous versions. While python-socketio does not require monkey patching, other libraries such as database drivers are likely to require it. Sanic ~~~~~ .. note:: The Sanic integration has not been updated in a long time. It is currently recommended that a Sanic application is deployed with the ASGI integration. .. _using-a-message-queue: Using a Message Queue --------------------- When working with distributed applications, it is often necessary to access the functionality of the Socket.IO from multiple processes. There are two specific use cases: - Highly available applications may want to use horizontal scaling of the Socket.IO server to be able to handle very large number of concurrent clients. - Applications that use work queues such as `Celery `_ may need to emit an event to a client once a background job completes. The most convenient place to carry out this task is the worker process that handled this job. As a solution to the above problems, the Socket.IO server can be configured to connect to a message queue such as `Redis `_ or `RabbitMQ `_, to communicate with other related Socket.IO servers or auxiliary workers. Redis ~~~~~ To use a Redis message queue, a Python Redis client must be installed:: # socketio.Server class pip install redis The Redis queue is configured through the :class:`socketio.RedisManager` and :class:`socketio.AsyncRedisManager` classes. These classes connect directly to the Redis store and use the queue's pub/sub functionality:: # socketio.Server class mgr = socketio.RedisManager('redis://') sio = socketio.Server(client_manager=mgr) # socketio.AsyncServer class mgr = socketio.AsyncRedisManager('redis://') sio = socketio.AsyncServer(client_manager=mgr) The ``client_manager`` argument instructs the server to connect to the given message queue, and to coordinate with other processes connected to the queue. Kombu ~~~~~ `Kombu `_ is a Python package that provides access to RabbitMQ and many other message queues. It can be installed with pip:: pip install kombu To use RabbitMQ or other AMQP protocol compatible queues, that is the only required dependency. But for other message queues, Kombu may require additional packages. For example, to use a Redis queue via Kombu, the Python package for Redis needs to be installed as well:: pip install redis The queue is configured through the :class:`socketio.KombuManager`:: mgr = socketio.KombuManager('amqp://') sio = socketio.Server(client_manager=mgr) The connection URL passed to the :class:`KombuManager` constructor is passed directly to Kombu's `Connection object `_, so the Kombu documentation should be consulted for information on how to build the correct URL for a given message queue. Note that Kombu currently does not support asyncio, so it cannot be used with the :class:`socketio.AsyncServer` class. Kafka ~~~~~ `Apache Kafka `_ is supported through the `kafka-python `_ package:: pip install kafka-python Access to Kafka is configured through the :class:`socketio.KafkaManager` class:: mgr = socketio.KafkaManager('kafka://') sio = socketio.Server(client_manager=mgr) Note that Kafka currently does not support asyncio, so it cannot be used with the :class:`socketio.AsyncServer` class. AioPika ~~~~~~~ A RabbitMQ message queue is supported in asyncio applications through the `AioPika `_ package:: You need to install aio_pika with pip:: pip install aio_pika The RabbitMQ queue is configured through the :class:`socketio.AsyncAioPikaManager` class:: mgr = socketio.AsyncAioPikaManager('amqp://') sio = socketio.AsyncServer(client_manager=mgr) Horizontal Scaling ~~~~~~~~~~~~~~~~~~ Socket.IO is a stateful protocol, which makes horizontal scaling more difficult. When deploying a cluster of Socket.IO processes, all processes must connect to the message queue by passing the ``client_manager`` argument to the server instance. This enables the workers to communicate and coordinate complex operations such as broadcasts. If the long-polling transport is used, then there are two additional requirements that must be met: - Each Socket.IO process must be able to handle multiple requests concurrently. This is needed because long-polling clients send two requests in parallel. Worker processes that can only handle one request at a time are not supported. - The load balancer must be configured to always forward requests from a client to the same worker process, so that all requests coming from a client are handled by the same node. Load balancers call this *sticky sessions*, or *session affinity*. Emitting from external processes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To have a process other than a server connect to the queue to emit a message, the same client manager classes can be used as standalone objects. In this case, the ``write_only`` argument should be set to ``True`` to disable the creation of a listening thread, which only makes sense in a server. For example:: # connect to the redis queue as an external process external_sio = socketio.RedisManager('redis://', write_only=True) # emit an event external_sio.emit('my event', data={'foo': 'bar'}, room='my room') A limitation of the write-only client manager object is that it cannot receive callbacks when emitting. When the external process needs to receive callbacks, using a client to connect to the server with read and write support is a better option than a write-only client manager. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744472440.0 python_socketio-5.13.0/pyproject.toml0000664000175000017500000000240314776504570017343 0ustar00miguelmiguel[project] name = "python-socketio" version = "5.13.0" authors = [ { name = "Miguel Grinberg", email = "miguel.grinberg@gmail.com" }, ] description = "Socket.IO server and client for Python" classifiers = [ "Environment :: Web Environment", "Intended Audience :: Developers", "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ] requires-python = ">=3.8" dependencies = [ "bidict >= 0.21.0", "python-engineio >= 4.11.0", ] [project.readme] file = "README.md" content-type = "text/markdown" [project.urls] Homepage = "https://github.com/miguelgrinberg/python-socketio" "Bug Tracker" = "https://github.com/miguelgrinberg/python-socketio/issues" [project.optional-dependencies] client = [ "requests >= 2.21.0", "websocket-client >= 0.54.0", ] asyncio_client = [ "aiohttp >= 3.4", ] docs = [ "sphinx", ] [tool.setuptools] zip-safe = false include-package-data = true [tool.setuptools.package-dir] "" = "src" [tool.setuptools.packages.find] where = [ "src", ] namespaces = false [build-system] requires = ["setuptools>=61.2"] build-backend = "setuptools.build_meta" [tool.pytest.ini_options] asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "session" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744472442.0610871 python_socketio-5.13.0/setup.cfg0000664000175000017500000000004614776504572016253 0ustar00miguelmiguel[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1744472442.055087 python_socketio-5.13.0/src/0000775000175000017500000000000014776504572015221 5ustar00miguelmiguel././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744472442.0600872 python_socketio-5.13.0/src/python_socketio.egg-info/0000775000175000017500000000000014776504572022134 5ustar00miguelmiguel././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744472442.0 python_socketio-5.13.0/src/python_socketio.egg-info/PKG-INFO0000644000175000017500000000623314776504572023233 0ustar00miguelmiguelMetadata-Version: 2.4 Name: python-socketio Version: 5.13.0 Summary: Socket.IO server and client for Python Author-email: Miguel Grinberg Project-URL: Homepage, https://github.com/miguelgrinberg/python-socketio Project-URL: Bug Tracker, https://github.com/miguelgrinberg/python-socketio/issues Classifier: Environment :: Web Environment Classifier: Intended Audience :: Developers Classifier: Programming Language :: Python :: 3 Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Requires-Python: >=3.8 Description-Content-Type: text/markdown License-File: LICENSE Requires-Dist: bidict>=0.21.0 Requires-Dist: python-engineio>=4.11.0 Provides-Extra: client Requires-Dist: requests>=2.21.0; extra == "client" Requires-Dist: websocket-client>=0.54.0; extra == "client" Provides-Extra: asyncio-client Requires-Dist: aiohttp>=3.4; extra == "asyncio-client" Provides-Extra: docs Requires-Dist: sphinx; extra == "docs" Dynamic: license-file python-socketio =============== [![Build status](https://github.com/miguelgrinberg/python-socketio/workflows/build/badge.svg)](https://github.com/miguelgrinberg/python-socketio/actions) [![codecov](https://codecov.io/gh/miguelgrinberg/python-socketio/branch/main/graph/badge.svg)](https://codecov.io/gh/miguelgrinberg/python-socketio) Python implementation of the `Socket.IO` realtime client and server. Sponsors -------- The following organizations are funding this project: ![Socket.IO](https://images.opencollective.com/socketio/050e5eb/logo/64.png)
[Socket.IO](https://socket.io) | [Add your company here!](https://github.com/sponsors/miguelgrinberg)| -|- Many individual sponsors also support this project through small ongoing contributions. Why not [join them](https://github.com/sponsors/miguelgrinberg)? Version compatibility --------------------- The Socket.IO protocol has been through a number of revisions, and some of these introduced backward incompatible changes, which means that the client and the server must use compatible versions for everything to work. If you are using the Python client and server, the easiest way to ensure compatibility is to use the same version of this package for the client and the server. If you are using this package with a different client or server, then you must ensure the versions are compatible. The version compatibility chart below maps versions of this package to versions of the JavaScript reference implementation and the versions of the Socket.IO and Engine.IO protocols. JavaScript Socket.IO version | Socket.IO protocol revision | Engine.IO protocol revision | python-socketio version -|-|-|- 0.9.x | 1, 2 | 1, 2 | Not supported 1.x and 2.x | 3, 4 | 3 | 4.x 3.x and 4.x | 5 | 4 | 5.x Resources --------- - [Documentation](http://python-socketio.readthedocs.io/) - [PyPI](https://pypi.python.org/pypi/python-socketio) - [Change Log](https://github.com/miguelgrinberg/python-socketio/blob/main/CHANGES.md) - Questions? See the [questions](https://stackoverflow.com/questions/tagged/python-socketio) others have asked on Stack Overflow, or [ask](https://stackoverflow.com/questions/ask?tags=python+python-socketio) your own question. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744472442.0 python_socketio-5.13.0/src/python_socketio.egg-info/SOURCES.txt0000644000175000017500000000443014776504572024017 0ustar00miguelmiguelLICENSE MANIFEST.in README.md pyproject.toml tox.ini docs/Makefile docs/api.rst docs/client.rst docs/conf.py docs/index.rst docs/intro.rst docs/make.bat docs/server.rst docs/_static/README.md docs/_static/custom.css src/python_socketio.egg-info/PKG-INFO src/python_socketio.egg-info/SOURCES.txt src/python_socketio.egg-info/dependency_links.txt src/python_socketio.egg-info/not-zip-safe src/python_socketio.egg-info/requires.txt src/python_socketio.egg-info/top_level.txt src/socketio/__init__.py src/socketio/admin.py src/socketio/asgi.py src/socketio/async_admin.py src/socketio/async_aiopika_manager.py src/socketio/async_client.py src/socketio/async_manager.py src/socketio/async_namespace.py src/socketio/async_pubsub_manager.py src/socketio/async_redis_manager.py src/socketio/async_server.py src/socketio/async_simple_client.py src/socketio/base_client.py src/socketio/base_manager.py src/socketio/base_namespace.py src/socketio/base_server.py src/socketio/client.py src/socketio/exceptions.py src/socketio/kafka_manager.py src/socketio/kombu_manager.py src/socketio/manager.py src/socketio/middleware.py src/socketio/msgpack_packet.py src/socketio/namespace.py src/socketio/packet.py src/socketio/pubsub_manager.py src/socketio/redis_manager.py src/socketio/server.py src/socketio/simple_client.py src/socketio/tornado.py src/socketio/zmq_manager.py tests/__init__.py tests/asyncio_web_server.py tests/web_server.py tests/async/__init__.py tests/async/test_admin.py tests/async/test_client.py tests/async/test_manager.py tests/async/test_namespace.py tests/async/test_pubsub_manager.py tests/async/test_server.py tests/async/test_simple_client.py tests/common/__init__.py tests/common/test_admin.py tests/common/test_client.py tests/common/test_manager.py tests/common/test_middleware.py tests/common/test_msgpack_packet.py tests/common/test_namespace.py tests/common/test_packet.py tests/common/test_pubsub_manager.py tests/common/test_redis_manager.py tests/common/test_server.py tests/common/test_simple_client.py tests/performance/README.md tests/performance/binary_packet.py tests/performance/json_packet.py tests/performance/namespace_packet.py tests/performance/run.sh tests/performance/server_receive.py tests/performance/server_send.py tests/performance/server_send_broadcast.py tests/performance/text_packet.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744472442.0 python_socketio-5.13.0/src/python_socketio.egg-info/dependency_links.txt0000644000175000017500000000000114776504572026200 0ustar00miguelmiguel ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1686651098.0 python_socketio-5.13.0/src/python_socketio.egg-info/not-zip-safe0000644000175000017500000000000114442040332024333 0ustar00miguelmiguel ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744472442.0 python_socketio-5.13.0/src/python_socketio.egg-info/requires.txt0000644000175000017500000000021114776504572024524 0ustar00miguelmiguelbidict>=0.21.0 python-engineio>=4.11.0 [asyncio_client] aiohttp>=3.4 [client] requests>=2.21.0 websocket-client>=0.54.0 [docs] sphinx ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744472442.0 python_socketio-5.13.0/src/python_socketio.egg-info/top_level.txt0000644000175000017500000000001114776504572024654 0ustar00miguelmiguelsocketio ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1744472442.058087 python_socketio-5.13.0/src/socketio/0000775000175000017500000000000014776504572017041 5ustar00miguelmiguel././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704499102.0 python_socketio-5.13.0/src/socketio/__init__.py0000644000175000017500000000236514546113636021146 0ustar00miguelmiguelfrom .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'] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1735501689.0 python_socketio-5.13.0/src/socketio/admin.py0000664000175000017500000003713614734323571020505 0ustar00miguelmiguelfrom datetime import datetime, timezone 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 = f'{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 `_. """ 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, disconnections and received events self.sio.__trigger_event = self.sio._trigger_event self.sio._trigger_event = self._trigger_event # 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 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._trigger_event = self.sio.__trigger_event 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.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 _trigger_event(self, event, namespace, *args): t = time.time() sid = args[0] if event == 'connect': eio_sid = self.sio.manager.eio_sid_from_sid(sid, namespace) self.sio.manager._timestamps[sid] = t serialized_socket = self.serialize_socket(sid, namespace, eio_sid) self.sio.emit('socket_connected', ( serialized_socket, datetime.fromtimestamp(t, timezone.utc).isoformat(), ), namespace=self.admin_namespace) elif event == 'disconnect': del self.sio.manager._timestamps[sid] reason = args[1] self.sio.emit('socket_disconnected', ( namespace, sid, reason, datetime.fromtimestamp(t, timezone.utc).isoformat(), ), namespace=self.admin_namespace) else: self.sio.emit('event_received', ( namespace, sid, (event, *args[1:]), datetime.fromtimestamp(t, timezone.utc).isoformat(), ), namespace=self.admin_namespace) return self.sio.__trigger_event(event, namespace, *args) 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.now(timezone.utc).isoformat(), ), 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.now(timezone.utc).isoformat(), ), 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 [event, 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.now(timezone.utc).isoformat(), ), 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, reason): self.event_buffer.push('rawDisconnection') return self.sio._handle_eio_disconnect(eio_sid, reason) 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.fromtimestamp(t, timezone.utc).isoformat(), ), 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.fromtimestamp(tm, timezone.utc).isoformat() if tm else '', }, 'rooms': self.sio.manager.get_rooms(sid, namespace), } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707505332.0 python_socketio-5.13.0/src/socketio/asgi.py0000644000175000017500000000422014561473264020325 0ustar00miguelmiguelimport 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1735501875.0 python_socketio-5.13.0/src/socketio/async_admin.py0000664000175000017500000003767514734324063021707 0ustar00miguelmiguelimport asyncio from datetime import datetime, timezone 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 `_. """ 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, disconnections and received events self.sio.__trigger_event = self.sio._trigger_event self.sio._trigger_event = self._trigger_event # 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 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._trigger_event = self.sio.__trigger_event 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.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 _trigger_event(self, event, namespace, *args): t = time.time() sid = args[0] if event == 'connect': eio_sid = self.sio.manager.eio_sid_from_sid(sid, namespace) self.sio.manager._timestamps[sid] = t serialized_socket = self.serialize_socket(sid, namespace, eio_sid) await self.sio.emit('socket_connected', ( serialized_socket, datetime.fromtimestamp(t, timezone.utc).isoformat(), ), namespace=self.admin_namespace) elif event == 'disconnect': del self.sio.manager._timestamps[sid] reason = args[1] await self.sio.emit('socket_disconnected', ( namespace, sid, reason, datetime.fromtimestamp(t, timezone.utc).isoformat(), ), namespace=self.admin_namespace) else: await self.sio.emit('event_received', ( namespace, sid, (event, *args[1:]), datetime.fromtimestamp(t, timezone.utc).isoformat(), ), namespace=self.admin_namespace) return await self.sio.__trigger_event(event, namespace, *args) 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.now(timezone.utc).isoformat(), ))) return ret def _basic_leave_room(self, sid, namespace, room): if room: self.admin_queue.append(('room_left', ( namespace, room, sid, datetime.now(timezone.utc).isoformat(), ))) 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 [event, 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.now(timezone.utc).isoformat(), ), 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, reason): self.event_buffer.push('rawDisconnection') return await self.sio._handle_eio_disconnect(eio_sid, reason) 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.fromtimestamp(t, timezone.utc).isoformat(), ), 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.fromtimestamp(tm, timezone.utc).isoformat() if tm else '', }, 'rooms': self.sio.manager.get_rooms(sid, namespace), } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704499102.0 python_socketio-5.13.0/src/socketio/async_aiopika_manager.py0000644000175000017500000001211114546113636023701 0ustar00miguelmiguelimport 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() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744472116.0 python_socketio-5.13.0/src/socketio/async_client.py0000664000175000017500000006627114776504064022076 0ustar00miguelmiguelimport 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() await 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 exc 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: if self.eio.state == 'connected': # pragma: no cover # connected while sleeping above print('oops') continue 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() 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() 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(f'Namespace {namespace} is connected') 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, self.reason.SERVER_DISCONNECT) await self._trigger_event('__disconnect_final', 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: try: ret = await handler(*args) except TypeError: # the legacy disconnect event does not take a reason # argument if event == 'disconnect': ret = await handler(*args[:-1]) else: # pragma: no cover raise except asyncio.CancelledError: # pragma: no cover ret = None else: try: ret = handler(*args) except TypeError: # the legacy disconnect event does not take a reason # argument if event == 'disconnect': ret = handler(*args[:-1]) else: # pragma: no cover raise 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, reason): """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', n, reason) if not will_reconnect: await self._trigger_event('__disconnect_final', n) self.namespaces = {} self.connected = False self.callbacks = {} self._binary_packet = None self.sid = None if will_reconnect and not self._reconnect_task: self._reconnect_task = self.start_background_task( self._handle_reconnect) def _engineio_client_class(self): return engineio.AsyncClient ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1734172605.0 python_socketio-5.13.0/src/socketio/async_manager.py0000664000175000017500000001062714727257675022235 0ustar00miguelmiguelimport 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1734543553.0 python_socketio-5.13.0/src/socketio/async_namespace.py0000664000175000017500000002737414730604301022536 0ustar00miguelmiguelimport 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: try: ret = await handler(*args) except TypeError: # legacy disconnect events do not have a reason # argument if event == 'disconnect': ret = await handler(*args[:-1]) else: # pragma: no cover raise except asyncio.CancelledError: # pragma: no cover ret = None else: try: ret = handler(*args) except TypeError: # legacy disconnect events do not have a reason # argument if event == 'disconnect': ret = handler(*args[:-1]) else: # pragma: no cover raise 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: try: ret = await handler(*args) except TypeError: # legacy disconnect events do not have a reason # argument if event == 'disconnect': ret = await handler(*args[:-1]) else: # pragma: no cover raise except asyncio.CancelledError: # pragma: no cover ret = None else: try: ret = handler(*args) except TypeError: # legacy disconnect events do not have a reason # argument if event == 'disconnect': ret = handler(*args[:-1]) else: # pragma: no cover raise 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() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1734172605.0 python_socketio-5.13.0/src/socketio/async_pubsub_manager.py0000664000175000017500000002560514727257675023617 0ustar00miguelmiguelimport 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') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744472116.0 python_socketio-5.13.0/src/socketio/async_redis_manager.py0000664000175000017500000001167514776504064023416 0ustar00miguelmiguelimport 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 from .redis_manager import parse_redis_sentinel_url 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 a TLS connection, use ``rediss://``. To use Redis Sentinel, use ``redis+sentinel://`` with a comma-separated list of hosts and the service name after the db in the URL path. Example: ``redis+sentinel://user:pw@host1:1234,host2:2345/0/myredis``. :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()`` or ``Sentinel()``. """ 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): if not self.redis_url.startswith('redis+sentinel://'): self.redis = aioredis.Redis.from_url(self.redis_url, **self.redis_options) else: sentinels, service_name, connection_kwargs = \ parse_redis_sentinel_url(self.redis_url) kwargs = self.redis_options kwargs.update(connection_kwargs) sentinel = aioredis.sentinel.Sentinel(sentinels, **kwargs) self.redis = sentinel.master_for(service_name or self.channel) 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1735470789.0 python_socketio-5.13.0/src/socketio/async_server.py0000664000175000017500000010703614734227305022113 0ustar00miguelmiguelimport 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: 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, self.reason.SERVER_DISCONNECT) 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 `_. :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, reason=None): """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, reason or self.reason.CLIENT_DISCONNECT) 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: try: ret = await handler(*args) except TypeError: # legacy disconnect events use only one argument if event == 'disconnect': ret = await handler(*args[:-1]) else: # pragma: no cover raise except asyncio.CancelledError: # pragma: no cover ret = None else: try: ret = handler(*args) except TypeError: # legacy disconnect events use only one argument if event == 'disconnect': ret = handler(*args[:-1]) else: # pragma: no cover raise 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, self.reason.CLIENT_DISCONNECT) 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, reason): """Handle Engine.IO disconnect event.""" for n in list(self.manager.get_namespaces()).copy(): await self._handle_disconnect(eio_sid, n, reason) if eio_sid in self.environ: del self.environ[eio_sid] def _engineio_server_class(self): return engineio.AsyncServer ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1738799821.0 python_socketio-5.13.0/src/socketio/async_simple_client.py0000664000175000017500000002137714750775315023446 0ustar00miguelmiguelimport 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. """ client_class = AsyncClient 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 = self.client_class( *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() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1734543553.0 python_socketio-5.13.0/src/socketio/base_client.py0000664000175000017500000002656514730604301021656 0ustar00miguelmiguelimport itertools import logging import signal import threading import engineio 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'] reason = engineio.Client.reason 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, reason): # pragma: no cover raise NotImplementedError() def _engineio_client_class(self): # pragma: no cover raise NotImplementedError() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1734189874.0 python_socketio-5.13.0/src/socketio/base_manager.py0000664000175000017500000001313714727321462022013 0ustar00miguelmiguelimport 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 {} yield from participants.items() 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1734189874.0 python_socketio-5.13.0/src/socketio/base_namespace.py0000664000175000017500000000171214727321462022331 0ustar00miguelmiguelclass BaseNamespace: 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1734543553.0 python_socketio-5.13.0/src/socketio/base_server.py0000664000175000017500000002461514730604301021700 0ustar00miguelmiguelimport logging import engineio from . import manager from . import base_namespace from . import packet default_logger = logging.getLogger('socketio.server') class BaseServer: reserved_events = ['connect', 'disconnect'] reason = engineio.Server.reason 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') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744472116.0 python_socketio-5.13.0/src/socketio/client.py0000664000175000017500000006265514776504064020703 0ustar00miguelmiguelimport 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 exc 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: if self.eio.state == 'connected': # pragma: no cover # connected while sleeping above continue else: # the reconnect task gave up 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() 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(f'Namespace {namespace} is connected') 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, self.reason.SERVER_DISCONNECT) self._trigger_event('__disconnect_final', 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: try: return handler(*args) except TypeError: # the legacy disconnect event does not take a reason argument if event == 'disconnect': return handler(*args[:-1]) else: # pragma: no cover raise # 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, reason): """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', n, reason) if not will_reconnect: self._trigger_event('__disconnect_final', n) self.namespaces = {} self.connected = False self.callbacks = {} self._binary_packet = None self.sid = None if will_reconnect and not self._reconnect_task: self._reconnect_task = self.start_background_task( self._handle_reconnect) def _engineio_client_class(self): return engineio.Client ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704499102.0 python_socketio-5.13.0/src/socketio/exceptions.py0000644000175000017500000000171714546113636021570 0ustar00miguelmiguelclass 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1734189874.0 python_socketio-5.13.0/src/socketio/kafka_manager.py0000664000175000017500000000452414727321462022156 0ustar00miguelmiguelimport 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): yield from self.consumer def _listen(self): for message in self._kafka_listen(): if message.topic == self.channel: yield pickle.loads(message.value) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1734189874.0 python_socketio-5.13.0/src/socketio/kombu_manager.py0000664000175000017500000001316414727321462022216 0ustar00miguelmiguelimport 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 `_. 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 `_ 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 = 'python-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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1734172605.0 python_socketio-5.13.0/src/socketio/manager.py0000664000175000017500000000742514727257675021042 0ustar00miguelmiguelimport 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704499102.0 python_socketio-5.13.0/src/socketio/middleware.py0000644000175000017500000000306714546113636021524 0ustar00miguelmiguelimport 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704499102.0 python_socketio-5.13.0/src/socketio/msgpack_packet.py0000644000175000017500000000100214546113636022346 0ustar00miguelmiguelimport 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'] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1734543553.0 python_socketio-5.13.0/src/socketio/namespace.py0000664000175000017500000002242014730604301021324 0ustar00miguelmiguelfrom . 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): try: return getattr(self, handler_name)(*args) except TypeError: # legacy disconnect events do not have a reason argument if event == 'disconnect': return getattr(self, handler_name)(*args[:-1]) else: # pragma: no cover raise 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): try: return getattr(self, handler_name)(*args) except TypeError: # legacy disconnect events do not have a reason argument if event == 'disconnect': return getattr(self, handler_name)(*args[:-1]) else: # pragma: no cover raise 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() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1734189874.0 python_socketio-5.13.0/src/socketio/packet.py0000664000175000017500000001563514727321462020663 0ustar00miguelmiguelimport 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: """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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1734280723.0 python_socketio-5.13.0/src/socketio/pubsub_manager.py0000664000175000017500000002431214727603023022372 0ustar00miguelmiguelfrom 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') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744472116.0 python_socketio-5.13.0/src/socketio/redis_manager.py0000664000175000017500000001371614776504064022217 0ustar00miguelmiguelimport logging import pickle import time from urllib.parse import urlparse try: import redis except ImportError: redis = None from .pubsub_manager import PubSubManager logger = logging.getLogger('socketio') def parse_redis_sentinel_url(url): """Parse a Redis Sentinel URL with the format: redis+sentinel://[:password]@host1:port1,host2:port2,.../db/service_name """ parsed_url = urlparse(url) if parsed_url.scheme != 'redis+sentinel': raise ValueError('Invalid Redis Sentinel URL') sentinels = [] for host_port in parsed_url.netloc.split('@')[-1].split(','): host, port = host_port.rsplit(':', 1) sentinels.append((host, int(port))) kwargs = {} if parsed_url.username: kwargs['username'] = parsed_url.username if parsed_url.password: kwargs['password'] = parsed_url.password service_name = None if parsed_url.path: parts = parsed_url.path.split('/') if len(parts) >= 2 and parts[1] != '': kwargs['db'] = int(parts[1]) if len(parts) >= 3 and parts[2] != '': service_name = parts[2] return sentinels, service_name, kwargs 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 a TLS connection, use ``rediss://``. To use Redis Sentinel, use ``redis+sentinel://`` with a comma-separated list of hosts and the service name after the db in the URL path. Example: ``redis+sentinel://user:pw@host1:1234,host2:2345/0/myredis``. :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()`` or ``Sentinel()``. """ 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): if not self.redis_url.startswith('redis+sentinel://'): self.redis = redis.Redis.from_url(self.redis_url, **self.redis_options) else: sentinels, service_name, connection_kwargs = \ parse_redis_sentinel_url(self.redis_url) kwargs = self.redis_options kwargs.update(connection_kwargs) sentinel = redis.sentinel.Sentinel(sentinels, **kwargs) self.redis = sentinel.master_for(service_name or self.channel) 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 yield from self.pubsub.listen() 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1735498604.0 python_socketio-5.13.0/src/socketio/server.py0000664000175000017500000010374414734315554020724 0ustar00miguelmiguelimport 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 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 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: 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.reason.SERVER_DISCONNECT) 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 `_. :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, reason=None): """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, reason or self.reason.CLIENT_DISCONNECT) 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: try: return handler(*args) except TypeError: # legacy disconnect events use only one argument if event == 'disconnect': return handler(*args[:-1]) else: # pragma: no cover raise # 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, self.reason.CLIENT_DISCONNECT) 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, reason): """Handle Engine.IO disconnect event.""" for n in list(self.manager.get_namespaces()).copy(): self._handle_disconnect(eio_sid, n, reason) if eio_sid in self.environ: del self.environ[eio_sid] def _engineio_server_class(self): return engineio.Server ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1738799821.0 python_socketio-5.13.0/src/socketio/simple_client.py0000664000175000017500000002027114750775315022241 0ustar00miguelmiguelfrom 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. """ client_class = Client 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 = self.client_class( *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() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1705229282.0 python_socketio-5.13.0/src/socketio/tornado.py0000644000175000017500000000044714550735742021057 0ustar00miguelmigueltry: 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1734189874.0 python_socketio-5.13.0/src/socketio/zmq_manager.py0000664000175000017500000000673014727321462021711 0ustar00miguelmiguelimport 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, '') 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 ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1744472442.058087 python_socketio-5.13.0/tests/0000775000175000017500000000000014776504572015574 5ustar00miguelmiguel././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704499078.0 python_socketio-5.13.0/tests/__init__.py0000644000175000017500000000000014546113606017656 0ustar00miguelmiguel././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1744472442.059087 python_socketio-5.13.0/tests/async/0000775000175000017500000000000014776504572016711 5ustar00miguelmiguel././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704499102.0 python_socketio-5.13.0/tests/async/__init__.py0000644000175000017500000000000014546113636020776 0ustar00miguelmiguel././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1734466545.0 python_socketio-5.13.0/tests/async/test_admin.py0000664000175000017500000003202014730355761021401 0ustar00miguelmiguelfrom functools import wraps import threading import time from unittest import mock import pytest try: from engineio.async_socket import AsyncSocket as EngineIOSocket except ImportError: from engineio.asyncio_socket import AsyncSocket as EngineIOSocket import socketio from socketio.exceptions import ConnectionError from tests.asyncio_web_server import SocketIOWebServer def with_instrumented_server(auth=False, **ikwargs): """This decorator can be applied to test functions or methods so that they run with a Socket.IO server that has been instrumented for the official Admin UI project. The arguments passed to the decorator are passed directly to the ``instrument()`` method of the server. """ def decorator(f): @wraps(f) def wrapped(self, *args, **kwargs): sio = socketio.AsyncServer(async_mode='asgi') @sio.event async def enter_room(sid, data): await sio.enter_room(sid, data) @sio.event async def emit(sid, event): await sio.emit(event, skip_sid=sid) @sio.event(namespace='/foo') def connect(sid, environ, auth): pass async def shutdown(): await self.isvr.shutdown() await sio.shutdown() if 'server_stats_interval' not in ikwargs: ikwargs['server_stats_interval'] = 0.25 self.isvr = sio.instrument(auth=auth, **ikwargs) server = SocketIOWebServer(sio, on_shutdown=shutdown) server.start() # import logging # logging.getLogger('engineio.client').setLevel(logging.DEBUG) # logging.getLogger('socketio.client').setLevel(logging.DEBUG) original_schedule_ping = EngineIOSocket.schedule_ping EngineIOSocket.schedule_ping = mock.MagicMock() try: ret = f(self, *args, **kwargs) finally: server.stop() self.isvr.uninstrument() self.isvr = None EngineIOSocket.schedule_ping = original_schedule_ping # import logging # logging.getLogger('engineio.client').setLevel(logging.NOTSET) # logging.getLogger('socketio.client').setLevel(logging.NOTSET) return ret return wrapped return decorator def _custom_auth(auth): return auth == {'foo': 'bar'} async def _async_custom_auth(auth): return auth == {'foo': 'bar'} class TestAsyncAdmin: def setup_method(self): print('threads at start:', threading.enumerate()) self.thread_count = threading.active_count() def teardown_method(self): print('threads at end:', threading.enumerate()) assert self.thread_count == threading.active_count() def _expect(self, expected, admin_client): events = {} while expected: data = admin_client.receive(timeout=5) if data[0] in expected: if expected[data[0]] == 1: events[data[0]] = data[1] del expected[data[0]] else: expected[data[0]] -= 1 return events def test_missing_auth(self): sio = socketio.AsyncServer(async_mode='asgi') with pytest.raises(ValueError): sio.instrument() @with_instrumented_server(auth=False) def test_admin_connect_with_no_auth(self): with socketio.SimpleClient() as admin_client: admin_client.connect('http://localhost:8900', namespace='/admin') with socketio.SimpleClient() as admin_client: admin_client.connect('http://localhost:8900', namespace='/admin', auth={'foo': 'bar'}) @with_instrumented_server(auth={'foo': 'bar'}) def test_admin_connect_with_dict_auth(self): with socketio.SimpleClient() as admin_client: admin_client.connect('http://localhost:8900', namespace='/admin', auth={'foo': 'bar'}) with socketio.SimpleClient() as admin_client: with pytest.raises(ConnectionError): admin_client.connect( 'http://localhost:8900', namespace='/admin', auth={'foo': 'baz'}) with socketio.SimpleClient() as admin_client: with pytest.raises(ConnectionError): admin_client.connect( 'http://localhost:8900', namespace='/admin') @with_instrumented_server(auth=[{'foo': 'bar'}, {'u': 'admin', 'p': 'secret'}]) def test_admin_connect_with_list_auth(self): with socketio.SimpleClient() as admin_client: admin_client.connect('http://localhost:8900', namespace='/admin', auth={'foo': 'bar'}) with socketio.SimpleClient() as admin_client: admin_client.connect('http://localhost:8900', namespace='/admin', auth={'u': 'admin', 'p': 'secret'}) with socketio.SimpleClient() as admin_client: with pytest.raises(ConnectionError): admin_client.connect('http://localhost:8900', namespace='/admin', auth={'foo': 'baz'}) with socketio.SimpleClient() as admin_client: with pytest.raises(ConnectionError): admin_client.connect('http://localhost:8900', namespace='/admin') @with_instrumented_server(auth=_custom_auth) def test_admin_connect_with_function_auth(self): with socketio.SimpleClient() as admin_client: admin_client.connect('http://localhost:8900', namespace='/admin', auth={'foo': 'bar'}) with socketio.SimpleClient() as admin_client: with pytest.raises(ConnectionError): admin_client.connect('http://localhost:8900', namespace='/admin', auth={'foo': 'baz'}) with socketio.SimpleClient() as admin_client: with pytest.raises(ConnectionError): admin_client.connect('http://localhost:8900', namespace='/admin') @with_instrumented_server(auth=_async_custom_auth) def test_admin_connect_with_async_function_auth(self): with socketio.SimpleClient() as admin_client: admin_client.connect('http://localhost:8900', namespace='/admin', auth={'foo': 'bar'}) with socketio.SimpleClient() as admin_client: with pytest.raises(ConnectionError): admin_client.connect('http://localhost:8900', namespace='/admin', auth={'foo': 'baz'}) with socketio.SimpleClient() as admin_client: with pytest.raises(ConnectionError): admin_client.connect('http://localhost:8900', namespace='/admin') @with_instrumented_server() def test_admin_connect_only_admin(self): with socketio.SimpleClient() as admin_client: admin_client.connect('http://localhost:8900', namespace='/admin') sid = admin_client.sid events = self._expect({'config': 1, 'all_sockets': 1, 'server_stats': 2}, admin_client) assert 'supportedFeatures' in events['config'] assert 'ALL_EVENTS' in events['config']['supportedFeatures'] assert 'AGGREGATED_EVENTS' in events['config']['supportedFeatures'] assert 'EMIT' in events['config']['supportedFeatures'] assert len(events['all_sockets']) == 1 assert events['all_sockets'][0]['id'] == sid assert events['all_sockets'][0]['rooms'] == [sid] assert events['server_stats']['clientsCount'] == 1 assert events['server_stats']['pollingClientsCount'] == 0 assert len(events['server_stats']['namespaces']) == 3 assert {'name': '/', 'socketsCount': 0} in \ events['server_stats']['namespaces'] assert {'name': '/foo', 'socketsCount': 0} in \ events['server_stats']['namespaces'] assert {'name': '/admin', 'socketsCount': 1} in \ events['server_stats']['namespaces'] @with_instrumented_server() def test_admin_connect_with_others(self): with socketio.SimpleClient() as client1, \ socketio.SimpleClient() as client2, \ socketio.SimpleClient() as client3, \ socketio.SimpleClient() as admin_client: client1.connect('http://localhost:8900') client1.emit('enter_room', 'room') sid1 = client1.sid saved_check_for_upgrade = self.isvr._check_for_upgrade self.isvr._check_for_upgrade = mock.AsyncMock() client2.connect('http://localhost:8900', namespace='/foo', transports=['polling']) sid2 = client2.sid self.isvr._check_for_upgrade = saved_check_for_upgrade client3.connect('http://localhost:8900', namespace='/admin') sid3 = client3.sid admin_client.connect('http://localhost:8900', namespace='/admin') sid = admin_client.sid events = self._expect({'config': 1, 'all_sockets': 1, 'server_stats': 2}, admin_client) assert 'supportedFeatures' in events['config'] assert 'ALL_EVENTS' in events['config']['supportedFeatures'] assert 'AGGREGATED_EVENTS' in events['config']['supportedFeatures'] assert 'EMIT' in events['config']['supportedFeatures'] assert len(events['all_sockets']) == 4 assert events['server_stats']['clientsCount'] == 4 assert events['server_stats']['pollingClientsCount'] == 1 assert len(events['server_stats']['namespaces']) == 3 assert {'name': '/', 'socketsCount': 1} in \ events['server_stats']['namespaces'] assert {'name': '/foo', 'socketsCount': 1} in \ events['server_stats']['namespaces'] assert {'name': '/admin', 'socketsCount': 2} in \ events['server_stats']['namespaces'] for socket in events['all_sockets']: if socket['id'] == sid: assert socket['rooms'] == [sid] elif socket['id'] == sid1: assert socket['rooms'] == [sid1, 'room'] elif socket['id'] == sid2: assert socket['rooms'] == [sid2] elif socket['id'] == sid3: assert socket['rooms'] == [sid3] @with_instrumented_server(mode='production', read_only=True) def test_admin_connect_production(self): with socketio.SimpleClient() as admin_client: admin_client.connect('http://localhost:8900', namespace='/admin') events = self._expect({'config': 1, 'server_stats': 2}, admin_client) assert 'supportedFeatures' in events['config'] assert 'ALL_EVENTS' not in events['config']['supportedFeatures'] assert 'AGGREGATED_EVENTS' in events['config']['supportedFeatures'] assert 'EMIT' not in events['config']['supportedFeatures'] assert events['server_stats']['clientsCount'] == 1 assert events['server_stats']['pollingClientsCount'] == 0 assert len(events['server_stats']['namespaces']) == 3 assert {'name': '/', 'socketsCount': 0} in \ events['server_stats']['namespaces'] assert {'name': '/foo', 'socketsCount': 0} in \ events['server_stats']['namespaces'] assert {'name': '/admin', 'socketsCount': 1} in \ events['server_stats']['namespaces'] @with_instrumented_server() def test_admin_features(self): with socketio.SimpleClient() as client1, \ socketio.SimpleClient() as client2, \ socketio.SimpleClient() as admin_client: client1.connect('http://localhost:8900') client2.connect('http://localhost:8900') admin_client.connect('http://localhost:8900', namespace='/admin') # emit from admin admin_client.emit( 'emit', ('/', client1.sid, 'foo', {'bar': 'baz'}, 'extra')) data = client1.receive(timeout=5) assert data == ['foo', {'bar': 'baz'}, 'extra'] # emit from regular client client1.emit('emit', 'foo') data = client2.receive(timeout=5) assert data == ['foo'] # join and leave admin_client.emit('join', ('/', 'room', client1.sid)) time.sleep(0.2) admin_client.emit( 'emit', ('/', 'room', 'foo', {'bar': 'baz'})) data = client1.receive(timeout=5) assert data == ['foo', {'bar': 'baz'}] admin_client.emit('leave', ('/', 'room')) # disconnect admin_client.emit('_disconnect', ('/', False, client1.sid)) for _ in range(10): if not client1.connected: break time.sleep(0.2) assert not client1.connected ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741531549.0 python_socketio-5.13.0/tests/async/test_client.py0000664000175000017500000012675114763324635021610 0ustar00miguelmiguelimport asyncio from unittest import mock import pytest from socketio import async_client from socketio import async_namespace from engineio import exceptions as engineio_exceptions from socketio import exceptions from socketio import packet class TestAsyncClient: async def test_is_asyncio_based(self): c = async_client.AsyncClient() assert c.is_asyncio_based() async def test_connect(self): c = async_client.AsyncClient() c.eio.connect = mock.AsyncMock() await c.connect( 'url', headers='headers', auth='auth', transports='transports', namespaces=['/foo', '/', '/bar'], socketio_path='path', wait=False, ) assert c.connection_url == 'url' assert c.connection_headers == 'headers' assert c.connection_auth == 'auth' assert c.connection_transports == 'transports' assert c.connection_namespaces == ['/foo', '/', '/bar'] assert c.socketio_path == 'path' c.eio.connect.assert_awaited_once_with( 'url', headers='headers', transports='transports', engineio_path='path', ) async def test_connect_functions(self): async def headers(): return 'headers' c = async_client.AsyncClient() c.eio.connect = mock.AsyncMock() await c.connect( lambda: 'url', headers=headers, auth='auth', transports='transports', namespaces=['/foo', '/', '/bar'], socketio_path='path', wait=False, ) c.eio.connect.assert_awaited_once_with( 'url', headers='headers', transports='transports', engineio_path='path', ) async def test_connect_one_namespace(self): c = async_client.AsyncClient() c.eio.connect = mock.AsyncMock() await c.connect( 'url', headers='headers', transports='transports', namespaces='/foo', socketio_path='path', wait=False, ) assert c.connection_url == 'url' assert c.connection_headers == 'headers' assert c.connection_transports == 'transports' assert c.connection_namespaces == ['/foo'] assert c.socketio_path == 'path' c.eio.connect.assert_awaited_once_with( 'url', headers='headers', transports='transports', engineio_path='path', ) async def test_connect_default_namespaces(self): c = async_client.AsyncClient() c.eio.connect = mock.AsyncMock() c.on('foo', mock.MagicMock(), namespace='/foo') c.on('bar', mock.MagicMock(), namespace='/') c.on('baz', mock.MagicMock(), namespace='*') await c.connect( 'url', headers='headers', transports='transports', socketio_path='path', wait=False, ) assert c.connection_url == 'url' assert c.connection_headers == 'headers' assert c.connection_transports == 'transports' assert c.connection_namespaces == ['/', '/foo'] or \ c.connection_namespaces == ['/foo', '/'] assert c.socketio_path == 'path' c.eio.connect.assert_awaited_once_with( 'url', headers='headers', transports='transports', engineio_path='path', ) async def test_connect_no_namespaces(self): c = async_client.AsyncClient() c.eio.connect = mock.AsyncMock() await c.connect( 'url', headers='headers', transports='transports', socketio_path='path', wait=False, ) assert c.connection_url == 'url' assert c.connection_headers == 'headers' assert c.connection_transports == 'transports' assert c.connection_namespaces == ['/'] assert c.socketio_path == 'path' c.eio.connect.assert_awaited_once_with( 'url', headers='headers', transports='transports', engineio_path='path', ) async def test_connect_error(self): c = async_client.AsyncClient() c.eio.connect = mock.AsyncMock( side_effect=engineio_exceptions.ConnectionError('foo') ) c.on('foo', mock.MagicMock(), namespace='/foo') c.on('bar', mock.MagicMock(), namespace='/') with pytest.raises(exceptions.ConnectionError): await c.connect( 'url', headers='headers', transports='transports', socketio_path='path', wait=False, ) async def test_connect_twice(self): c = async_client.AsyncClient() c.eio.connect = mock.AsyncMock() await c.connect( 'url', wait=False, ) with pytest.raises(exceptions.ConnectionError): await c.connect( 'url', wait=False, ) async def test_connect_wait_single_namespace(self): c = async_client.AsyncClient() c.eio.connect = mock.AsyncMock() c._connect_event = mock.MagicMock() async def mock_connect(): c.namespaces = {'/': '123'} return True c._connect_event.wait = mock_connect await c.connect( 'url', wait=True, wait_timeout=0.01, ) assert c.connected is True async def test_connect_wait_two_namespaces(self): c = async_client.AsyncClient() c.eio.connect = mock.AsyncMock() c._connect_event = mock.MagicMock() async def mock_connect(): if c.namespaces == {}: c.namespaces = {'/bar': '123'} return True elif c.namespaces == {'/bar': '123'}: c.namespaces = {'/bar': '123', '/foo': '456'} return True return False c._connect_event.wait = mock_connect await c.connect( 'url', namespaces=['/foo', '/bar'], wait=True, wait_timeout=0.01, ) assert c.connected is True assert c.namespaces == {'/bar': '123', '/foo': '456'} async def test_connect_timeout(self): c = async_client.AsyncClient() c.eio.connect = mock.AsyncMock() c.disconnect = mock.AsyncMock() with pytest.raises(exceptions.ConnectionError): await c.connect( 'url', wait=True, wait_timeout=0.01, ) c.disconnect.assert_awaited_once_with() async def test_wait_no_reconnect(self): c = async_client.AsyncClient() c.eio.wait = mock.AsyncMock() c.sleep = mock.AsyncMock() c._reconnect_task = None await c.wait() c.eio.wait.assert_awaited_once_with() c.sleep.assert_awaited_once_with(1) async def test_wait_reconnect_failed(self): c = async_client.AsyncClient() c.eio.wait = mock.AsyncMock() c.sleep = mock.AsyncMock() states = ['disconnected'] async def fake_wait(): c.eio.state = states.pop(0) c._reconnect_task = fake_wait() await c.wait() c.eio.wait.assert_awaited_once_with() c.sleep.assert_awaited_once_with(1) async def test_wait_reconnect_successful(self): c = async_client.AsyncClient() c.eio.wait = mock.AsyncMock() c.sleep = mock.AsyncMock() states = ['connected', 'disconnected'] async def fake_wait(): c.eio.state = states.pop(0) c._reconnect_task = fake_wait() c._reconnect_task = fake_wait() await c.wait() assert c.eio.wait.await_count == 2 assert c.sleep.await_count == 2 async def test_emit_no_arguments(self): c = async_client.AsyncClient() c.namespaces = {'/': '1'} c._send_packet = mock.AsyncMock() await c.emit('foo') expected_packet = packet.Packet( packet.EVENT, namespace='/', data=['foo'], id=None) assert c._send_packet.await_count == 1 assert ( c._send_packet.await_args_list[0][0][0].encode() == expected_packet.encode() ) async def test_emit_one_argument(self): c = async_client.AsyncClient() c.namespaces = {'/': '1'} c._send_packet = mock.AsyncMock() await c.emit('foo', 'bar') expected_packet = packet.Packet( packet.EVENT, namespace='/', data=['foo', 'bar'], id=None, ) assert c._send_packet.await_count == 1 assert ( c._send_packet.await_args_list[0][0][0].encode() == expected_packet.encode() ) async def test_emit_one_argument_list(self): c = async_client.AsyncClient() c.namespaces = {'/': '1'} c._send_packet = mock.AsyncMock() await c.emit('foo', ['bar', 'baz']) expected_packet = packet.Packet( packet.EVENT, namespace='/', data=['foo', ['bar', 'baz']], id=None, ) assert c._send_packet.await_count == 1 assert ( c._send_packet.await_args_list[0][0][0].encode() == expected_packet.encode() ) async def test_emit_two_arguments(self): c = async_client.AsyncClient() c.namespaces = {'/': '1'} c._send_packet = mock.AsyncMock() await c.emit('foo', ('bar', 'baz')) expected_packet = packet.Packet( packet.EVENT, namespace='/', data=['foo', 'bar', 'baz'], id=None, ) assert c._send_packet.await_count == 1 assert ( c._send_packet.await_args_list[0][0][0].encode() == expected_packet.encode() ) async def test_emit_namespace(self): c = async_client.AsyncClient() c.namespaces = {'/foo': '1'} c._send_packet = mock.AsyncMock() await c.emit('foo', namespace='/foo') expected_packet = packet.Packet( packet.EVENT, namespace='/foo', data=['foo'], id=None) assert c._send_packet.await_count == 1 assert ( c._send_packet.await_args_list[0][0][0].encode() == expected_packet.encode() ) async def test_emit_unknown_namespace(self): c = async_client.AsyncClient() c.namespaces = {'/foo': '1'} with pytest.raises(exceptions.BadNamespaceError): await c.emit('foo', namespace='/bar') async def test_emit_with_callback(self): c = async_client.AsyncClient() c._send_packet = mock.AsyncMock() c._generate_ack_id = mock.MagicMock(return_value=123) c.namespaces = {'/': '1'} await c.emit('foo', callback='cb') expected_packet = packet.Packet( packet.EVENT, namespace='/', data=['foo'], id=123) assert c._send_packet.await_count == 1 assert ( c._send_packet.await_args_list[0][0][0].encode() == expected_packet.encode() ) c._generate_ack_id.assert_called_once_with('/', 'cb') async def test_emit_namespace_with_callback(self): c = async_client.AsyncClient() c.namespaces = {'/foo': '1'} c._send_packet = mock.AsyncMock() c._generate_ack_id = mock.MagicMock(return_value=123) await c.emit('foo', namespace='/foo', callback='cb') expected_packet = packet.Packet( packet.EVENT, namespace='/foo', data=['foo'], id=123) assert c._send_packet.await_count == 1 assert ( c._send_packet.await_args_list[0][0][0].encode() == expected_packet.encode() ) c._generate_ack_id.assert_called_once_with('/foo', 'cb') async def test_emit_binary(self): c = async_client.AsyncClient() c.namespaces = {'/': '1'} c._send_packet = mock.AsyncMock() await c.emit('foo', b'bar') expected_packet = packet.Packet( packet.EVENT, namespace='/', data=['foo', b'bar'], id=None, ) assert c._send_packet.await_count == 1 assert ( c._send_packet.await_args_list[0][0][0].encode() == expected_packet.encode() ) async def test_emit_not_binary(self): c = async_client.AsyncClient() c.namespaces = {'/': '1'} c._send_packet = mock.AsyncMock() await c.emit('foo', 'bar') expected_packet = packet.Packet( packet.EVENT, namespace='/', data=['foo', 'bar'], id=None, ) assert c._send_packet.await_count == 1 assert ( c._send_packet.await_args_list[0][0][0].encode() == expected_packet.encode() ) async def test_send(self): c = async_client.AsyncClient() c.emit = mock.AsyncMock() await c.send('data', 'namespace', 'callback') c.emit.assert_awaited_once_with( 'message', data='data', namespace='namespace', callback='callback' ) async def test_send_with_defaults(self): c = async_client.AsyncClient() c.emit = mock.AsyncMock() await c.send('data') c.emit.assert_awaited_once_with( 'message', data='data', namespace=None, callback=None ) async def test_call(self): c = async_client.AsyncClient() c.namespaces = {'/': '1'} async def fake_event_wait(): c._generate_ack_id.call_args_list[0][0][1]('foo', 321) c._send_packet = mock.AsyncMock() c._generate_ack_id = mock.MagicMock(return_value=123) c.eio = mock.MagicMock() c.eio.create_event.return_value.wait = fake_event_wait assert await c.call('foo') == ('foo', 321) expected_packet = packet.Packet( packet.EVENT, namespace='/', data=['foo'], id=123) assert c._send_packet.await_count == 1 assert ( c._send_packet.await_args_list[0][0][0].encode() == expected_packet.encode() ) async def test_call_with_timeout(self): c = async_client.AsyncClient() c.namespaces = {'/': '1'} async def fake_event_wait(): await asyncio.sleep(1) c._send_packet = mock.AsyncMock() c._generate_ack_id = mock.MagicMock(return_value=123) c.eio = mock.MagicMock() c.eio.create_event.return_value.wait = fake_event_wait with pytest.raises(exceptions.TimeoutError): await c.call('foo', timeout=0.01) expected_packet = packet.Packet( packet.EVENT, namespace='/', data=['foo'], id=123) assert c._send_packet.await_count == 1 assert ( c._send_packet.await_args_list[0][0][0].encode() == expected_packet.encode() ) async def test_disconnect(self): c = async_client.AsyncClient() c.connected = True c.namespaces = {'/': '1'} c._trigger_event = mock.AsyncMock() c._send_packet = mock.AsyncMock() c.eio = mock.MagicMock() c.eio.disconnect = mock.AsyncMock() c.eio.state = 'connected' await c.disconnect() assert c.connected assert c._trigger_event.await_count == 0 assert c._send_packet.await_count == 1 expected_packet = packet.Packet(packet.DISCONNECT, namespace='/') assert ( c._send_packet.await_args_list[0][0][0].encode() == expected_packet.encode() ) c.eio.disconnect.assert_awaited_once_with() async def test_disconnect_namespaces(self): c = async_client.AsyncClient() c.namespaces = {'/foo': '1', '/bar': '2'} c._trigger_event = mock.AsyncMock() c._send_packet = mock.AsyncMock() c.eio = mock.MagicMock() c.eio.disconnect = mock.AsyncMock() c.eio.state = 'connected' await c.disconnect() assert c._trigger_event.await_count == 0 assert c._send_packet.await_count == 2 expected_packet = packet.Packet(packet.DISCONNECT, namespace='/foo') assert ( c._send_packet.await_args_list[0][0][0].encode() == expected_packet.encode() ) expected_packet = packet.Packet(packet.DISCONNECT, namespace='/bar') assert ( c._send_packet.await_args_list[1][0][0].encode() == expected_packet.encode() ) async def test_start_background_task(self): c = async_client.AsyncClient() c.eio.start_background_task = mock.MagicMock(return_value='foo') assert c.start_background_task('foo', 'bar', baz='baz') == 'foo' c.eio.start_background_task.assert_called_once_with( 'foo', 'bar', baz='baz' ) async def test_sleep(self): c = async_client.AsyncClient() c.eio.sleep = mock.AsyncMock() await c.sleep(1.23) c.eio.sleep.assert_awaited_once_with(1.23) async def test_send_packet(self): c = async_client.AsyncClient() c.eio.send = mock.AsyncMock() await c._send_packet(packet.Packet(packet.EVENT, 'foo')) c.eio.send.assert_awaited_once_with('2"foo"') async def test_send_packet_binary(self): c = async_client.AsyncClient() c.eio.send = mock.AsyncMock() await c._send_packet(packet.Packet(packet.EVENT, b'foo')) assert c.eio.send.await_args_list == [ mock.call('51-{"_placeholder":true,"num":0}'), mock.call(b'foo'), ] or c.eio.send.await_args_list == [ mock.call('51-{"num":0,"_placeholder":true}'), mock.call(b'foo'), ] async def test_send_packet_default_binary(self): c = async_client.AsyncClient() c.eio.send = mock.AsyncMock() await c._send_packet(packet.Packet(packet.EVENT, 'foo')) c.eio.send.assert_awaited_once_with('2"foo"') async def test_handle_connect(self): c = async_client.AsyncClient() c._connect_event = mock.MagicMock() c._trigger_event = mock.AsyncMock() c._send_packet = mock.AsyncMock() await c._handle_connect('/', {'sid': '123'}) c._connect_event.set.assert_called_once_with() c._trigger_event.assert_awaited_once_with('connect', namespace='/') c._send_packet.assert_not_awaited() async def test_handle_connect_with_namespaces(self): c = async_client.AsyncClient() c.namespaces = {'/foo': '1', '/bar': '2'} c._connect_event = mock.MagicMock() c._trigger_event = mock.AsyncMock() c._send_packet = mock.AsyncMock() await c._handle_connect('/', {'sid': '3'}) c._connect_event.set.assert_called_once_with() c._trigger_event.assert_awaited_once_with('connect', namespace='/') assert c.namespaces == {'/': '3', '/foo': '1', '/bar': '2'} async def test_handle_connect_namespace(self): c = async_client.AsyncClient() c.namespaces = {'/foo': '1'} c._connect_event = mock.MagicMock() c._trigger_event = mock.AsyncMock() c._send_packet = mock.AsyncMock() await c._handle_connect('/foo', {'sid': '123'}) await c._handle_connect('/bar', {'sid': '2'}) assert c._trigger_event.await_count == 1 c._connect_event.set.assert_called_once_with() c._trigger_event.assert_awaited_once_with( 'connect', namespace='/bar') assert c.namespaces == {'/foo': '1', '/bar': '2'} async def test_handle_disconnect(self): c = async_client.AsyncClient() c.connected = True c._trigger_event = mock.AsyncMock() await c._handle_disconnect('/') c._trigger_event.assert_any_await( 'disconnect', '/', c.reason.SERVER_DISCONNECT ) c._trigger_event.assert_any_await('__disconnect_final', '/') assert not c.connected await c._handle_disconnect('/') assert c._trigger_event.await_count == 2 async def test_handle_disconnect_namespace(self): c = async_client.AsyncClient() c.connected = True c.namespaces = {'/foo': '1', '/bar': '2'} c._trigger_event = mock.AsyncMock() await c._handle_disconnect('/foo') c._trigger_event.assert_any_await('disconnect', '/foo', c.reason.SERVER_DISCONNECT) c._trigger_event.assert_any_await('__disconnect_final', '/foo') assert c.namespaces == {'/bar': '2'} assert c.connected await c._handle_disconnect('/bar') c._trigger_event.assert_any_await('disconnect', '/bar', c.reason.SERVER_DISCONNECT) c._trigger_event.assert_any_await('__disconnect_final', '/bar') assert c.namespaces == {} assert not c.connected async def test_handle_disconnect_unknown_namespace(self): c = async_client.AsyncClient() c.connected = True c.namespaces = {'/foo': '1', '/bar': '2'} c._trigger_event = mock.AsyncMock() await c._handle_disconnect('/baz') c._trigger_event.assert_any_await('disconnect', '/baz', c.reason.SERVER_DISCONNECT) c._trigger_event.assert_any_await('__disconnect_final', '/baz') assert c.namespaces == {'/foo': '1', '/bar': '2'} assert c.connected async def test_handle_disconnect_default_namespaces(self): c = async_client.AsyncClient() c.connected = True c.namespaces = {'/foo': '1', '/bar': '2'} c._trigger_event = mock.AsyncMock() await c._handle_disconnect('/') c._trigger_event.assert_any_await('disconnect', '/', c.reason.SERVER_DISCONNECT) c._trigger_event.assert_any_await('__disconnect_final', '/') assert c.namespaces == {'/foo': '1', '/bar': '2'} assert c.connected async def test_handle_event(self): c = async_client.AsyncClient() c._trigger_event = mock.AsyncMock() await c._handle_event('/', None, ['foo', ('bar', 'baz')]) c._trigger_event.assert_awaited_once_with( 'foo', '/', ('bar', 'baz') ) async def test_handle_event_with_id_no_arguments(self): c = async_client.AsyncClient() c._trigger_event = mock.AsyncMock(return_value=None) c._send_packet = mock.AsyncMock() await c._handle_event('/', 123, ['foo', ('bar', 'baz')]) c._trigger_event.assert_awaited_once_with( 'foo', '/', ('bar', 'baz') ) assert c._send_packet.await_count == 1 expected_packet = packet.Packet( packet.ACK, namespace='/', id=123, data=[]) assert ( c._send_packet.await_args_list[0][0][0].encode() == expected_packet.encode() ) async def test_handle_event_with_id_one_argument(self): c = async_client.AsyncClient() c._trigger_event = mock.AsyncMock(return_value='ret') c._send_packet = mock.AsyncMock() await c._handle_event('/', 123, ['foo', ('bar', 'baz')]) c._trigger_event.assert_awaited_once_with( 'foo', '/', ('bar', 'baz') ) assert c._send_packet.await_count == 1 expected_packet = packet.Packet( packet.ACK, namespace='/', id=123, data=['ret']) assert ( c._send_packet.await_args_list[0][0][0].encode() == expected_packet.encode() ) async def test_handle_event_with_id_one_list_argument(self): c = async_client.AsyncClient() c._trigger_event = mock.AsyncMock(return_value=['a', 'b']) c._send_packet = mock.AsyncMock() await c._handle_event('/', 123, ['foo', ('bar', 'baz')]) c._trigger_event.assert_awaited_once_with( 'foo', '/', ('bar', 'baz') ) assert c._send_packet.await_count == 1 expected_packet = packet.Packet( packet.ACK, namespace='/', id=123, data=[['a', 'b']]) assert ( c._send_packet.await_args_list[0][0][0].encode() == expected_packet.encode() ) async def test_handle_event_with_id_two_arguments(self): c = async_client.AsyncClient() c._trigger_event = mock.AsyncMock(return_value=('a', 'b')) c._send_packet = mock.AsyncMock() await c._handle_event('/', 123, ['foo', ('bar', 'baz')]) c._trigger_event.assert_awaited_once_with( 'foo', '/', ('bar', 'baz') ) assert c._send_packet.await_count == 1 expected_packet = packet.Packet( packet.ACK, namespace='/', id=123, data=['a', 'b']) assert ( c._send_packet.await_args_list[0][0][0].encode() == expected_packet.encode() ) async def test_handle_ack(self): c = async_client.AsyncClient() mock_cb = mock.MagicMock() c.callbacks['/foo'] = {123: mock_cb} await c._handle_ack('/foo', 123, ['bar', 'baz']) mock_cb.assert_called_once_with('bar', 'baz') assert 123 not in c.callbacks['/foo'] async def test_handle_ack_async(self): c = async_client.AsyncClient() mock_cb = mock.AsyncMock() c.callbacks['/foo'] = {123: mock_cb} await c._handle_ack('/foo', 123, ['bar', 'baz']) mock_cb.assert_awaited_once_with('bar', 'baz') assert 123 not in c.callbacks['/foo'] async def test_handle_ack_not_found(self): c = async_client.AsyncClient() mock_cb = mock.MagicMock() c.callbacks['/foo'] = {123: mock_cb} await c._handle_ack('/foo', 124, ['bar', 'baz']) mock_cb.assert_not_called() assert 123 in c.callbacks['/foo'] async def test_handle_error(self): c = async_client.AsyncClient() c.connected = True c._connect_event = mock.MagicMock() c._trigger_event = mock.AsyncMock() c.namespaces = {'/foo': '1', '/bar': '2'} await c._handle_error('/', 'error') assert c.namespaces == {} assert not c.connected c._connect_event.set.assert_called_once_with() c._trigger_event.assert_awaited_once_with( 'connect_error', '/', 'error' ) async def test_handle_error_with_no_arguments(self): c = async_client.AsyncClient() c.connected = True c._connect_event = mock.MagicMock() c._trigger_event = mock.AsyncMock() c.namespaces = {'/foo': '1', '/bar': '2'} await c._handle_error('/', None) assert c.namespaces == {} assert not c.connected c._connect_event.set.assert_called_once_with() c._trigger_event.assert_awaited_once_with('connect_error', '/') async def test_handle_error_namespace(self): c = async_client.AsyncClient() c.connected = True c.namespaces = {'/foo': '1', '/bar': '2'} c._connect_event = mock.MagicMock() c._trigger_event = mock.AsyncMock() await c._handle_error('/bar', ['error', 'message']) assert c.namespaces == {'/foo': '1'} assert c.connected c._connect_event.set.assert_called_once_with() c._trigger_event.assert_awaited_once_with( 'connect_error', '/bar', 'error', 'message' ) async def test_handle_error_namespace_with_no_arguments(self): c = async_client.AsyncClient() c.connected = True c.namespaces = {'/foo': '1', '/bar': '2'} c._connect_event = mock.MagicMock() c._trigger_event = mock.AsyncMock() await c._handle_error('/bar', None) assert c.namespaces == {'/foo': '1'} assert c.connected c._connect_event.set.assert_called_once_with() c._trigger_event.assert_awaited_once_with('connect_error', '/bar') async def test_handle_error_unknown_namespace(self): c = async_client.AsyncClient() c.connected = True c.namespaces = {'/foo': '1', '/bar': '2'} c._connect_event = mock.MagicMock() await c._handle_error('/baz', 'error') assert c.namespaces == {'/foo': '1', '/bar': '2'} assert c.connected c._connect_event.set.assert_called_once_with() async def test_trigger_event(self): c = async_client.AsyncClient() handler = mock.MagicMock() catchall_handler = mock.MagicMock() c.on('foo', handler) c.on('*', catchall_handler) await c._trigger_event('foo', '/', 1, '2') await c._trigger_event('bar', '/', 1, '2', 3) await c._trigger_event('connect', '/') # should not trigger handler.assert_called_once_with(1, '2') catchall_handler.assert_called_once_with('bar', 1, '2', 3) async def test_trigger_event_namespace(self): c = async_client.AsyncClient() handler = mock.AsyncMock() catchall_handler = mock.AsyncMock() c.on('foo', handler, namespace='/bar') c.on('*', catchall_handler, namespace='/bar') await c._trigger_event('foo', '/bar', 1, '2') await c._trigger_event('bar', '/bar', 1, '2', 3) handler.assert_awaited_once_with(1, '2') catchall_handler.assert_awaited_once_with('bar', 1, '2', 3) async def test_trigger_legacy_disconnect_event(self): c = async_client.AsyncClient() @c.on('disconnect') def baz(): return 'baz' r = await c._trigger_event('disconnect', '/', 'foo') assert r == 'baz' async def test_trigger_legacy_disconnect_event_async(self): c = async_client.AsyncClient() @c.on('disconnect') async def baz(): return 'baz' r = await c._trigger_event('disconnect', '/', 'foo') assert r == 'baz' async def test_trigger_event_class_namespace(self): c = async_client.AsyncClient() result = [] class MyNamespace(async_namespace.AsyncClientNamespace): def on_foo(self, a, b): result.append(a) result.append(b) c.register_namespace(MyNamespace('/')) await c._trigger_event('foo', '/', 1, '2') assert result == [1, '2'] async def test_trigger_event_with_catchall_class_namespace(self): result = {} class MyNamespace(async_namespace.AsyncClientNamespace): def on_connect(self, ns): result['result'] = (ns,) def on_disconnect(self, ns): result['result'] = ('disconnect', ns) def on_foo(self, ns, data): result['result'] = (ns, data) def on_bar(self, ns): result['result'] = 'bar' + ns def on_baz(self, ns, data1, data2): result['result'] = (ns, data1, data2) c = async_client.AsyncClient() c.register_namespace(MyNamespace('*')) await c._trigger_event('connect', '/foo') assert result['result'] == ('/foo',) await c._trigger_event('foo', '/foo', 'a') assert result['result'] == ('/foo', 'a') await c._trigger_event('bar', '/foo') assert result['result'] == 'bar/foo' await c._trigger_event('baz', '/foo', 'a', 'b') assert result['result'] == ('/foo', 'a', 'b') await c._trigger_event('disconnect', '/foo') assert result['result'] == ('disconnect', '/foo') async def test_trigger_event_unknown_namespace(self): c = async_client.AsyncClient() result = [] class MyNamespace(async_namespace.AsyncClientNamespace): def on_foo(self, a, b): result.append(a) result.append(b) c.register_namespace(MyNamespace('/')) await c._trigger_event('foo', '/bar', 1, '2') assert result == [] @mock.patch( 'asyncio.wait_for', new_callable=mock.AsyncMock, side_effect=asyncio.TimeoutError, ) @mock.patch('socketio.client.random.random', side_effect=[1, 0, 0.5]) async def test_handle_reconnect(self, random, wait_for): c = async_client.AsyncClient() c._reconnect_task = 'foo' c.connect = mock.AsyncMock( side_effect=[ValueError, exceptions.ConnectionError, None] ) await c._handle_reconnect() assert wait_for.await_count == 3 assert [x[0][1] for x in asyncio.wait_for.await_args_list] == [ 1.5, 1.5, 4.0, ] assert c._reconnect_task is None @mock.patch( 'asyncio.wait_for', new_callable=mock.AsyncMock, side_effect=asyncio.TimeoutError, ) @mock.patch('socketio.client.random.random', side_effect=[1, 0, 0.5]) async def test_handle_reconnect_max_delay(self, random, wait_for): c = async_client.AsyncClient(reconnection_delay_max=3) c._reconnect_task = 'foo' c.connect = mock.AsyncMock( side_effect=[ValueError, exceptions.ConnectionError, None] ) await c._handle_reconnect() assert wait_for.await_count == 3 assert [x[0][1] for x in asyncio.wait_for.await_args_list] == [ 1.5, 1.5, 3.0, ] assert c._reconnect_task is None @mock.patch( 'asyncio.wait_for', new_callable=mock.AsyncMock, side_effect=asyncio.TimeoutError, ) @mock.patch('socketio.client.random.random', side_effect=[1, 0, 0.5]) async def test_handle_reconnect_max_attempts(self, random, wait_for): c = async_client.AsyncClient(reconnection_attempts=2, logger=True) c.connection_namespaces = ['/'] c._reconnect_task = 'foo' c._trigger_event = mock.AsyncMock() c.connect = mock.AsyncMock( side_effect=[ValueError, exceptions.ConnectionError, None] ) await c._handle_reconnect() assert wait_for.await_count == 2 assert [x[0][1] for x in asyncio.wait_for.await_args_list] == [ 1.5, 1.5, ] assert c._reconnect_task == 'foo' c._trigger_event.assert_awaited_once_with('__disconnect_final', namespace='/') @mock.patch( 'asyncio.wait_for', new_callable=mock.AsyncMock, side_effect=[asyncio.TimeoutError, None], ) @mock.patch('socketio.client.random.random', side_effect=[1, 0, 0.5]) async def test_handle_reconnect_aborted(self, random, wait_for): c = async_client.AsyncClient(logger=True) c.connection_namespaces = ['/'] c._reconnect_task = 'foo' c._trigger_event = mock.AsyncMock() c.connect = mock.AsyncMock( side_effect=[ValueError, exceptions.ConnectionError, None] ) await c._handle_reconnect() assert wait_for.await_count == 2 assert [x[0][1] for x in asyncio.wait_for.await_args_list] == [ 1.5, 1.5, ] assert c._reconnect_task == 'foo' c._trigger_event.assert_awaited_once_with('__disconnect_final', namespace='/') async def test_shutdown_disconnect(self): c = async_client.AsyncClient() c.connected = True c.namespaces = {'/': '1'} c._trigger_event = mock.AsyncMock() c._send_packet = mock.AsyncMock() c.eio = mock.MagicMock() c.eio.disconnect = mock.AsyncMock() c.eio.state = 'connected' await c.shutdown() assert c._trigger_event.await_count == 0 assert c._send_packet.await_count == 1 expected_packet = packet.Packet(packet.DISCONNECT, namespace='/') assert ( c._send_packet.await_args_list[0][0][0].encode() == expected_packet.encode() ) c.eio.disconnect.assert_awaited_once_with() async def test_shutdown_disconnect_namespaces(self): c = async_client.AsyncClient() c.connected = True c.namespaces = {'/foo': '1', '/bar': '2'} c._trigger_event = mock.AsyncMock() c._send_packet = mock.AsyncMock() c.eio = mock.MagicMock() c.eio.disconnect = mock.AsyncMock() c.eio.state = 'connected' await c.shutdown() assert c._trigger_event.await_count == 0 assert c._send_packet.await_count == 2 expected_packet = packet.Packet(packet.DISCONNECT, namespace='/foo') assert ( c._send_packet.await_args_list[0][0][0].encode() == expected_packet.encode() ) expected_packet = packet.Packet(packet.DISCONNECT, namespace='/bar') assert ( c._send_packet.await_args_list[1][0][0].encode() == expected_packet.encode() ) @mock.patch('socketio.client.random.random', side_effect=[1, 0, 0.5]) async def test_shutdown_reconnect(self, random): c = async_client.AsyncClient() c.connection_namespaces = ['/'] c._reconnect_task = mock.AsyncMock()() c._trigger_event = mock.AsyncMock() c.connect = mock.AsyncMock(side_effect=exceptions.ConnectionError) async def r(): task = c.start_background_task(c._handle_reconnect) await asyncio.sleep(0.1) await c.shutdown() await task await r() c._trigger_event.assert_awaited_once_with('__disconnect_final', namespace='/') async def test_handle_eio_connect(self): c = async_client.AsyncClient() c.connection_namespaces = ['/', '/foo'] c.connection_auth = 'auth' c._send_packet = mock.AsyncMock() c.eio.sid = 'foo' assert c.sid is None await c._handle_eio_connect() assert c.sid == 'foo' assert c._send_packet.await_count == 2 expected_packet = packet.Packet( packet.CONNECT, data='auth', namespace='/') assert ( c._send_packet.await_args_list[0][0][0].encode() == expected_packet.encode() ) expected_packet = packet.Packet( packet.CONNECT, data='auth', namespace='/foo') assert ( c._send_packet.await_args_list[1][0][0].encode() == expected_packet.encode() ) async def test_handle_eio_connect_function(self): c = async_client.AsyncClient() c.connection_namespaces = ['/', '/foo'] c.connection_auth = lambda: 'auth' c._send_packet = mock.AsyncMock() c.eio.sid = 'foo' assert c.sid is None await c._handle_eio_connect() assert c.sid == 'foo' assert c._send_packet.await_count == 2 expected_packet = packet.Packet( packet.CONNECT, data='auth', namespace='/') assert ( c._send_packet.await_args_list[0][0][0].encode() == expected_packet.encode() ) expected_packet = packet.Packet( packet.CONNECT, data='auth', namespace='/foo') assert ( c._send_packet.await_args_list[1][0][0].encode() == expected_packet.encode() ) async def test_handle_eio_message(self): c = async_client.AsyncClient() c._handle_connect = mock.AsyncMock() c._handle_disconnect = mock.AsyncMock() c._handle_event = mock.AsyncMock() c._handle_ack = mock.AsyncMock() c._handle_error = mock.AsyncMock() await c._handle_eio_message('0{"sid":"123"}') c._handle_connect.assert_awaited_with(None, {'sid': '123'}) await c._handle_eio_message('0/foo,{"sid":"123"}') c._handle_connect.assert_awaited_with('/foo', {'sid': '123'}) await c._handle_eio_message('1') c._handle_disconnect.assert_awaited_with(None) await c._handle_eio_message('1/foo') c._handle_disconnect.assert_awaited_with('/foo') await c._handle_eio_message('2["foo"]') c._handle_event.assert_awaited_with(None, None, ['foo']) await c._handle_eio_message('3/foo,["bar"]') c._handle_ack.assert_awaited_with('/foo', None, ['bar']) await c._handle_eio_message('4') c._handle_error.assert_awaited_with(None, None) await c._handle_eio_message('4"foo"') c._handle_error.assert_awaited_with(None, 'foo') await c._handle_eio_message('4["foo"]') c._handle_error.assert_awaited_with(None, ['foo']) await c._handle_eio_message('4/foo') c._handle_error.assert_awaited_with('/foo', None) await c._handle_eio_message('4/foo,["foo","bar"]') c._handle_error.assert_awaited_with('/foo', ['foo', 'bar']) await c._handle_eio_message('51-{"_placeholder":true,"num":0}') assert c._binary_packet.packet_type == packet.BINARY_EVENT await c._handle_eio_message(b'foo') c._handle_event.assert_awaited_with(None, None, b'foo') await c._handle_eio_message( '62-/foo,{"1":{"_placeholder":true,"num":1},' '"2":{"_placeholder":true,"num":0}}' ) assert c._binary_packet.packet_type == packet.BINARY_ACK await c._handle_eio_message(b'bar') await c._handle_eio_message(b'foo') c._handle_ack.assert_awaited_with( '/foo', None, {'1': b'foo', '2': b'bar'} ) with pytest.raises(ValueError): await c._handle_eio_message('9') async def test_eio_disconnect(self): c = async_client.AsyncClient() c.namespaces = {'/': '1'} c.connected = True c._trigger_event = mock.AsyncMock() c.start_background_task = mock.MagicMock() c.sid = 'foo' c.eio.state = 'connected' await c._handle_eio_disconnect('foo') c._trigger_event.assert_awaited_once_with('disconnect', '/', 'foo') assert c.sid is None assert not c.connected async def test_eio_disconnect_namespaces(self): c = async_client.AsyncClient(reconnection=False) c.namespaces = {'/foo': '1', '/bar': '2'} c.connected = True c._trigger_event = mock.AsyncMock() c.sid = 'foo' c.eio.state = 'connected' await c._handle_eio_disconnect(c.reason.CLIENT_DISCONNECT) c._trigger_event.assert_any_await('disconnect', '/foo', c.reason.CLIENT_DISCONNECT) c._trigger_event.assert_any_await('disconnect', '/bar', c.reason.CLIENT_DISCONNECT) c._trigger_event.asserT_any_await('disconnect', '/', c.reason.CLIENT_DISCONNECT) assert c.sid is None assert not c.connected async def test_eio_disconnect_reconnect(self): c = async_client.AsyncClient(reconnection=True) c.start_background_task = mock.MagicMock() c.eio.state = 'connected' await c._handle_eio_disconnect(c.reason.CLIENT_DISCONNECT) c.start_background_task.assert_called_once_with(c._handle_reconnect) async def test_eio_disconnect_self_disconnect(self): c = async_client.AsyncClient(reconnection=True) c.start_background_task = mock.MagicMock() c.eio.state = 'disconnected' await c._handle_eio_disconnect(c.reason.CLIENT_DISCONNECT) c.start_background_task.assert_not_called() async def test_eio_disconnect_no_reconnect(self): c = async_client.AsyncClient(reconnection=False) c.namespaces = {'/': '1'} c.connected = True c._trigger_event = mock.AsyncMock() c.start_background_task = mock.MagicMock() c.sid = 'foo' c.eio.state = 'connected' await c._handle_eio_disconnect(c.reason.TRANSPORT_ERROR) c._trigger_event.assert_any_await('disconnect', '/', c.reason.TRANSPORT_ERROR) c._trigger_event.assert_any_await('__disconnect_final', '/') assert c.sid is None assert not c.connected c.start_background_task.assert_not_called() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1734543553.0 python_socketio-5.13.0/tests/async/test_manager.py0000664000175000017500000004070614730604301021720 0ustar00miguelmiguelfrom unittest import mock from socketio import async_manager from socketio import packet class TestAsyncManager: def setup_method(self): id = 0 def generate_id(): nonlocal id id += 1 return str(id) mock_server = mock.MagicMock() mock_server._send_packet = mock.AsyncMock() mock_server._send_eio_packet = mock.AsyncMock() mock_server.eio.generate_id = generate_id mock_server.packet_class = packet.Packet self.bm = async_manager.AsyncManager() self.bm.set_server(mock_server) self.bm.initialize() async def test_connect(self): sid = await self.bm.connect('123', '/foo') assert None in self.bm.rooms['/foo'] assert sid in self.bm.rooms['/foo'] assert sid in self.bm.rooms['/foo'][None] assert sid in self.bm.rooms['/foo'][sid] assert dict(self.bm.rooms['/foo'][None]) == {sid: '123'} assert dict(self.bm.rooms['/foo'][sid]) == {sid: '123'} assert self.bm.sid_from_eio_sid('123', '/foo') == sid async def test_pre_disconnect(self): sid1 = await self.bm.connect('123', '/foo') sid2 = await self.bm.connect('456', '/foo') assert self.bm.is_connected(sid1, '/foo') assert self.bm.pre_disconnect(sid1, '/foo') == '123' assert self.bm.pending_disconnect == {'/foo': [sid1]} assert not self.bm.is_connected(sid1, '/foo') assert self.bm.pre_disconnect(sid2, '/foo') == '456' assert self.bm.pending_disconnect == {'/foo': [sid1, sid2]} assert not self.bm.is_connected(sid2, '/foo') await self.bm.disconnect(sid1, '/foo') assert self.bm.pending_disconnect == {'/foo': [sid2]} await self.bm.disconnect(sid2, '/foo') assert self.bm.pending_disconnect == {} async def test_disconnect(self): sid1 = await self.bm.connect('123', '/foo') sid2 = await self.bm.connect('456', '/foo') await self.bm.enter_room(sid1, '/foo', 'bar') await self.bm.enter_room(sid2, '/foo', 'baz') await self.bm.disconnect(sid1, '/foo') assert dict(self.bm.rooms['/foo'][None]) == {sid2: '456'} assert dict(self.bm.rooms['/foo'][sid2]) == {sid2: '456'} assert dict(self.bm.rooms['/foo']['baz']) == {sid2: '456'} async def test_disconnect_default_namespace(self): sid1 = await self.bm.connect('123', '/') sid2 = await self.bm.connect('123', '/foo') sid3 = await self.bm.connect('456', '/') sid4 = await self.bm.connect('456', '/foo') assert self.bm.is_connected(sid1, '/') assert self.bm.is_connected(sid2, '/foo') assert not self.bm.is_connected(sid2, '/') assert not self.bm.is_connected(sid1, '/foo') await self.bm.disconnect(sid1, '/') assert not self.bm.is_connected(sid1, '/') assert self.bm.is_connected(sid2, '/foo') await self.bm.disconnect(sid2, '/foo') assert not self.bm.is_connected(sid2, '/foo') assert dict(self.bm.rooms['/'][None]) == {sid3: '456'} assert dict(self.bm.rooms['/'][sid3]) == {sid3: '456'} assert dict(self.bm.rooms['/foo'][None]) == {sid4: '456'} assert dict(self.bm.rooms['/foo'][sid4]) == {sid4: '456'} async def test_disconnect_twice(self): sid1 = await self.bm.connect('123', '/') sid2 = await self.bm.connect('123', '/foo') sid3 = await self.bm.connect('456', '/') sid4 = await self.bm.connect('456', '/foo') await self.bm.disconnect(sid1, '/') await self.bm.disconnect(sid2, '/foo') await self.bm.disconnect(sid1, '/') await self.bm.disconnect(sid2, '/foo') assert dict(self.bm.rooms['/'][None]) == {sid3: '456'} assert dict(self.bm.rooms['/'][sid3]) == {sid3: '456'} assert dict(self.bm.rooms['/foo'][None]) == {sid4: '456'} assert dict(self.bm.rooms['/foo'][sid4]) == {sid4: '456'} async def test_disconnect_all(self): sid1 = await self.bm.connect('123', '/foo') sid2 = await self.bm.connect('456', '/foo') await self.bm.enter_room(sid1, '/foo', 'bar') await self.bm.enter_room(sid2, '/foo', 'baz') await self.bm.disconnect(sid1, '/foo') await self.bm.disconnect(sid2, '/foo') assert self.bm.rooms == {} async def test_disconnect_with_callbacks(self): sid1 = await self.bm.connect('123', '/') sid2 = await self.bm.connect('123', '/foo') sid3 = await self.bm.connect('456', '/foo') self.bm._generate_ack_id(sid1, 'f') self.bm._generate_ack_id(sid2, 'g') self.bm._generate_ack_id(sid3, 'h') await self.bm.disconnect(sid2, '/foo') assert sid2 not in self.bm.callbacks await self.bm.disconnect(sid1, '/') assert sid1 not in self.bm.callbacks assert sid3 in self.bm.callbacks async def test_trigger_sync_callback(self): sid1 = await self.bm.connect('123', '/') sid2 = await self.bm.connect('123', '/foo') cb = mock.MagicMock() id1 = self.bm._generate_ack_id(sid1, cb) id2 = self.bm._generate_ack_id(sid2, cb) await self.bm.trigger_callback(sid1, id1, ['foo']) await self.bm.trigger_callback(sid2, id2, ['bar', 'baz']) assert cb.call_count == 2 cb.assert_any_call('foo') cb.assert_any_call('bar', 'baz') async def test_trigger_async_callback(self): sid1 = await self.bm.connect('123', '/') sid2 = await self.bm.connect('123', '/foo') cb = mock.AsyncMock() id1 = self.bm._generate_ack_id(sid1, cb) id2 = self.bm._generate_ack_id(sid2, cb) await self.bm.trigger_callback(sid1, id1, ['foo']) await self.bm.trigger_callback(sid2, id2, ['bar', 'baz']) assert cb.await_count == 2 cb.assert_any_await('foo') cb.assert_any_await('bar', 'baz') async def test_invalid_callback(self): sid = await self.bm.connect('123', '/') cb = mock.MagicMock() id = self.bm._generate_ack_id(sid, cb) # these should not raise an exception await self.bm.trigger_callback('xxx', id, ['foo']) await self.bm.trigger_callback(sid, id + 1, ['foo']) assert cb.call_count == 0 async def test_get_namespaces(self): assert list(self.bm.get_namespaces()) == [] await self.bm.connect('123', '/') await self.bm.connect('123', '/foo') namespaces = list(self.bm.get_namespaces()) assert len(namespaces) == 2 assert '/' in namespaces assert '/foo' in namespaces async def test_get_participants(self): sid1 = await self.bm.connect('123', '/') sid2 = await self.bm.connect('456', '/') sid3 = await self.bm.connect('789', '/') await self.bm.disconnect(sid3, '/') assert sid3 not in self.bm.rooms['/'][None] participants = list(self.bm.get_participants('/', None)) assert len(participants) == 2 assert (sid1, '123') in participants assert (sid2, '456') in participants assert (sid3, '789') not in participants async def test_leave_invalid_room(self): sid = await self.bm.connect('123', '/foo') await self.bm.leave_room(sid, '/foo', 'baz') await self.bm.leave_room(sid, '/bar', 'baz') async def test_no_room(self): rooms = self.bm.get_rooms('123', '/foo') assert [] == rooms async def test_close_room(self): sid = await self.bm.connect('123', '/foo') await self.bm.connect('456', '/foo') await self.bm.connect('789', '/foo') await self.bm.enter_room(sid, '/foo', 'bar') await self.bm.enter_room(sid, '/foo', 'bar') await self.bm.close_room('bar', '/foo') assert 'bar' not in self.bm.rooms['/foo'] async def test_close_invalid_room(self): self.bm.close_room('bar', '/foo') async def test_rooms(self): sid = await self.bm.connect('123', '/foo') await self.bm.enter_room(sid, '/foo', 'bar') r = self.bm.get_rooms(sid, '/foo') assert len(r) == 2 assert sid in r assert 'bar' in r async def test_emit_to_sid(self): sid = await self.bm.connect('123', '/foo') await self.bm.connect('456', '/foo') await self.bm.emit( 'my event', {'foo': 'bar'}, namespace='/foo', to=sid ) assert self.bm.server._send_eio_packet.await_count == 1 assert self.bm.server._send_eio_packet.await_args_list[0][0][0] \ == '123' pkt = self.bm.server._send_eio_packet.await_args_list[0][0][1] assert pkt.encode() == '42/foo,["my event",{"foo":"bar"}]' async def test_emit_to_room(self): sid1 = await self.bm.connect('123', '/foo') await self.bm.enter_room(sid1, '/foo', 'bar') sid2 = await self.bm.connect('456', '/foo') await self.bm.enter_room(sid2, '/foo', 'bar') await self.bm.connect('789', '/foo') await self.bm.emit( 'my event', {'foo': 'bar'}, namespace='/foo', room='bar' ) assert self.bm.server._send_eio_packet.await_count == 2 assert self.bm.server._send_eio_packet.await_args_list[0][0][0] \ == '123' assert self.bm.server._send_eio_packet.await_args_list[1][0][0] \ == '456' pkt = self.bm.server._send_eio_packet.await_args_list[0][0][1] assert self.bm.server._send_eio_packet.await_args_list[1][0][1] \ == pkt assert pkt.encode() == '42/foo,["my event",{"foo":"bar"}]' async def test_emit_to_rooms(self): sid1 = await self.bm.connect('123', '/foo') await self.bm.enter_room(sid1, '/foo', 'bar') sid2 = await self.bm.connect('456', '/foo') await self.bm.enter_room(sid2, '/foo', 'bar') await self.bm.enter_room(sid2, '/foo', 'baz') sid3 = await self.bm.connect('789', '/foo') await self.bm.enter_room(sid3, '/foo', 'baz') await self.bm.emit('my event', {'foo': 'bar'}, namespace='/foo', room=['bar', 'baz']) assert self.bm.server._send_eio_packet.await_count == 3 assert self.bm.server._send_eio_packet.await_args_list[0][0][0] \ == '123' assert self.bm.server._send_eio_packet.await_args_list[1][0][0] \ == '456' assert self.bm.server._send_eio_packet.await_args_list[2][0][0] \ == '789' pkt = self.bm.server._send_eio_packet.await_args_list[0][0][1] assert self.bm.server._send_eio_packet.await_args_list[1][0][1] \ == pkt assert self.bm.server._send_eio_packet.await_args_list[2][0][1] \ == pkt assert pkt.encode() == '42/foo,["my event",{"foo":"bar"}]' async def test_emit_to_all(self): sid1 = await self.bm.connect('123', '/foo') await self.bm.enter_room(sid1, '/foo', 'bar') sid2 = await self.bm.connect('456', '/foo') await self.bm.enter_room(sid2, '/foo', 'bar') await self.bm.connect('789', '/foo') await self.bm.connect('abc', '/bar') await self.bm.emit('my event', {'foo': 'bar'}, namespace='/foo') assert self.bm.server._send_eio_packet.await_count == 3 assert self.bm.server._send_eio_packet.await_args_list[0][0][0] \ == '123' assert self.bm.server._send_eio_packet.await_args_list[1][0][0] \ == '456' assert self.bm.server._send_eio_packet.await_args_list[2][0][0] \ == '789' pkt = self.bm.server._send_eio_packet.await_args_list[0][0][1] assert self.bm.server._send_eio_packet.await_args_list[1][0][1] \ == pkt assert self.bm.server._send_eio_packet.await_args_list[2][0][1] \ == pkt assert pkt.encode() == '42/foo,["my event",{"foo":"bar"}]' async def test_emit_to_all_skip_one(self): sid1 = await self.bm.connect('123', '/foo') await self.bm.enter_room(sid1, '/foo', 'bar') sid2 = await self.bm.connect('456', '/foo') await self.bm.enter_room(sid2, '/foo', 'bar') await self.bm.connect('789', '/foo') await self.bm.connect('abc', '/bar') await self.bm.emit( 'my event', {'foo': 'bar'}, namespace='/foo', skip_sid=sid2 ) assert self.bm.server._send_eio_packet.await_count == 2 assert self.bm.server._send_eio_packet.await_args_list[0][0][0] \ == '123' assert self.bm.server._send_eio_packet.await_args_list[1][0][0] \ == '789' pkt = self.bm.server._send_eio_packet.await_args_list[0][0][1] assert self.bm.server._send_eio_packet.await_args_list[1][0][1] \ == pkt assert pkt.encode() == '42/foo,["my event",{"foo":"bar"}]' async def test_emit_to_all_skip_two(self): sid1 = await self.bm.connect('123', '/foo') await self.bm.enter_room(sid1, '/foo', 'bar') sid2 = await self.bm.connect('456', '/foo') await self.bm.enter_room(sid2, '/foo', 'bar') sid3 = await self.bm.connect('789', '/foo') await self.bm.connect('abc', '/bar') await self.bm.emit( 'my event', {'foo': 'bar'}, namespace='/foo', skip_sid=[sid1, sid3], ) assert self.bm.server._send_eio_packet.await_count == 1 assert self.bm.server._send_eio_packet.await_args_list[0][0][0] \ == '456' pkt = self.bm.server._send_eio_packet.await_args_list[0][0][1] assert pkt.encode() == '42/foo,["my event",{"foo":"bar"}]' async def test_emit_with_callback(self): sid = await self.bm.connect('123', '/foo') self.bm._generate_ack_id = mock.MagicMock() self.bm._generate_ack_id.return_value = 11 await self.bm.emit( 'my event', {'foo': 'bar'}, namespace='/foo', callback='cb' ) self.bm._generate_ack_id.assert_called_once_with(sid, 'cb') assert self.bm.server._send_packet.await_count == 1 assert self.bm.server._send_packet.await_args_list[0][0][0] \ == '123' pkt = self.bm.server._send_packet.await_args_list[0][0][1] assert pkt.encode() == '2/foo,11["my event",{"foo":"bar"}]' async def test_emit_to_invalid_room(self): await self.bm.emit('my event', {'foo': 'bar'}, namespace='/', room='123') async def test_emit_to_invalid_namespace(self): await self.bm.emit('my event', {'foo': 'bar'}, namespace='/foo') async def test_emit_with_tuple(self): sid = await self.bm.connect('123', '/foo') await self.bm.emit( 'my event', ('foo', 'bar'), namespace='/foo', room=sid ) assert self.bm.server._send_eio_packet.await_count == 1 assert self.bm.server._send_eio_packet.await_args_list[0][0][0] \ == '123' pkt = self.bm.server._send_eio_packet.await_args_list[0][0][1] assert pkt.encode() == '42/foo,["my event","foo","bar"]' async def test_emit_with_list(self): sid = await self.bm.connect('123', '/foo') await self.bm.emit( 'my event', ['foo', 'bar'], namespace='/foo', room=sid ) assert self.bm.server._send_eio_packet.await_count == 1 assert self.bm.server._send_eio_packet.await_args_list[0][0][0] \ == '123' pkt = self.bm.server._send_eio_packet.await_args_list[0][0][1] assert pkt.encode() == '42/foo,["my event",["foo","bar"]]' async def test_emit_with_none(self): sid = await self.bm.connect('123', '/foo') await self.bm.emit( 'my event', None, namespace='/foo', room=sid ) assert self.bm.server._send_eio_packet.await_count == 1 assert self.bm.server._send_eio_packet.await_args_list[0][0][0] \ == '123' pkt = self.bm.server._send_eio_packet.await_args_list[0][0][1] assert pkt.encode() == '42/foo,["my event"]' async def test_emit_binary(self): sid = await self.bm.connect('123', '/') await self.bm.emit( 'my event', b'my binary data', namespace='/', room=sid ) assert self.bm.server._send_eio_packet.await_count == 2 assert self.bm.server._send_eio_packet.await_args_list[0][0][0] \ == '123' pkt = self.bm.server._send_eio_packet.await_args_list[0][0][1] assert pkt.encode() == '451-["my event",{"_placeholder":true,"num":0}]' assert self.bm.server._send_eio_packet.await_args_list[1][0][0] \ == '123' pkt = self.bm.server._send_eio_packet.await_args_list[1][0][1] assert pkt.encode() == b'my binary data' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1734543553.0 python_socketio-5.13.0/tests/async/test_namespace.py0000664000175000017500000003262514730604301022243 0ustar00miguelmiguelfrom unittest import mock from socketio import async_namespace class TestAsyncNamespace: async def test_connect_event(self): result = {} class MyNamespace(async_namespace.AsyncNamespace): async def on_connect(self, sid, environ): result['result'] = (sid, environ) ns = MyNamespace('/foo') ns._set_server(mock.MagicMock()) await ns.trigger_event('connect', 'sid', {'foo': 'bar'}) assert result['result'] == ('sid', {'foo': 'bar'}) async def test_disconnect_event(self): result = {} class MyNamespace(async_namespace.AsyncNamespace): async def on_disconnect(self, sid, reason): result['result'] = (sid, reason) ns = MyNamespace('/foo') ns._set_server(mock.MagicMock()) await ns.trigger_event('disconnect', 'sid', 'foo') assert result['result'] == ('sid', 'foo') async def test_legacy_disconnect_event(self): result = {} class MyNamespace(async_namespace.AsyncNamespace): def on_disconnect(self, sid): result['result'] = sid ns = MyNamespace('/foo') ns._set_server(mock.MagicMock()) await ns.trigger_event('disconnect', 'sid', 'foo') assert result['result'] == 'sid' async def test_legacy_disconnect_event_async(self): result = {} class MyNamespace(async_namespace.AsyncNamespace): async def on_disconnect(self, sid): result['result'] = sid ns = MyNamespace('/foo') ns._set_server(mock.MagicMock()) await ns.trigger_event('disconnect', 'sid', 'foo') assert result['result'] == 'sid' async def test_sync_event(self): result = {} class MyNamespace(async_namespace.AsyncNamespace): def on_custom_message(self, sid, data): result['result'] = (sid, data) ns = MyNamespace('/foo') ns._set_server(mock.MagicMock()) await ns.trigger_event('custom_message', 'sid', {'data': 'data'}) assert result['result'] == ('sid', {'data': 'data'}) async def test_async_event(self): result = {} class MyNamespace(async_namespace.AsyncNamespace): async def on_custom_message(self, sid, data): result['result'] = (sid, data) ns = MyNamespace('/foo') ns._set_server(mock.MagicMock()) await ns.trigger_event('custom_message', 'sid', {'data': 'data'}) assert result['result'] == ('sid', {'data': 'data'}) async def test_event_not_found(self): result = {} class MyNamespace(async_namespace.AsyncNamespace): async def on_custom_message(self, sid, data): result['result'] = (sid, data) ns = MyNamespace('/foo') ns._set_server(mock.MagicMock()) await ns.trigger_event('another_custom_message', 'sid', {'data': 'data'}) assert result == {} async def test_emit(self): ns = async_namespace.AsyncNamespace('/foo') mock_server = mock.MagicMock() mock_server.emit = mock.AsyncMock() ns._set_server(mock_server) await ns.emit( 'ev', data='data', to='room', skip_sid='skip', callback='cb' ) ns.server.emit.assert_awaited_with( 'ev', data='data', to='room', room=None, skip_sid='skip', namespace='/foo', callback='cb', ignore_queue=False, ) await ns.emit( 'ev', data='data', room='room', skip_sid='skip', namespace='/bar', callback='cb', ignore_queue=True, ) ns.server.emit.assert_awaited_with( 'ev', data='data', to=None, room='room', skip_sid='skip', namespace='/bar', callback='cb', ignore_queue=True, ) async def test_send(self): ns = async_namespace.AsyncNamespace('/foo') mock_server = mock.MagicMock() mock_server.send = mock.AsyncMock() ns._set_server(mock_server) await ns.send(data='data', to='room', skip_sid='skip', callback='cb') ns.server.send.assert_awaited_with( 'data', to='room', room=None, skip_sid='skip', namespace='/foo', callback='cb', ignore_queue=False, ) await ns.send( data='data', room='room', skip_sid='skip', namespace='/bar', callback='cb', ignore_queue=True, ) ns.server.send.assert_awaited_with( 'data', to=None, room='room', skip_sid='skip', namespace='/bar', callback='cb', ignore_queue=True, ) async def test_call(self): ns = async_namespace.AsyncNamespace('/foo') mock_server = mock.MagicMock() mock_server.call = mock.AsyncMock() ns._set_server(mock_server) await ns.call('ev', data='data', to='sid') ns.server.call.assert_awaited_with( 'ev', data='data', to='sid', sid=None, namespace='/foo', timeout=None, ignore_queue=False, ) await ns.call('ev', data='data', sid='sid', namespace='/bar', timeout=45, ignore_queue=True) ns.server.call.assert_awaited_with( 'ev', data='data', to=None, sid='sid', namespace='/bar', timeout=45, ignore_queue=True, ) async def test_enter_room(self): ns = async_namespace.AsyncNamespace('/foo') mock_server = mock.MagicMock() mock_server.enter_room = mock.AsyncMock() ns._set_server(mock_server) await ns.enter_room('sid', 'room') ns.server.enter_room.assert_awaited_with( 'sid', 'room', namespace='/foo' ) await ns.enter_room('sid', 'room', namespace='/bar') ns.server.enter_room.assert_awaited_with( 'sid', 'room', namespace='/bar' ) async def test_leave_room(self): ns = async_namespace.AsyncNamespace('/foo') mock_server = mock.MagicMock() mock_server.leave_room = mock.AsyncMock() ns._set_server(mock_server) await ns.leave_room('sid', 'room') ns.server.leave_room.assert_awaited_with( 'sid', 'room', namespace='/foo' ) await ns.leave_room('sid', 'room', namespace='/bar') ns.server.leave_room.assert_awaited_with( 'sid', 'room', namespace='/bar' ) async def test_close_room(self): ns = async_namespace.AsyncNamespace('/foo') mock_server = mock.MagicMock() mock_server.close_room = mock.AsyncMock() ns._set_server(mock_server) await ns.close_room('room') ns.server.close_room.assert_awaited_with('room', namespace='/foo') await ns.close_room('room', namespace='/bar') ns.server.close_room.assert_awaited_with('room', namespace='/bar') async def test_rooms(self): ns = async_namespace.AsyncNamespace('/foo') ns._set_server(mock.MagicMock()) ns.rooms('sid') ns.server.rooms.assert_called_with('sid', namespace='/foo') ns.rooms('sid', namespace='/bar') ns.server.rooms.assert_called_with('sid', namespace='/bar') async def test_session(self): ns = async_namespace.AsyncNamespace('/foo') mock_server = mock.MagicMock() mock_server.get_session = mock.AsyncMock() mock_server.save_session = mock.AsyncMock() ns._set_server(mock_server) await ns.get_session('sid') ns.server.get_session.assert_awaited_with('sid', namespace='/foo') await ns.get_session('sid', namespace='/bar') ns.server.get_session.assert_awaited_with('sid', namespace='/bar') await ns.save_session('sid', {'a': 'b'}) ns.server.save_session.assert_awaited_with( 'sid', {'a': 'b'}, namespace='/foo' ) await ns.save_session('sid', {'a': 'b'}, namespace='/bar') ns.server.save_session.assert_awaited_with( 'sid', {'a': 'b'}, namespace='/bar' ) ns.session('sid') ns.server.session.assert_called_with('sid', namespace='/foo') ns.session('sid', namespace='/bar') ns.server.session.assert_called_with('sid', namespace='/bar') async def test_disconnect(self): ns = async_namespace.AsyncNamespace('/foo') mock_server = mock.MagicMock() mock_server.disconnect = mock.AsyncMock() ns._set_server(mock_server) await ns.disconnect('sid') ns.server.disconnect.assert_awaited_with('sid', namespace='/foo') await ns.disconnect('sid', namespace='/bar') ns.server.disconnect.assert_awaited_with('sid', namespace='/bar') async def test_disconnect_event_client(self): result = {} class MyNamespace(async_namespace.AsyncClientNamespace): async def on_disconnect(self, reason): result['result'] = reason ns = MyNamespace('/foo') ns._set_client(mock.MagicMock()) await ns.trigger_event('disconnect', 'foo') assert result['result'] == 'foo' async def test_legacy_disconnect_event_client(self): result = {} class MyNamespace(async_namespace.AsyncClientNamespace): def on_disconnect(self): result['result'] = 'ok' ns = MyNamespace('/foo') ns._set_client(mock.MagicMock()) await ns.trigger_event('disconnect', 'foo') assert result['result'] == 'ok' async def test_legacy_disconnect_event_client_async(self): result = {} class MyNamespace(async_namespace.AsyncClientNamespace): async def on_disconnect(self): result['result'] = 'ok' ns = MyNamespace('/foo') ns._set_client(mock.MagicMock()) await ns.trigger_event('disconnect', 'foo') assert result['result'] == 'ok' async def test_sync_event_client(self): result = {} class MyNamespace(async_namespace.AsyncClientNamespace): def on_custom_message(self, sid, data): result['result'] = (sid, data) ns = MyNamespace('/foo') ns._set_client(mock.MagicMock()) await ns.trigger_event('custom_message', 'sid', {'data': 'data'}) assert result['result'] == ('sid', {'data': 'data'}) async def test_async_event_client(self): result = {} class MyNamespace(async_namespace.AsyncClientNamespace): async def on_custom_message(self, sid, data): result['result'] = (sid, data) ns = MyNamespace('/foo') ns._set_client(mock.MagicMock()) await ns.trigger_event('custom_message', 'sid', {'data': 'data'}) assert result['result'] == ('sid', {'data': 'data'}) async def test_event_not_found_client(self): result = {} class MyNamespace(async_namespace.AsyncClientNamespace): async def on_custom_message(self, sid, data): result['result'] = (sid, data) ns = MyNamespace('/foo') ns._set_client(mock.MagicMock()) await ns.trigger_event('another_custom_message', 'sid', {'data': 'data'}) assert result == {} async def test_emit_client(self): ns = async_namespace.AsyncClientNamespace('/foo') mock_client = mock.MagicMock() mock_client.emit = mock.AsyncMock() ns._set_client(mock_client) await ns.emit('ev', data='data', callback='cb') ns.client.emit.assert_awaited_with( 'ev', data='data', namespace='/foo', callback='cb' ) await ns.emit('ev', data='data', namespace='/bar', callback='cb') ns.client.emit.assert_awaited_with( 'ev', data='data', namespace='/bar', callback='cb' ) async def test_send_client(self): ns = async_namespace.AsyncClientNamespace('/foo') mock_client = mock.MagicMock() mock_client.send = mock.AsyncMock() ns._set_client(mock_client) await ns.send(data='data', callback='cb') ns.client.send.assert_awaited_with( 'data', namespace='/foo', callback='cb' ) await ns.send(data='data', namespace='/bar', callback='cb') ns.client.send.assert_awaited_with( 'data', namespace='/bar', callback='cb' ) async def test_call_client(self): ns = async_namespace.AsyncClientNamespace('/foo') mock_client = mock.MagicMock() mock_client.call = mock.AsyncMock() ns._set_client(mock_client) await ns.call('ev', data='data') ns.client.call.assert_awaited_with( 'ev', data='data', namespace='/foo', timeout=None ) await ns.call('ev', data='data', namespace='/bar', timeout=45) ns.client.call.assert_awaited_with( 'ev', data='data', namespace='/bar', timeout=45 ) async def test_disconnect_client(self): ns = async_namespace.AsyncClientNamespace('/foo') mock_client = mock.MagicMock() mock_client.disconnect = mock.AsyncMock() ns._set_client(mock_client) await ns.disconnect() ns.client.disconnect.assert_awaited_with() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1734467514.0 python_socketio-5.13.0/tests/async/test_pubsub_manager.py0000664000175000017500000004726414730357672023326 0ustar00miguelmiguelimport asyncio import functools from unittest import mock import pytest from socketio import async_manager from socketio import async_pubsub_manager from socketio import packet class TestAsyncPubSubManager: def setup_method(self): id = 0 def generate_id(): nonlocal id id += 1 return str(id) mock_server = mock.MagicMock() mock_server.eio.generate_id = generate_id mock_server.packet_class = packet.Packet mock_server._send_packet = mock.AsyncMock() mock_server._send_eio_packet = mock.AsyncMock() mock_server.disconnect = mock.AsyncMock() self.pm = async_pubsub_manager.AsyncPubSubManager() self.pm._publish = mock.AsyncMock() self.pm.set_server(mock_server) self.pm.host_id = '123456' self.pm.initialize() async def test_default_init(self): assert self.pm.channel == 'socketio' self.pm.server.start_background_task.assert_called_once_with( self.pm._thread ) async def test_custom_init(self): pubsub = async_pubsub_manager.AsyncPubSubManager(channel='foo') assert pubsub.channel == 'foo' assert len(pubsub.host_id) == 32 async def test_write_only_init(self): mock_server = mock.MagicMock() pm = async_pubsub_manager.AsyncPubSubManager(write_only=True) pm.set_server(mock_server) pm.initialize() assert pm.channel == 'socketio' assert len(pm.host_id) == 32 assert pm.server.start_background_task.call_count == 0 async def test_emit(self): await self.pm.emit('foo', 'bar') self.pm._publish.assert_awaited_once_with( { 'method': 'emit', 'event': 'foo', 'data': 'bar', 'namespace': '/', 'room': None, 'skip_sid': None, 'callback': None, 'host_id': '123456', } ) async def test_emit_with_to(self): sid = 'room-mate' await self.pm.emit('foo', 'bar', to=sid) self.pm._publish.assert_awaited_once_with( { 'method': 'emit', 'event': 'foo', 'data': 'bar', 'namespace': '/', 'room': sid, 'skip_sid': None, 'callback': None, 'host_id': '123456', } ) async def test_emit_with_namespace(self): await self.pm.emit('foo', 'bar', namespace='/baz') self.pm._publish.assert_awaited_once_with( { 'method': 'emit', 'event': 'foo', 'data': 'bar', 'namespace': '/baz', 'room': None, 'skip_sid': None, 'callback': None, 'host_id': '123456', } ) async def test_emit_with_room(self): await self.pm.emit('foo', 'bar', room='baz') self.pm._publish.assert_awaited_once_with( { 'method': 'emit', 'event': 'foo', 'data': 'bar', 'namespace': '/', 'room': 'baz', 'skip_sid': None, 'callback': None, 'host_id': '123456', } ) async def test_emit_with_skip_sid(self): await self.pm.emit('foo', 'bar', skip_sid='baz') self.pm._publish.assert_awaited_once_with( { 'method': 'emit', 'event': 'foo', 'data': 'bar', 'namespace': '/', 'room': None, 'skip_sid': 'baz', 'callback': None, 'host_id': '123456', } ) async def test_emit_with_callback(self): with mock.patch.object( self.pm, '_generate_ack_id', return_value='123' ): await self.pm.emit('foo', 'bar', room='baz', callback='cb') self.pm._publish.assert_awaited_once_with( { 'method': 'emit', 'event': 'foo', 'data': 'bar', 'namespace': '/', 'room': 'baz', 'skip_sid': None, 'callback': ('baz', '/', '123'), 'host_id': '123456', } ) async def test_emit_with_callback_without_server(self): standalone_pm = async_pubsub_manager.AsyncPubSubManager() with pytest.raises(RuntimeError): await standalone_pm.emit('foo', 'bar', callback='cb') async def test_emit_with_callback_missing_room(self): with mock.patch.object( self.pm, '_generate_ack_id', return_value='123' ): with pytest.raises(ValueError): await self.pm.emit('foo', 'bar', callback='cb') async def test_emit_with_ignore_queue(self): sid = await self.pm.connect('123', '/') await self.pm.emit( 'foo', 'bar', room=sid, namespace='/', ignore_queue=True ) self.pm._publish.assert_not_awaited() assert self.pm.server._send_eio_packet.await_count == 1 assert self.pm.server._send_eio_packet.await_args_list[0][0][0] \ == '123' pkt = self.pm.server._send_eio_packet.await_args_list[0][0][1] assert pkt.encode() == '42["foo","bar"]' async def test_can_disconnect(self): sid = await self.pm.connect('123', '/') assert await self.pm.can_disconnect(sid, '/') is True await self.pm.can_disconnect(sid, '/foo') self.pm._publish.assert_awaited_once_with( {'method': 'disconnect', 'sid': sid, 'namespace': '/foo', 'host_id': '123456'} ) async def test_disconnect(self): await self.pm.disconnect('foo', '/') self.pm._publish.assert_awaited_once_with( {'method': 'disconnect', 'sid': 'foo', 'namespace': '/', 'host_id': '123456'} ) async def test_disconnect_ignore_queue(self): sid = await self.pm.connect('123', '/') self.pm.pre_disconnect(sid, '/') await self.pm.disconnect(sid, '/', ignore_queue=True) self.pm._publish.assert_not_awaited() assert self.pm.is_connected(sid, '/') is False async def test_enter_room(self): sid = await self.pm.connect('123', '/') await self.pm.enter_room(sid, '/', 'foo') await self.pm.enter_room('456', '/', 'foo') assert sid in self.pm.rooms['/']['foo'] assert self.pm.rooms['/']['foo'][sid] == '123' self.pm._publish.assert_awaited_once_with( {'method': 'enter_room', 'sid': '456', 'room': 'foo', 'namespace': '/', 'host_id': '123456'} ) async def test_leave_room(self): sid = await self.pm.connect('123', '/') await self.pm.leave_room(sid, '/', 'foo') await self.pm.leave_room('456', '/', 'foo') assert 'foo' not in self.pm.rooms['/'] self.pm._publish.assert_awaited_once_with( {'method': 'leave_room', 'sid': '456', 'room': 'foo', 'namespace': '/', 'host_id': '123456'} ) async def test_close_room(self): await self.pm.close_room('foo') self.pm._publish.assert_awaited_once_with( {'method': 'close_room', 'room': 'foo', 'namespace': '/', 'host_id': '123456'} ) async def test_close_room_with_namespace(self): await self.pm.close_room('foo', '/bar') self.pm._publish.assert_awaited_once_with( {'method': 'close_room', 'room': 'foo', 'namespace': '/bar', 'host_id': '123456'} ) async def test_handle_emit(self): with mock.patch.object( async_manager.AsyncManager, 'emit' ) as super_emit: await self.pm._handle_emit({'event': 'foo', 'data': 'bar'}) super_emit.assert_awaited_once_with( 'foo', 'bar', namespace=None, room=None, skip_sid=None, callback=None, ) async def test_handle_emit_with_namespace(self): with mock.patch.object( async_manager.AsyncManager, 'emit' ) as super_emit: await self.pm._handle_emit( {'event': 'foo', 'data': 'bar', 'namespace': '/baz'} ) super_emit.assert_awaited_once_with( 'foo', 'bar', namespace='/baz', room=None, skip_sid=None, callback=None, ) async def test_handle_emit_with_room(self): with mock.patch.object( async_manager.AsyncManager, 'emit' ) as super_emit: await self.pm._handle_emit( {'event': 'foo', 'data': 'bar', 'room': 'baz'} ) super_emit.assert_awaited_once_with( 'foo', 'bar', namespace=None, room='baz', skip_sid=None, callback=None, ) async def test_handle_emit_with_skip_sid(self): with mock.patch.object( async_manager.AsyncManager, 'emit' ) as super_emit: await self.pm._handle_emit( {'event': 'foo', 'data': 'bar', 'skip_sid': '123'} ) super_emit.assert_awaited_once_with( 'foo', 'bar', namespace=None, room=None, skip_sid='123', callback=None, ) async def test_handle_emit_with_remote_callback(self): with mock.patch.object( async_manager.AsyncManager, 'emit' ) as super_emit: await self.pm._handle_emit( { 'event': 'foo', 'data': 'bar', 'namespace': '/baz', 'callback': ('sid', '/baz', 123), 'host_id': 'x', } ) assert super_emit.await_count == 1 assert super_emit.await_args[0] == ('foo', 'bar') assert super_emit.await_args[1]['namespace'] == '/baz' assert super_emit.await_args[1]['room'] is None assert super_emit.await_args[1]['skip_sid'] is None assert isinstance( super_emit.await_args[1]['callback'], functools.partial ) await super_emit.await_args[1]['callback']('one', 2, 'three') self.pm._publish.assert_awaited_once_with( { 'method': 'callback', 'host_id': 'x', 'sid': 'sid', 'namespace': '/baz', 'id': 123, 'args': ('one', 2, 'three'), } ) async def test_handle_emit_with_local_callback(self): with mock.patch.object( async_manager.AsyncManager, 'emit' ) as super_emit: await self.pm._handle_emit( { 'event': 'foo', 'data': 'bar', 'namespace': '/baz', 'callback': ('sid', '/baz', 123), 'host_id': self.pm.host_id, } ) assert super_emit.await_count == 1 assert super_emit.await_args[0] == ('foo', 'bar') assert super_emit.await_args[1]['namespace'] == '/baz' assert super_emit.await_args[1]['room'] is None assert super_emit.await_args[1]['skip_sid'] is None assert isinstance( super_emit.await_args[1]['callback'], functools.partial ) await super_emit.await_args[1]['callback']('one', 2, 'three') self.pm._publish.assert_not_awaited() async def test_handle_callback(self): host_id = self.pm.host_id with mock.patch.object( self.pm, 'trigger_callback' ) as trigger: await self.pm._handle_callback( { 'method': 'callback', 'host_id': host_id, 'sid': 'sid', 'namespace': '/', 'id': 123, 'args': ('one', 2), } ) trigger.assert_awaited_once_with('sid', 123, ('one', 2)) async def test_handle_callback_bad_host_id(self): with mock.patch.object( self.pm, 'trigger_callback' ) as trigger: await self.pm._handle_callback( { 'method': 'callback', 'host_id': 'bad', 'sid': 'sid', 'namespace': '/', 'id': 123, 'args': ('one', 2), } ) assert trigger.await_count == 0 async def test_handle_callback_missing_args(self): host_id = self.pm.host_id with mock.patch.object( self.pm, 'trigger_callback' ) as trigger: await self.pm._handle_callback( { 'method': 'callback', 'host_id': host_id, 'sid': 'sid', 'namespace': '/', 'id': 123, } ) await self.pm._handle_callback( { 'method': 'callback', 'host_id': host_id, 'sid': 'sid', 'namespace': '/', } ) await self.pm._handle_callback( {'method': 'callback', 'host_id': host_id, 'sid': 'sid'} ) await self.pm._handle_callback( {'method': 'callback', 'host_id': host_id} ) assert trigger.await_count == 0 async def test_handle_disconnect(self): await self.pm._handle_disconnect( {'method': 'disconnect', 'sid': '123', 'namespace': '/foo'} ) self.pm.server.disconnect.assert_awaited_once_with( sid='123', namespace='/foo', ignore_queue=True ) async def test_handle_enter_room(self): sid = await self.pm.connect('123', '/') with mock.patch.object( async_manager.AsyncManager, 'enter_room' ) as super_enter_room: await self.pm._handle_enter_room( {'method': 'enter_room', 'sid': sid, 'namespace': '/', 'room': 'foo'} ) await self.pm._handle_enter_room( {'method': 'enter_room', 'sid': '456', 'namespace': '/', 'room': 'foo'} ) super_enter_room.assert_awaited_once_with(sid, '/', 'foo') async def test_handle_leave_room(self): sid = await self.pm.connect('123', '/') with mock.patch.object( async_manager.AsyncManager, 'leave_room' ) as super_leave_room: await self.pm._handle_leave_room( {'method': 'leave_room', 'sid': sid, 'namespace': '/', 'room': 'foo'} ) await self.pm._handle_leave_room( {'method': 'leave_room', 'sid': '456', 'namespace': '/', 'room': 'foo'} ) super_leave_room.assert_awaited_once_with(sid, '/', 'foo') async def test_handle_close_room(self): with mock.patch.object( async_manager.AsyncManager, 'close_room' ) as super_close_room: await self.pm._handle_close_room( {'method': 'close_room', 'room': 'foo'} ) super_close_room.assert_awaited_once_with( room='foo', namespace=None ) async def test_handle_close_room_with_namespace(self): with mock.patch.object( async_manager.AsyncManager, 'close_room' ) as super_close_room: await self.pm._handle_close_room( { 'method': 'close_room', 'room': 'foo', 'namespace': '/bar', } ) super_close_room.assert_awaited_once_with( room='foo', namespace='/bar' ) async def test_background_thread(self): self.pm._handle_emit = mock.AsyncMock() self.pm._handle_callback = mock.AsyncMock() self.pm._handle_disconnect = mock.AsyncMock() self.pm._handle_enter_room = mock.AsyncMock() self.pm._handle_leave_room = mock.AsyncMock() self.pm._handle_close_room = mock.AsyncMock() host_id = self.pm.host_id async def messages(): import pickle yield {'method': 'emit', 'value': 'foo', 'host_id': 'x'} yield {'missing': 'method', 'host_id': 'x'} yield '{"method": "callback", "value": "bar", "host_id": "x"}' yield {'method': 'disconnect', 'sid': '123', 'namespace': '/foo', 'host_id': 'x'} yield {'method': 'bogus', 'host_id': 'x'} yield pickle.dumps({'method': 'close_room', 'value': 'baz', 'host_id': 'x'}) yield {'method': 'enter_room', 'sid': '123', 'namespace': '/foo', 'room': 'room', 'host_id': 'x'} yield {'method': 'leave_room', 'sid': '123', 'namespace': '/foo', 'room': 'room', 'host_id': 'x'} yield 'bad json' yield b'bad pickled' # these should not publish anything on the queue, as they come from # the same host yield {'method': 'emit', 'value': 'foo', 'host_id': host_id} yield {'method': 'callback', 'value': 'bar', 'host_id': host_id} yield {'method': 'disconnect', 'sid': '123', 'namespace': '/foo', 'host_id': host_id} yield pickle.dumps({'method': 'close_room', 'value': 'baz', 'host_id': host_id}) self.pm._listen = messages await self.pm._thread() self.pm._handle_emit.assert_awaited_once_with( {'method': 'emit', 'value': 'foo', 'host_id': 'x'} ) self.pm._handle_callback.assert_any_await( {'method': 'callback', 'value': 'bar', 'host_id': 'x'} ) self.pm._handle_callback.assert_any_await( {'method': 'callback', 'value': 'bar', 'host_id': host_id} ) self.pm._handle_disconnect.assert_awaited_once_with( {'method': 'disconnect', 'sid': '123', 'namespace': '/foo', 'host_id': 'x'} ) self.pm._handle_enter_room.assert_awaited_once_with( {'method': 'enter_room', 'sid': '123', 'namespace': '/foo', 'room': 'room', 'host_id': 'x'} ) self.pm._handle_leave_room.assert_awaited_once_with( {'method': 'leave_room', 'sid': '123', 'namespace': '/foo', 'room': 'room', 'host_id': 'x'} ) self.pm._handle_close_room.assert_awaited_once_with( {'method': 'close_room', 'value': 'baz', 'host_id': 'x'} ) async def test_background_thread_exception(self): self.pm._handle_emit = mock.AsyncMock(side_effect=[ ValueError(), asyncio.CancelledError]) async def messages(): yield {'method': 'emit', 'value': 'foo', 'host_id': 'x'} yield {'method': 'emit', 'value': 'bar', 'host_id': 'x'} self.pm._listen = messages await self.pm._thread() self.pm._handle_emit.assert_any_await( {'method': 'emit', 'value': 'foo', 'host_id': 'x'} ) self.pm._handle_emit.assert_awaited_with( {'method': 'emit', 'value': 'bar', 'host_id': 'x'} ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1734543553.0 python_socketio-5.13.0/tests/async/test_server.py0000664000175000017500000012556714730604301021625 0ustar00miguelmiguelimport asyncio import logging from unittest import mock from engineio import json from engineio import packet as eio_packet import pytest from socketio import async_server from socketio import async_namespace from socketio import exceptions from socketio import namespace from socketio import packet @mock.patch('socketio.server.engineio.AsyncServer', **{ 'return_value.generate_id.side_effect': [str(i) for i in range(1, 10)], 'return_value.send_packet': mock.AsyncMock()}) class TestAsyncServer: def teardown_method(self): # restore JSON encoder, in case a test changed it packet.Packet.json = json def _get_mock_manager(self): mgr = mock.MagicMock() mgr.can_disconnect = mock.AsyncMock() mgr.emit = mock.AsyncMock() mgr.enter_room = mock.AsyncMock() mgr.leave_room = mock.AsyncMock() mgr.close_room = mock.AsyncMock() mgr.trigger_callback = mock.AsyncMock() return mgr async def test_create(self, eio): eio.return_value.handle_request = mock.AsyncMock() mgr = self._get_mock_manager() s = async_server.AsyncServer( client_manager=mgr, async_handlers=True, foo='bar' ) await s.handle_request({}) await s.handle_request({}) eio.assert_called_once_with(**{'foo': 'bar', 'async_handlers': False}) assert s.manager == mgr assert s.eio.on.call_count == 3 assert s.async_handlers async def test_attach(self, eio): s = async_server.AsyncServer() s.attach('app', 'path') eio.return_value.attach.assert_called_once_with('app', 'path') async def test_on_event(self, eio): s = async_server.AsyncServer() @s.on('connect') def foo(): pass def bar(reason): pass s.on('disconnect', bar) s.on('disconnect', bar, namespace='/foo') assert s.handlers['/']['connect'] == foo assert s.handlers['/']['disconnect'] == bar assert s.handlers['/foo']['disconnect'] == bar async def test_emit(self, eio): mgr = self._get_mock_manager() s = async_server.AsyncServer(client_manager=mgr) await s.emit( 'my event', {'foo': 'bar'}, to='room', skip_sid='123', namespace='/foo', callback='cb', ) s.manager.emit.assert_awaited_once_with( 'my event', {'foo': 'bar'}, '/foo', room='room', skip_sid='123', callback='cb', ignore_queue=False, ) await s.emit( 'my event', {'foo': 'bar'}, room='room', skip_sid='123', namespace='/foo', callback='cb', ignore_queue=True, ) s.manager.emit.assert_awaited_with( 'my event', {'foo': 'bar'}, '/foo', room='room', skip_sid='123', callback='cb', ignore_queue=True, ) async def test_emit_default_namespace(self, eio): mgr = self._get_mock_manager() s = async_server.AsyncServer(client_manager=mgr) await s.emit( 'my event', {'foo': 'bar'}, to='room', skip_sid='123', callback='cb', ) s.manager.emit.assert_awaited_once_with( 'my event', {'foo': 'bar'}, '/', room='room', skip_sid='123', callback='cb', ignore_queue=False, ) await s.emit( 'my event', {'foo': 'bar'}, room='room', skip_sid='123', callback='cb', ignore_queue=True, ) s.manager.emit.assert_awaited_with( 'my event', {'foo': 'bar'}, '/', room='room', skip_sid='123', callback='cb', ignore_queue=True, ) async def test_send(self, eio): mgr = self._get_mock_manager() s = async_server.AsyncServer(client_manager=mgr) await s.send( 'foo', to='room', skip_sid='123', namespace='/foo', callback='cb', ) s.manager.emit.assert_awaited_once_with( 'message', 'foo', '/foo', room='room', skip_sid='123', callback='cb', ignore_queue=False, ) await s.send( 'foo', room='room', skip_sid='123', namespace='/foo', callback='cb', ignore_queue=True, ) s.manager.emit.assert_awaited_with( 'message', 'foo', '/foo', room='room', skip_sid='123', callback='cb', ignore_queue=True, ) async def test_call(self, eio): mgr = self._get_mock_manager() s = async_server.AsyncServer(client_manager=mgr) async def fake_event_wait(): s.manager.emit.await_args_list[0][1]['callback']('foo', 321) return True s.eio.create_event.return_value.wait = fake_event_wait assert await s.call('foo', sid='123') == ('foo', 321) async def test_call_with_timeout(self, eio): mgr = self._get_mock_manager() s = async_server.AsyncServer(client_manager=mgr) async def fake_event_wait(): await asyncio.sleep(1) s.eio.create_event.return_value.wait = fake_event_wait with pytest.raises(exceptions.TimeoutError): await s.call('foo', sid='123', timeout=0.01) async def test_call_with_broadcast(self, eio): s = async_server.AsyncServer() with pytest.raises(ValueError): await s.call('foo') async def test_call_without_async_handlers(self, eio): mgr = self._get_mock_manager() s = async_server.AsyncServer( client_manager=mgr, async_handlers=False ) with pytest.raises(RuntimeError): await s.call('foo', sid='123', timeout=12) async def test_enter_room(self, eio): mgr = self._get_mock_manager() s = async_server.AsyncServer(client_manager=mgr) await s.enter_room('123', 'room', namespace='/foo') s.manager.enter_room.assert_awaited_once_with('123', '/foo', 'room') async def test_enter_room_default_namespace(self, eio): mgr = self._get_mock_manager() s = async_server.AsyncServer(client_manager=mgr) await s.enter_room('123', 'room') s.manager.enter_room.assert_awaited_once_with('123', '/', 'room') async def test_leave_room(self, eio): mgr = self._get_mock_manager() s = async_server.AsyncServer(client_manager=mgr) await s.leave_room('123', 'room', namespace='/foo') s.manager.leave_room.assert_awaited_once_with('123', '/foo', 'room') async def test_leave_room_default_namespace(self, eio): mgr = self._get_mock_manager() s = async_server.AsyncServer(client_manager=mgr) await s.leave_room('123', 'room') s.manager.leave_room.assert_awaited_once_with('123', '/', 'room') async def test_close_room(self, eio): mgr = self._get_mock_manager() s = async_server.AsyncServer(client_manager=mgr) await s.close_room('room', namespace='/foo') s.manager.close_room.assert_awaited_once_with('room', '/foo') async def test_close_room_default_namespace(self, eio): mgr = self._get_mock_manager() s = async_server.AsyncServer(client_manager=mgr) await s.close_room('room') s.manager.close_room.assert_awaited_once_with('room', '/') async def test_rooms(self, eio): mgr = self._get_mock_manager() s = async_server.AsyncServer(client_manager=mgr) s.rooms('123', namespace='/foo') s.manager.get_rooms.assert_called_once_with('123', '/foo') async def test_rooms_default_namespace(self, eio): mgr = self._get_mock_manager() s = async_server.AsyncServer(client_manager=mgr) s.rooms('123') s.manager.get_rooms.assert_called_once_with('123', '/') async def test_handle_request(self, eio): eio.return_value.handle_request = mock.AsyncMock() s = async_server.AsyncServer() await s.handle_request('environ') s.eio.handle_request.assert_awaited_once_with('environ') async def test_send_packet(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() await s._send_packet('123', packet.Packet( packet.EVENT, ['my event', 'my data'], namespace='/foo')) s.eio.send.assert_awaited_once_with( '123', '2/foo,["my event","my data"]' ) async def test_send_eio_packet(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() await s._send_eio_packet('123', eio_packet.Packet( eio_packet.MESSAGE, 'hello')) assert s.eio.send_packet.await_count == 1 assert s.eio.send_packet.await_args_list[0][0][0] == '123' pkt = s.eio.send_packet.await_args_list[0][0][1] assert pkt.encode() == '4hello' async def test_transport(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() s.eio.transport = mock.MagicMock(return_value='polling') sid_foo = await s.manager.connect('123', '/foo') assert s.transport(sid_foo, '/foo') == 'polling' s.eio.transport.assert_called_once_with('123') async def test_handle_connect(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() s.manager.initialize = mock.MagicMock() handler = mock.MagicMock() s.on('connect', handler) await s._handle_eio_connect('123', 'environ') await s._handle_eio_message('123', '0') assert s.manager.is_connected('1', '/') handler.assert_called_once_with('1', 'environ') s.eio.send.assert_awaited_once_with('123', '0{"sid":"1"}') assert s.manager.initialize.call_count == 1 await s._handle_eio_connect('456', 'environ') await s._handle_eio_message('456', '0') assert s.manager.initialize.call_count == 1 async def test_handle_connect_with_auth(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() s.manager.initialize = mock.MagicMock() handler = mock.MagicMock() s.on('connect', handler) await s._handle_eio_connect('123', 'environ') await s._handle_eio_message('123', '0{"token":"abc"}') assert s.manager.is_connected('1', '/') handler.assert_called_once_with('1', 'environ', {'token': 'abc'}) s.eio.send.assert_awaited_once_with('123', '0{"sid":"1"}') assert s.manager.initialize.call_count == 1 await s._handle_eio_connect('456', 'environ') await s._handle_eio_message('456', '0') assert s.manager.initialize.call_count == 1 async def test_handle_connect_with_auth_none(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() s.manager.initialize = mock.MagicMock() handler = mock.MagicMock(side_effect=[TypeError, None, None]) s.on('connect', handler) await s._handle_eio_connect('123', 'environ') await s._handle_eio_message('123', '0') assert s.manager.is_connected('1', '/') handler.assert_called_with('1', 'environ', None) s.eio.send.assert_awaited_once_with('123', '0{"sid":"1"}') assert s.manager.initialize.call_count == 1 await s._handle_eio_connect('456', 'environ') await s._handle_eio_message('456', '0') assert s.manager.initialize.call_count == 1 async def test_handle_connect_async(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() s.manager.initialize = mock.MagicMock() handler = mock.AsyncMock() s.on('connect', handler) await s._handle_eio_connect('123', 'environ') await s._handle_eio_message('123', '0') assert s.manager.is_connected('1', '/') handler.assert_awaited_once_with('1', 'environ') s.eio.send.assert_awaited_once_with('123', '0{"sid":"1"}') assert s.manager.initialize.call_count == 1 await s._handle_eio_connect('456', 'environ') await s._handle_eio_message('456', '0') assert s.manager.initialize.call_count == 1 async def test_handle_connect_with_default_implied_namespaces(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() await s._handle_eio_connect('123', 'environ') await s._handle_eio_message('123', '0') await s._handle_eio_message('123', '0/foo,') assert s.manager.is_connected('1', '/') assert not s.manager.is_connected('2', '/foo') async def test_handle_connect_with_implied_namespaces(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer(namespaces=['/foo']) await s._handle_eio_connect('123', 'environ') await s._handle_eio_message('123', '0') await s._handle_eio_message('123', '0/foo,') assert not s.manager.is_connected('1', '/') assert s.manager.is_connected('1', '/foo') async def test_handle_connect_with_all_implied_namespaces(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer(namespaces='*') await s._handle_eio_connect('123', 'environ') await s._handle_eio_message('123', '0') await s._handle_eio_message('123', '0/foo,') assert s.manager.is_connected('1', '/') assert s.manager.is_connected('2', '/foo') async def test_handle_connect_namespace(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() handler = mock.MagicMock() s.on('connect', handler, namespace='/foo') await s._handle_eio_connect('123', 'environ') await s._handle_eio_message('123', '0/foo,') assert s.manager.is_connected('1', '/foo') handler.assert_called_once_with('1', 'environ') s.eio.send.assert_awaited_once_with('123', '0/foo,{"sid":"1"}') async def test_handle_connect_always_connect(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer(always_connect=True) s.manager.initialize = mock.MagicMock() handler = mock.MagicMock() s.on('connect', handler) await s._handle_eio_connect('123', 'environ') await s._handle_eio_message('123', '0') assert s.manager.is_connected('1', '/') handler.assert_called_once_with('1', 'environ') s.eio.send.assert_awaited_once_with('123', '0{"sid":"1"}') assert s.manager.initialize.call_count == 1 await s._handle_eio_connect('456', 'environ') await s._handle_eio_message('456', '0') assert s.manager.initialize.call_count == 1 async def test_handle_connect_rejected(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() handler = mock.MagicMock(return_value=False) s.on('connect', handler) await s._handle_eio_connect('123', 'environ') await s._handle_eio_message('123', '0') assert not s.manager.is_connected('1', '/foo') handler.assert_called_once_with('1', 'environ') s.eio.send.assert_awaited_once_with( '123', '4{"message":"Connection rejected by server"}') assert s.environ == {'123': 'environ'} async def test_handle_connect_namespace_rejected(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() handler = mock.MagicMock(return_value=False) s.on('connect', handler, namespace='/foo') await s._handle_eio_connect('123', 'environ') await s._handle_eio_message('123', '0/foo,') assert not s.manager.is_connected('1', '/foo') handler.assert_called_once_with('1', 'environ') s.eio.send.assert_any_await( '123', '4/foo,{"message":"Connection rejected by server"}') assert s.environ == {'123': 'environ'} async def test_handle_connect_rejected_always_connect(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer(always_connect=True) handler = mock.MagicMock(return_value=False) s.on('connect', handler) await s._handle_eio_connect('123', 'environ') await s._handle_eio_message('123', '0') assert not s.manager.is_connected('1', '/') handler.assert_called_once_with('1', 'environ') s.eio.send.assert_any_await('123', '0{"sid":"1"}') s.eio.send.assert_any_await( '123', '1{"message":"Connection rejected by server"}') assert s.environ == {'123': 'environ'} async def test_handle_connect_namespace_rejected_always_connect(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer(always_connect=True) handler = mock.MagicMock(return_value=False) s.on('connect', handler, namespace='/foo') await s._handle_eio_connect('123', 'environ') await s._handle_eio_message('123', '0/foo,') assert not s.manager.is_connected('1', '/foo') handler.assert_called_once_with('1', 'environ') s.eio.send.assert_any_await('123', '0/foo,{"sid":"1"}') s.eio.send.assert_any_await( '123', '1/foo,{"message":"Connection rejected by server"}') assert s.environ == {'123': 'environ'} async def test_handle_connect_rejected_with_exception(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() handler = mock.MagicMock( side_effect=exceptions.ConnectionRefusedError('fail_reason') ) s.on('connect', handler) await s._handle_eio_connect('123', 'environ') await s._handle_eio_message('123', '0') assert not s.manager.is_connected('1', '/') handler.assert_called_once_with('1', 'environ') s.eio.send.assert_awaited_once_with( '123', '4{"message":"fail_reason"}') assert s.environ == {'123': 'environ'} async def test_handle_connect_rejected_with_empty_exception(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() handler = mock.MagicMock( side_effect=exceptions.ConnectionRefusedError() ) s.on('connect', handler) await s._handle_eio_connect('123', 'environ') await s._handle_eio_message('123', '0') assert not s.manager.is_connected('1', '/') handler.assert_called_once_with('1', 'environ') s.eio.send.assert_awaited_once_with( '123', '4{"message":"Connection rejected by server"}') assert s.environ == {'123': 'environ'} async def test_handle_connect_namespace_rejected_with_exception(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() handler = mock.MagicMock( side_effect=exceptions.ConnectionRefusedError( 'fail_reason', 1, '2') ) s.on('connect', handler, namespace='/foo') await s._handle_eio_connect('123', 'environ') await s._handle_eio_message('123', '0/foo,') assert not s.manager.is_connected('1', '/foo') handler.assert_called_once_with('1', 'environ') s.eio.send.assert_awaited_once_with( '123', '4/foo,{"message":"fail_reason","data":[1,"2"]}') assert s.environ == {'123': 'environ'} async def test_handle_connect_namespace_rejected_with_empty_exception( self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() handler = mock.MagicMock( side_effect=exceptions.ConnectionRefusedError() ) s.on('connect', handler, namespace='/foo') await s._handle_eio_connect('123', 'environ') await s._handle_eio_message('123', '0/foo,') assert not s.manager.is_connected('1', '/foo') handler.assert_called_once_with('1', 'environ') s.eio.send.assert_awaited_once_with( '123', '4/foo,{"message":"Connection rejected by server"}') assert s.environ == {'123': 'environ'} async def test_handle_disconnect(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() s.manager.disconnect = mock.AsyncMock() handler = mock.MagicMock() s.on('disconnect', handler) await s._handle_eio_connect('123', 'environ') await s._handle_eio_message('123', '0') await s._handle_eio_disconnect('123', 'foo') handler.assert_called_once_with('1', 'foo') s.manager.disconnect.assert_awaited_once_with( '1', '/', ignore_queue=True) assert s.environ == {} async def test_handle_legacy_disconnect(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() s.manager.disconnect = mock.AsyncMock() handler = mock.MagicMock(side_effect=[TypeError, None]) s.on('disconnect', handler) await s._handle_eio_connect('123', 'environ') await s._handle_eio_message('123', '0') await s._handle_eio_disconnect('123', 'foo') handler.assert_called_with('1') s.manager.disconnect.assert_awaited_once_with( '1', '/', ignore_queue=True) assert s.environ == {} async def test_handle_legacy_disconnect_async(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() s.manager.disconnect = mock.AsyncMock() handler = mock.AsyncMock(side_effect=[TypeError, None]) s.on('disconnect', handler) await s._handle_eio_connect('123', 'environ') await s._handle_eio_message('123', '0') await s._handle_eio_disconnect('123', 'foo') handler.assert_awaited_with('1') s.manager.disconnect.assert_awaited_once_with( '1', '/', ignore_queue=True) assert s.environ == {} async def test_handle_disconnect_namespace(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() handler = mock.MagicMock() s.on('disconnect', handler) handler_namespace = mock.MagicMock() s.on('disconnect', handler_namespace, namespace='/foo') await s._handle_eio_connect('123', 'environ') await s._handle_eio_message('123', '0/foo,') await s._handle_eio_disconnect('123', 'foo') handler.assert_not_called() handler_namespace.assert_called_once_with('1', 'foo') assert s.environ == {} async def test_handle_disconnect_only_namespace(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() handler = mock.MagicMock() s.on('disconnect', handler) handler_namespace = mock.MagicMock() s.on('disconnect', handler_namespace, namespace='/foo') await s._handle_eio_connect('123', 'environ') await s._handle_eio_message('123', '0/foo,') await s._handle_eio_message('123', '1/foo,') assert handler.call_count == 0 handler_namespace.assert_called_once_with( '1', s.reason.CLIENT_DISCONNECT) assert s.environ == {'123': 'environ'} async def test_handle_disconnect_unknown_client(self, eio): mgr = self._get_mock_manager() s = async_server.AsyncServer(client_manager=mgr) await s._handle_eio_disconnect('123', 'foo') async def test_handle_event(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer(async_handlers=False) sid = await s.manager.connect('123', '/') handler = mock.AsyncMock() catchall_handler = mock.AsyncMock() s.on('msg', handler) s.on('*', catchall_handler) await s._handle_eio_message('123', '2["msg","a","b"]') await s._handle_eio_message('123', '2["my message","a","b","c"]') handler.assert_awaited_once_with(sid, 'a', 'b') catchall_handler.assert_awaited_once_with( 'my message', sid, 'a', 'b', 'c') async def test_handle_event_with_namespace(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer(async_handlers=False) sid = await s.manager.connect('123', '/foo') handler = mock.MagicMock() catchall_handler = mock.MagicMock() s.on('msg', handler, namespace='/foo') s.on('*', catchall_handler, namespace='/foo') await s._handle_eio_message('123', '2/foo,["msg","a","b"]') await s._handle_eio_message('123', '2/foo,["my message","a","b","c"]') handler.assert_called_once_with(sid, 'a', 'b') catchall_handler.assert_called_once_with( 'my message', sid, 'a', 'b', 'c') async def test_handle_event_with_catchall_namespace(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer(async_handlers=False) sid_foo = await s.manager.connect('123', '/foo') sid_bar = await s.manager.connect('123', '/bar') connect_star_handler = mock.MagicMock() msg_foo_handler = mock.MagicMock() msg_star_handler = mock.MagicMock() star_foo_handler = mock.MagicMock() star_star_handler = mock.MagicMock() s.on('connect', connect_star_handler, namespace='*') s.on('msg', msg_foo_handler, namespace='/foo') s.on('msg', msg_star_handler, namespace='*') s.on('*', star_foo_handler, namespace='/foo') s.on('*', star_star_handler, namespace='*') await s._trigger_event('connect', '/bar', sid_bar) await s._handle_eio_message('123', '2/foo,["msg","a","b"]') await s._handle_eio_message('123', '2/bar,["msg","a","b"]') await s._handle_eio_message('123', '2/foo,["my message","a","b","c"]') await s._handle_eio_message('123', '2/bar,["my message","a","b","c"]') await s._trigger_event('disconnect', '/bar', sid_bar, s.reason.CLIENT_DISCONNECT) connect_star_handler.assert_called_once_with('/bar', sid_bar) msg_foo_handler.assert_called_once_with(sid_foo, 'a', 'b') msg_star_handler.assert_called_once_with('/bar', sid_bar, 'a', 'b') star_foo_handler.assert_called_once_with( 'my message', sid_foo, 'a', 'b', 'c') star_star_handler.assert_called_once_with( 'my message', '/bar', sid_bar, 'a', 'b', 'c') async def test_handle_event_with_disconnected_namespace(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer(async_handlers=False) await s.manager.connect('123', '/foo') handler = mock.MagicMock() s.on('my message', handler, namespace='/bar') await s._handle_eio_message('123', '2/bar,["my message","a","b","c"]') handler.assert_not_called() async def test_handle_event_binary(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer(async_handlers=False) sid = await s.manager.connect('123', '/') handler = mock.MagicMock() s.on('my message', handler) await s._handle_eio_message( '123', '52-["my message","a",' '{"_placeholder":true,"num":1},' '{"_placeholder":true,"num":0}]', ) await s._handle_eio_message('123', b'foo') await s._handle_eio_message('123', b'bar') handler.assert_called_once_with(sid, 'a', b'bar', b'foo') async def test_handle_event_binary_ack(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer(async_handlers=False) s.manager.trigger_callback = mock.AsyncMock() sid = await s.manager.connect('123', '/') await s._handle_eio_message( '123', '61-321["my message","a",' '{"_placeholder":true,"num":0}]', ) await s._handle_eio_message('123', b'foo') s.manager.trigger_callback.assert_awaited_once_with( sid, 321, ['my message', 'a', b'foo'] ) async def test_handle_event_with_ack(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer(async_handlers=False) sid = await s.manager.connect('123', '/') handler = mock.MagicMock(return_value='foo') s.on('my message', handler) await s._handle_eio_message('123', '21000["my message","foo"]') handler.assert_called_once_with(sid, 'foo') s.eio.send.assert_awaited_once_with( '123', '31000["foo"]' ) async def test_handle_unknown_event_with_ack(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer(async_handlers=False) await s.manager.connect('123', '/') handler = mock.MagicMock(return_value='foo') s.on('my message', handler) await s._handle_eio_message('123', '21000["another message","foo"]') s.eio.send.assert_not_awaited() async def test_handle_event_with_ack_none(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer(async_handlers=False) sid = await s.manager.connect('123', '/') handler = mock.MagicMock(return_value=None) s.on('my message', handler) await s._handle_eio_message('123', '21000["my message","foo"]') handler.assert_called_once_with(sid, 'foo') s.eio.send.assert_awaited_once_with('123', '31000[]') async def test_handle_event_with_ack_tuple(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer(async_handlers=False) sid = await s.manager.connect('123', '/') handler = mock.MagicMock(return_value=(1, '2', True)) s.on('my message', handler) await s._handle_eio_message('123', '21000["my message","a","b","c"]') handler.assert_called_once_with(sid, 'a', 'b', 'c') s.eio.send.assert_awaited_once_with( '123', '31000[1,"2",true]' ) async def test_handle_event_with_ack_list(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer(async_handlers=False) sid = await s.manager.connect('123', '/') handler = mock.MagicMock(return_value=[1, '2', True]) s.on('my message', handler) await s._handle_eio_message('123', '21000["my message","a","b","c"]') handler.assert_called_once_with(sid, 'a', 'b', 'c') s.eio.send.assert_awaited_once_with( '123', '31000[[1,"2",true]]' ) async def test_handle_event_with_ack_binary(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer(async_handlers=False) sid = await s.manager.connect('123', '/') handler = mock.MagicMock(return_value=b'foo') s.on('my message', handler) await s._handle_eio_message('123', '21000["my message","foo"]') handler.assert_any_call(sid, 'foo') async def test_handle_error_packet(self, eio): s = async_server.AsyncServer() with pytest.raises(ValueError): await s._handle_eio_message('123', '4') async def test_handle_invalid_packet(self, eio): s = async_server.AsyncServer() with pytest.raises(ValueError): await s._handle_eio_message('123', '9') async def test_send_with_ack(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() s.handlers['/'] = {} await s._handle_eio_connect('123', 'environ') await s._handle_eio_message('123', '0') cb = mock.MagicMock() id1 = s.manager._generate_ack_id('1', cb) id2 = s.manager._generate_ack_id('1', cb) await s._send_packet('123', packet.Packet( packet.EVENT, ['my event', 'foo'], id=id1)) await s._send_packet('123', packet.Packet( packet.EVENT, ['my event', 'bar'], id=id2)) await s._handle_eio_message('123', '31["foo",2]') cb.assert_called_once_with('foo', 2) async def test_send_with_ack_namespace(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() s.handlers['/foo'] = {} await s._handle_eio_connect('123', 'environ') await s._handle_eio_message('123', '0/foo,') cb = mock.MagicMock() id = s.manager._generate_ack_id('1', cb) await s._send_packet( '123', packet.Packet(packet.EVENT, ['my event', 'foo'], namespace='/foo', id=id) ) await s._handle_eio_message('123', '3/foo,1["foo",2]') cb.assert_called_once_with('foo', 2) async def test_session(self, eio): fake_session = {} async def fake_get_session(eio_sid): assert eio_sid == '123' return fake_session async def fake_save_session(eio_sid, session): global fake_session assert eio_sid == '123' fake_session = session eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() s.handlers['/'] = {} s.handlers['/ns'] = {} s.eio.get_session = fake_get_session s.eio.save_session = fake_save_session async def _test(): await s._handle_eio_connect('123', 'environ') await s._handle_eio_message('123', '0') await s._handle_eio_message('123', '0/ns') sid = s.manager.sid_from_eio_sid('123', '/') sid2 = s.manager.sid_from_eio_sid('123', '/ns') await s.save_session(sid, {'foo': 'bar'}) async with s.session(sid) as session: assert session == {'foo': 'bar'} session['foo'] = 'baz' session['bar'] = 'foo' assert await s.get_session(sid) == {'foo': 'baz', 'bar': 'foo'} assert fake_session == {'/': {'foo': 'baz', 'bar': 'foo'}} async with s.session(sid2, namespace='/ns') as session: assert session == {} session['a'] = 'b' assert await s.get_session(sid2, namespace='/ns') == {'a': 'b'} assert fake_session == { '/': {'foo': 'baz', 'bar': 'foo'}, '/ns': {'a': 'b'}, } await _test() async def test_disconnect(self, eio): eio.return_value.send = mock.AsyncMock() eio.return_value.disconnect = mock.AsyncMock() s = async_server.AsyncServer() s.handlers['/'] = {} await s._handle_eio_connect('123', 'environ') await s._handle_eio_message('123', '0') await s.disconnect('1') s.eio.send.assert_any_await('123', '1') assert not s.manager.is_connected('1', '/') async def test_disconnect_ignore_queue(self, eio): eio.return_value.send = mock.AsyncMock() eio.return_value.disconnect = mock.AsyncMock() s = async_server.AsyncServer() s.handlers['/'] = {} await s._handle_eio_connect('123', 'environ') await s._handle_eio_message('123', '0') await s.disconnect('1', ignore_queue=True) s.eio.send.assert_any_await('123', '1') assert not s.manager.is_connected('1', '/') async def test_disconnect_namespace(self, eio): eio.return_value.send = mock.AsyncMock() eio.return_value.disconnect = mock.AsyncMock() s = async_server.AsyncServer() s.handlers['/foo'] = {} await s._handle_eio_connect('123', 'environ') await s._handle_eio_message('123', '0/foo,') await s.disconnect('1', namespace='/foo') s.eio.send.assert_any_await('123', '1/foo,') assert not s.manager.is_connected('1', '/foo') async def test_disconnect_twice(self, eio): eio.return_value.send = mock.AsyncMock() eio.return_value.disconnect = mock.AsyncMock() s = async_server.AsyncServer() await s._handle_eio_connect('123', 'environ') await s._handle_eio_message('123', '0') await s.disconnect('1') calls = s.eio.send.await_count assert not s.manager.is_connected('1', '/') await s.disconnect('1') assert calls == s.eio.send.await_count async def test_disconnect_twice_namespace(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() await s._handle_eio_connect('123', 'environ') await s._handle_eio_message('123', '0/foo,') await s.disconnect('1', namespace='/foo') calls = s.eio.send.await_count assert not s.manager.is_connected('1', '/foo') await s.disconnect('1', namespace='/foo') assert calls == s.eio.send.await_count async def test_namespace_handler(self, eio): eio.return_value.send = mock.AsyncMock() result = {} class MyNamespace(async_namespace.AsyncNamespace): def on_connect(self, sid, environ): result['result'] = (sid, environ) async def on_disconnect(self, sid, reason): result['result'] = ('disconnect', sid, reason) async def on_foo(self, sid, data): result['result'] = (sid, data) def on_bar(self, sid): result['result'] = 'bar' async def on_baz(self, sid, data1, data2): result['result'] = (data1, data2) s = async_server.AsyncServer(async_handlers=False) s.register_namespace(MyNamespace('/foo')) await s._handle_eio_connect('123', 'environ') await s._handle_eio_message('123', '0/foo,') assert result['result'] == ('1', 'environ') await s._handle_eio_message('123', '2/foo,["foo","a"]') assert result['result'] == ('1', 'a') await s._handle_eio_message('123', '2/foo,["bar"]') assert result['result'] == 'bar' await s._handle_eio_message('123', '2/foo,["baz","a","b"]') assert result['result'] == ('a', 'b') await s.disconnect('1', '/foo') assert result['result'] == ('disconnect', '1', s.reason.SERVER_DISCONNECT) async def test_catchall_namespace_handler(self, eio): eio.return_value.send = mock.AsyncMock() result = {} class MyNamespace(async_namespace.AsyncNamespace): def on_connect(self, ns, sid, environ): result['result'] = (sid, ns, environ) async def on_disconnect(self, ns, sid): result['result'] = ('disconnect', sid, ns) async def on_foo(self, ns, sid, data): result['result'] = (sid, ns, data) def on_bar(self, ns, sid): result['result'] = 'bar' + ns async def on_baz(self, ns, sid, data1, data2): result['result'] = (ns, data1, data2) s = async_server.AsyncServer(async_handlers=False, namespaces='*') s.register_namespace(MyNamespace('*')) await s._handle_eio_connect('123', 'environ') await s._handle_eio_message('123', '0/foo,') assert result['result'] == ('1', '/foo', 'environ') await s._handle_eio_message('123', '2/foo,["foo","a"]') assert result['result'] == ('1', '/foo', 'a') await s._handle_eio_message('123', '2/foo,["bar"]') assert result['result'] == 'bar/foo' await s._handle_eio_message('123', '2/foo,["baz","a","b"]') assert result['result'] == ('/foo', 'a', 'b') await s.disconnect('1', '/foo') assert result['result'] == ('disconnect', '1', '/foo') async def test_bad_namespace_handler(self, eio): class Dummy: pass class SyncNS(namespace.Namespace): pass s = async_server.AsyncServer() with pytest.raises(ValueError): s.register_namespace(123) with pytest.raises(ValueError): s.register_namespace(Dummy) with pytest.raises(ValueError): s.register_namespace(Dummy()) with pytest.raises(ValueError): s.register_namespace(namespace.Namespace) with pytest.raises(ValueError): s.register_namespace(SyncNS()) async def test_logger(self, eio): s = async_server.AsyncServer(logger=False) assert s.logger.getEffectiveLevel() == logging.ERROR s.logger.setLevel(logging.NOTSET) s = async_server.AsyncServer(logger=True) assert s.logger.getEffectiveLevel() == logging.INFO s.logger.setLevel(logging.WARNING) s = async_server.AsyncServer(logger=True) assert s.logger.getEffectiveLevel() == logging.WARNING s.logger.setLevel(logging.NOTSET) s = async_server.AsyncServer(logger='foo') assert s.logger == 'foo' async def test_engineio_logger(self, eio): async_server.AsyncServer(engineio_logger='foo') eio.assert_called_once_with( **{'logger': 'foo', 'async_handlers': False} ) async def test_custom_json(self, eio): # Warning: this test cannot run in parallel with other tests, as it # changes the JSON encoding/decoding functions class CustomJSON: @staticmethod def dumps(*args, **kwargs): return '*** encoded ***' @staticmethod def loads(*args, **kwargs): return '+++ decoded +++' async_server.AsyncServer(json=CustomJSON) eio.assert_called_once_with( **{'json': CustomJSON, 'async_handlers': False} ) pkt = packet.Packet( packet_type=packet.EVENT, data={'foo': 'bar'}, ) assert pkt.encode() == '2*** encoded ***' pkt2 = packet.Packet(encoded_packet=pkt.encode()) assert pkt2.data == '+++ decoded +++' # restore the default JSON module packet.Packet.json = json async def test_async_handlers(self, eio): s = async_server.AsyncServer(async_handlers=True) await s.manager.connect('123', '/') await s._handle_eio_message('123', '2["my message","a","b","c"]') s.eio.start_background_task.assert_called_once_with( s._handle_event_internal, s, '1', '123', ['my message', 'a', 'b', 'c'], '/', None, ) async def test_shutdown(self, eio): s = async_server.AsyncServer() s.eio.shutdown = mock.AsyncMock() await s.shutdown() s.eio.shutdown.assert_awaited_once_with() async def test_start_background_task(self, eio): s = async_server.AsyncServer() s.start_background_task('foo', 'bar', baz='baz') s.eio.start_background_task.assert_called_once_with( 'foo', 'bar', baz='baz' ) async def test_sleep(self, eio): eio.return_value.sleep = mock.AsyncMock() s = async_server.AsyncServer() await s.sleep(1.23) s.eio.sleep.assert_awaited_once_with(1.23) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1738799821.0 python_socketio-5.13.0/tests/async/test_simple_client.py0000664000175000017500000001606414750775315023155 0ustar00miguelmiguelimport asyncio from unittest import mock import pytest from socketio import AsyncSimpleClient from socketio.exceptions import SocketIOError, TimeoutError, DisconnectedError class TestAsyncAsyncSimpleClient: async def test_constructor(self): client = AsyncSimpleClient(1, '2', a='3', b=4) assert client.client_args == (1, '2') assert client.client_kwargs == {'a': '3', 'b': 4} assert client.client is None assert client.input_buffer == [] assert not client.connected async def test_connect(self): mock_client = mock.MagicMock() original_client_class = AsyncSimpleClient.client_class AsyncSimpleClient.client_class = mock_client client = AsyncSimpleClient(123, a='b') mock_client.return_value.connect = mock.AsyncMock() await client.connect('url', headers='h', auth='a', transports='t', namespace='n', socketio_path='s', wait_timeout='w') mock_client.assert_called_once_with(123, a='b') assert client.client == mock_client() mock_client().connect.assert_awaited_once_with( 'url', headers='h', auth='a', transports='t', namespaces=['n'], socketio_path='s', wait_timeout='w') mock_client().event.call_count == 3 mock_client().on.assert_called_once_with('*', namespace='n') assert client.namespace == 'n' assert not client.input_event.is_set() AsyncSimpleClient.client_class = original_client_class async def test_connect_context_manager(self): mock_client = mock.MagicMock() original_client_class = AsyncSimpleClient.client_class AsyncSimpleClient.client_class = mock_client async with AsyncSimpleClient(123, a='b') as client: mock_client.return_value.connect = mock.AsyncMock() await client.connect('url', headers='h', auth='a', transports='t', namespace='n', socketio_path='s', wait_timeout='w') mock_client.assert_called_once_with(123, a='b') assert client.client == mock_client() mock_client().connect.assert_awaited_once_with( 'url', headers='h', auth='a', transports='t', namespaces=['n'], socketio_path='s', wait_timeout='w') mock_client().event.call_count == 3 mock_client().on.assert_called_once_with( '*', namespace='n') assert client.namespace == 'n' assert not client.input_event.is_set() AsyncSimpleClient.client_class = original_client_class async def test_connect_twice(self): client = AsyncSimpleClient(123, a='b') client.client = mock.MagicMock() client.connected = True with pytest.raises(RuntimeError): await client.connect('url') async def test_properties(self): client = AsyncSimpleClient() client.client = mock.MagicMock(transport='websocket') client.client.get_sid.return_value = 'sid' client.connected_event.set() client.connected = True assert client.sid == 'sid' assert client.transport == 'websocket' async def test_emit(self): client = AsyncSimpleClient() client.client = mock.MagicMock() client.client.emit = mock.AsyncMock() client.namespace = '/ns' client.connected_event.set() client.connected = True await client.emit('foo', 'bar') client.client.emit.assert_awaited_once_with('foo', 'bar', namespace='/ns') async def test_emit_disconnected(self): client = AsyncSimpleClient() client.connected_event.set() client.connected = False with pytest.raises(DisconnectedError): await client.emit('foo', 'bar') async def test_emit_retries(self): client = AsyncSimpleClient() client.connected_event.set() client.connected = True client.client = mock.MagicMock() client.client.emit = mock.AsyncMock() client.client.emit.side_effect = [SocketIOError(), None] await client.emit('foo', 'bar') client.client.emit.assert_awaited_with('foo', 'bar', namespace='/') async def test_call(self): client = AsyncSimpleClient() client.client = mock.MagicMock() client.client.call = mock.AsyncMock() client.client.call.return_value = 'result' client.namespace = '/ns' client.connected_event.set() client.connected = True assert await client.call('foo', 'bar') == 'result' client.client.call.assert_awaited_once_with( 'foo', 'bar', namespace='/ns', timeout=60) async def test_call_disconnected(self): client = AsyncSimpleClient() client.connected_event.set() client.connected = False with pytest.raises(DisconnectedError): await client.call('foo', 'bar') async def test_call_retries(self): client = AsyncSimpleClient() client.connected_event.set() client.connected = True client.client = mock.MagicMock() client.client.call = mock.AsyncMock() client.client.call.side_effect = [SocketIOError(), 'result'] assert await client.call('foo', 'bar') == 'result' client.client.call.assert_awaited_with('foo', 'bar', namespace='/', timeout=60) async def test_receive_with_input_buffer(self): client = AsyncSimpleClient() client.input_buffer = ['foo', 'bar'] assert await client.receive() == 'foo' assert await client.receive() == 'bar' async def test_receive_without_input_buffer(self): client = AsyncSimpleClient() client.connected_event.set() client.connected = True client.input_event = mock.MagicMock() async def fake_wait(timeout=None): client.input_buffer = ['foo'] return True client.input_event.wait = fake_wait assert await client.receive() == 'foo' async def test_receive_with_timeout(self): client = AsyncSimpleClient() client.connected_event.set() client.connected = True client.input_event = mock.MagicMock() async def fake_wait(timeout=None): await asyncio.sleep(1) client.input_event.wait = fake_wait with pytest.raises(TimeoutError): await client.receive(timeout=0.01) async def test_receive_disconnected(self): client = AsyncSimpleClient() client.connected_event.set() client.connected = False with pytest.raises(DisconnectedError): await client.receive() async def test_disconnect(self): client = AsyncSimpleClient() mc = mock.MagicMock() mc.disconnect = mock.AsyncMock() client.client = mc client.connected = True await client.disconnect() await client.disconnect() mc.disconnect.assert_awaited_once_with() assert client.client is None ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704499102.0 python_socketio-5.13.0/tests/asyncio_web_server.py0000644000175000017500000000336114546113636022027 0ustar00miguelmiguelimport requests import threading import time import uvicorn import socketio class SocketIOWebServer: """A simple web server used for running Socket.IO servers in tests. :param sio: a Socket.IO server instance. Note 1: This class is not production-ready and is intended for testing. Note 2: This class only supports the "asgi" async_mode. """ def __init__(self, sio, on_shutdown=None): if sio.async_mode != 'asgi': raise ValueError('The async_mode must be "asgi"') async def http_app(scope, receive, send): await send({'type': 'http.response.start', 'status': 200, 'headers': [('Content-Type', 'text/plain')]}) await send({'type': 'http.response.body', 'body': b'OK'}) self.sio = sio self.app = socketio.ASGIApp(sio, http_app, on_shutdown=on_shutdown) self.httpd = None self.thread = None def start(self, port=8900): """Start the web server. :param port: the port to listen on. Defaults to 8900. The server is started in a background thread. """ self.httpd = uvicorn.Server(config=uvicorn.Config(self.app, port=port)) self.thread = threading.Thread(target=self.httpd.run) self.thread.start() # wait for the server to start while True: try: r = requests.get(f'http://localhost:{port}/') r.raise_for_status() if r.text == 'OK': break except: time.sleep(0.1) def stop(self): """Stop the web server.""" self.httpd.should_exit = True self.thread.join() self.thread = None ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1744472442.059087 python_socketio-5.13.0/tests/common/0000775000175000017500000000000014776504572017064 5ustar00miguelmiguel././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704499081.0 python_socketio-5.13.0/tests/common/__init__.py0000644000175000017500000000000014546113611021142 0ustar00miguelmiguel././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1734189874.0 python_socketio-5.13.0/tests/common/test_admin.py0000664000175000017500000002772514727321462021571 0ustar00miguelmiguelfrom functools import wraps import threading import time from unittest import mock import pytest from engineio.socket import Socket as EngineIOSocket import socketio from socketio.exceptions import ConnectionError from tests.web_server import SocketIOWebServer def with_instrumented_server(auth=False, **ikwargs): """This decorator can be applied to test functions or methods so that they run with a Socket.IO server that has been instrumented for the official Admin UI project. The arguments passed to the decorator are passed directly to the ``instrument()`` method of the server. """ def decorator(f): @wraps(f) def wrapped(self, *args, **kwargs): sio = socketio.Server(async_mode='threading') @sio.event def enter_room(sid, data): sio.enter_room(sid, data) @sio.event def emit(sid, event): sio.emit(event, skip_sid=sid) @sio.event(namespace='/foo') def connect(sid, environ, auth): pass if 'server_stats_interval' not in ikwargs: ikwargs['server_stats_interval'] = 0.25 self.isvr = sio.instrument(auth=auth, **ikwargs) server = SocketIOWebServer(sio) server.start() # import logging # logging.getLogger('engineio.client').setLevel(logging.DEBUG) # logging.getLogger('socketio.client').setLevel(logging.DEBUG) original_schedule_ping = EngineIOSocket.schedule_ping EngineIOSocket.schedule_ping = mock.MagicMock() try: ret = f(self, *args, **kwargs) finally: server.stop() self.isvr.shutdown() self.isvr.uninstrument() self.isvr = None EngineIOSocket.schedule_ping = original_schedule_ping # import logging # logging.getLogger('engineio.client').setLevel(logging.NOTSET) # logging.getLogger('socketio.client').setLevel(logging.NOTSET) return ret return wrapped return decorator def _custom_auth(auth): return auth == {'foo': 'bar'} class TestAdmin: def setup_method(self): print('threads at start:', threading.enumerate()) self.thread_count = threading.active_count() def teardown_method(self): print('threads at end:', threading.enumerate()) assert self.thread_count == threading.active_count() def _expect(self, expected, admin_client): events = {} while expected: data = admin_client.receive(timeout=5) if data[0] in expected: if expected[data[0]] == 1: events[data[0]] = data[1] del expected[data[0]] else: expected[data[0]] -= 1 return events def test_missing_auth(self): sio = socketio.Server(async_mode='threading') with pytest.raises(ValueError): sio.instrument() @with_instrumented_server(auth=False) def test_admin_connect_with_no_auth(self): with socketio.SimpleClient() as admin_client: admin_client.connect('http://localhost:8900', namespace='/admin') with socketio.SimpleClient() as admin_client: admin_client.connect('http://localhost:8900', namespace='/admin', auth={'foo': 'bar'}) @with_instrumented_server(auth={'foo': 'bar'}) def test_admin_connect_with_dict_auth(self): with socketio.SimpleClient() as admin_client: admin_client.connect('http://localhost:8900', namespace='/admin', auth={'foo': 'bar'}) with socketio.SimpleClient() as admin_client: with pytest.raises(ConnectionError): admin_client.connect( 'http://localhost:8900', namespace='/admin', auth={'foo': 'baz'}) with socketio.SimpleClient() as admin_client: with pytest.raises(ConnectionError): admin_client.connect( 'http://localhost:8900', namespace='/admin') @with_instrumented_server(auth=[{'foo': 'bar'}, {'u': 'admin', 'p': 'secret'}]) def test_admin_connect_with_list_auth(self): with socketio.SimpleClient() as admin_client: admin_client.connect('http://localhost:8900', namespace='/admin', auth={'foo': 'bar'}) with socketio.SimpleClient() as admin_client: admin_client.connect('http://localhost:8900', namespace='/admin', auth={'u': 'admin', 'p': 'secret'}) with socketio.SimpleClient() as admin_client: with pytest.raises(ConnectionError): admin_client.connect('http://localhost:8900', namespace='/admin', auth={'foo': 'baz'}) with socketio.SimpleClient() as admin_client: with pytest.raises(ConnectionError): admin_client.connect('http://localhost:8900', namespace='/admin') @with_instrumented_server(auth=_custom_auth) def test_admin_connect_with_function_auth(self): with socketio.SimpleClient() as admin_client: admin_client.connect('http://localhost:8900', namespace='/admin', auth={'foo': 'bar'}) with socketio.SimpleClient() as admin_client: with pytest.raises(ConnectionError): admin_client.connect('http://localhost:8900', namespace='/admin', auth={'foo': 'baz'}) with socketio.SimpleClient() as admin_client: with pytest.raises(ConnectionError): admin_client.connect('http://localhost:8900', namespace='/admin') @with_instrumented_server() def test_admin_connect_only_admin(self): with socketio.SimpleClient() as admin_client: admin_client.connect('http://localhost:8900', namespace='/admin') sid = admin_client.sid events = self._expect({'config': 1, 'all_sockets': 1, 'server_stats': 2}, admin_client) assert 'supportedFeatures' in events['config'] assert 'ALL_EVENTS' in events['config']['supportedFeatures'] assert 'AGGREGATED_EVENTS' in events['config']['supportedFeatures'] assert 'EMIT' in events['config']['supportedFeatures'] assert len(events['all_sockets']) == 1 assert events['all_sockets'][0]['id'] == sid assert events['all_sockets'][0]['rooms'] == [sid] assert events['server_stats']['clientsCount'] == 1 assert events['server_stats']['pollingClientsCount'] == 0 assert len(events['server_stats']['namespaces']) == 3 assert {'name': '/', 'socketsCount': 0} in \ events['server_stats']['namespaces'] assert {'name': '/foo', 'socketsCount': 0} in \ events['server_stats']['namespaces'] assert {'name': '/admin', 'socketsCount': 1} in \ events['server_stats']['namespaces'] @with_instrumented_server() def test_admin_connect_with_others(self): with socketio.SimpleClient() as client1, \ socketio.SimpleClient() as client2, \ socketio.SimpleClient() as client3, \ socketio.SimpleClient() as admin_client: client1.connect('http://localhost:8900') client1.emit('enter_room', 'room') sid1 = client1.sid saved_check_for_upgrade = self.isvr._check_for_upgrade self.isvr._check_for_upgrade = mock.MagicMock() client2.connect('http://localhost:8900', namespace='/foo', transports=['polling']) sid2 = client2.sid self.isvr._check_for_upgrade = saved_check_for_upgrade client3.connect('http://localhost:8900', namespace='/admin') sid3 = client3.sid admin_client.connect('http://localhost:8900', namespace='/admin') sid = admin_client.sid events = self._expect({'config': 1, 'all_sockets': 1, 'server_stats': 2}, admin_client) assert 'supportedFeatures' in events['config'] assert 'ALL_EVENTS' in events['config']['supportedFeatures'] assert 'AGGREGATED_EVENTS' in events['config']['supportedFeatures'] assert 'EMIT' in events['config']['supportedFeatures'] assert len(events['all_sockets']) == 4 assert events['server_stats']['clientsCount'] == 4 assert events['server_stats']['pollingClientsCount'] == 1 assert len(events['server_stats']['namespaces']) == 3 assert {'name': '/', 'socketsCount': 1} in \ events['server_stats']['namespaces'] assert {'name': '/foo', 'socketsCount': 1} in \ events['server_stats']['namespaces'] assert {'name': '/admin', 'socketsCount': 2} in \ events['server_stats']['namespaces'] for socket in events['all_sockets']: if socket['id'] == sid: assert socket['rooms'] == [sid] elif socket['id'] == sid1: assert socket['rooms'] == [sid1, 'room'] elif socket['id'] == sid2: assert socket['rooms'] == [sid2] elif socket['id'] == sid3: assert socket['rooms'] == [sid3] @with_instrumented_server(mode='production', read_only=True) def test_admin_connect_production(self): with socketio.SimpleClient() as admin_client: admin_client.connect('http://localhost:8900', namespace='/admin') events = self._expect({'config': 1, 'server_stats': 2}, admin_client) assert 'supportedFeatures' in events['config'] assert 'ALL_EVENTS' not in events['config']['supportedFeatures'] assert 'AGGREGATED_EVENTS' in events['config']['supportedFeatures'] assert 'EMIT' not in events['config']['supportedFeatures'] assert events['server_stats']['clientsCount'] == 1 assert events['server_stats']['pollingClientsCount'] == 0 assert len(events['server_stats']['namespaces']) == 3 assert {'name': '/', 'socketsCount': 0} in \ events['server_stats']['namespaces'] assert {'name': '/foo', 'socketsCount': 0} in \ events['server_stats']['namespaces'] assert {'name': '/admin', 'socketsCount': 1} in \ events['server_stats']['namespaces'] @with_instrumented_server() def test_admin_features(self): with socketio.SimpleClient() as client1, \ socketio.SimpleClient() as client2, \ socketio.SimpleClient() as admin_client: client1.connect('http://localhost:8900') client2.connect('http://localhost:8900') admin_client.connect('http://localhost:8900', namespace='/admin') # emit from admin admin_client.emit( 'emit', ('/', client1.sid, 'foo', {'bar': 'baz'}, 'extra')) data = client1.receive(timeout=5) assert data == ['foo', {'bar': 'baz'}, 'extra'] # emit from regular client client1.emit('emit', 'foo') data = client2.receive(timeout=5) assert data == ['foo'] # join and leave admin_client.emit('join', ('/', 'room', client1.sid)) time.sleep(0.2) admin_client.emit( 'emit', ('/', 'room', 'foo', {'bar': 'baz'})) data = client1.receive(timeout=5) assert data == ['foo', {'bar': 'baz'}] admin_client.emit('leave', ('/', 'room')) # disconnect admin_client.emit('_disconnect', ('/', False, client1.sid)) for _ in range(10): if not client1.connected: break time.sleep(0.2) assert not client1.connected ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741531549.0 python_socketio-5.13.0/tests/common/test_client.py0000664000175000017500000013640214763324635021755 0ustar00miguelmiguelimport logging import time from unittest import mock from engineio import exceptions as engineio_exceptions from engineio import json from engineio import packet as engineio_packet import pytest from socketio import async_namespace from socketio import client from socketio import exceptions from socketio import msgpack_packet from socketio import namespace from socketio import packet class TestClient: def test_is_asyncio_based(self): c = client.Client() assert not c.is_asyncio_based() @mock.patch('socketio.client.Client._engineio_client_class') def test_create(self, engineio_client_class): c = client.Client( reconnection=False, reconnection_attempts=123, reconnection_delay=5, reconnection_delay_max=10, randomization_factor=0.2, handle_sigint=False, foo='bar', ) assert not c.reconnection assert c.reconnection_attempts == 123 assert c.reconnection_delay == 5 assert c.reconnection_delay_max == 10 assert c.randomization_factor == 0.2 assert not c.handle_sigint engineio_client_class().assert_called_once_with( foo='bar', handle_sigint=False) assert c.connection_url is None assert c.connection_headers is None assert c.connection_transports is None assert c.connection_namespaces == [] assert c.socketio_path is None assert c.sid is None assert c.namespaces == {} assert c.handlers == {} assert c.namespace_handlers == {} assert c.callbacks == {} assert c._binary_packet is None assert c._reconnect_task is None assert c.packet_class == packet.Packet def test_msgpack(self): c = client.Client(serializer='msgpack') assert c.packet_class == msgpack_packet.MsgPackPacket def test_custom_serializer(self): class CustomPacket(packet.Packet): pass c = client.Client(serializer=CustomPacket) assert c.packet_class == CustomPacket def test_custom_json(self): client.Client() assert packet.Packet.json == json assert engineio_packet.Packet.json == json client.Client(json='foo') assert packet.Packet.json == 'foo' assert engineio_packet.Packet.json == 'foo' packet.Packet.json = json def test_logger(self): c = client.Client(logger=False) assert c.logger.getEffectiveLevel() == logging.ERROR c.logger.setLevel(logging.NOTSET) c = client.Client(logger=True) assert c.logger.getEffectiveLevel() == logging.INFO c.logger.setLevel(logging.WARNING) c = client.Client(logger=True) assert c.logger.getEffectiveLevel() == logging.WARNING c.logger.setLevel(logging.NOTSET) my_logger = logging.Logger('foo') c = client.Client(logger=my_logger) assert c.logger == my_logger @mock.patch('socketio.client.Client._engineio_client_class') def test_engineio_logger(self, engineio_client_class): client.Client(engineio_logger='foo') engineio_client_class().assert_called_once_with( handle_sigint=True, logger='foo') def test_on_event(self): c = client.Client() @c.on('connect') def foo(): pass def bar(): pass c.on('disconnect', bar) c.on('disconnect', bar, namespace='/foo') assert c.handlers['/']['connect'] == foo assert c.handlers['/']['disconnect'] == bar assert c.handlers['/foo']['disconnect'] == bar def test_event(self): c = client.Client() @c.event def connect(): pass @c.event def foo(): pass @c.event def bar(): pass @c.event(namespace='/foo') def disconnect(): pass assert c.handlers['/']['connect'] == connect assert c.handlers['/']['foo'] == foo assert c.handlers['/']['bar'] == bar assert c.handlers['/foo']['disconnect'] == disconnect def test_namespace_handler(self): class MyNamespace(namespace.ClientNamespace): pass c = client.Client() n = MyNamespace('/foo') c.register_namespace(n) assert c.namespace_handlers['/foo'] == n def test_namespace_handler_wrong_class(self): class MyNamespace: def __init__(self, n): pass c = client.Client() n = MyNamespace('/foo') with pytest.raises(ValueError): c.register_namespace(n) def test_namespace_handler_wrong_async(self): class MyNamespace(async_namespace.AsyncClientNamespace): pass c = client.Client() n = MyNamespace('/foo') with pytest.raises(ValueError): c.register_namespace(n) def test_connect(self): c = client.Client() c.eio.connect = mock.MagicMock() c.connect( 'url', headers='headers', auth='auth', transports='transports', namespaces=['/foo', '/', '/bar'], socketio_path='path', wait=False, ) assert c.connection_url == 'url' assert c.connection_headers == 'headers' assert c.connection_auth == 'auth' assert c.connection_transports == 'transports' assert c.connection_namespaces == ['/foo', '/', '/bar'] assert c.socketio_path == 'path' c.eio.connect.assert_called_once_with( 'url', headers='headers', transports='transports', engineio_path='path', ) def test_connect_functions(self): c = client.Client() c.eio.connect = mock.MagicMock() c.connect( lambda: 'url', headers=lambda: 'headers', auth='auth', transports='transports', namespaces=['/foo', '/', '/bar'], socketio_path='path', wait=False, ) c.eio.connect.assert_called_once_with( 'url', headers='headers', transports='transports', engineio_path='path', ) def test_connect_one_namespace(self): c = client.Client() c.eio.connect = mock.MagicMock() c.connect( 'url', headers='headers', transports='transports', namespaces='/foo', socketio_path='path', wait=False, ) assert c.connection_url == 'url' assert c.connection_headers == 'headers' assert c.connection_transports == 'transports' assert c.connection_namespaces == ['/foo'] assert c.socketio_path == 'path' c.eio.connect.assert_called_once_with( 'url', headers='headers', transports='transports', engineio_path='path', ) def test_connect_default_namespaces(self): c = client.Client() c.eio.connect = mock.MagicMock() c.on('foo', mock.MagicMock(), namespace='/foo') c.on('bar', mock.MagicMock(), namespace='/') c.on('baz', mock.MagicMock(), namespace='*') c.connect( 'url', headers='headers', transports='transports', socketio_path='path', wait=False, ) assert c.connection_url == 'url' assert c.connection_headers == 'headers' assert c.connection_transports == 'transports' assert c.connection_namespaces == ['/foo', '/'] or \ c.connection_namespaces == ['/', '/foo'] assert c.socketio_path == 'path' c.eio.connect.assert_called_once_with( 'url', headers='headers', transports='transports', engineio_path='path', ) def test_connect_no_namespaces(self): c = client.Client() c.eio.connect = mock.MagicMock() c.connect( 'url', headers='headers', transports='transports', socketio_path='path', wait=False, ) assert c.connection_url == 'url' assert c.connection_headers == 'headers' assert c.connection_transports == 'transports' assert c.connection_namespaces == ['/'] assert c.socketio_path == 'path' c.eio.connect.assert_called_once_with( 'url', headers='headers', transports='transports', engineio_path='path', ) def test_connect_error(self): c = client.Client() c.eio.connect = mock.MagicMock( side_effect=engineio_exceptions.ConnectionError('foo') ) c.on('foo', mock.MagicMock(), namespace='/foo') c.on('bar', mock.MagicMock(), namespace='/') with pytest.raises(exceptions.ConnectionError): c.connect( 'url', headers='headers', transports='transports', socketio_path='path', wait=False, ) def test_connect_twice(self): c = client.Client() c.eio.connect = mock.MagicMock() c.connect( 'url', wait=False, ) with pytest.raises(exceptions.ConnectionError): c.connect( 'url', wait=False, ) def test_connect_wait_single_namespace(self): c = client.Client() c.eio.connect = mock.MagicMock() c._connect_event = mock.MagicMock() def mock_connect(timeout): assert timeout == 0.01 c.namespaces = {'/': '123'} return True c._connect_event.wait = mock_connect c.connect( 'url', wait=True, wait_timeout=0.01, ) assert c.connected is True def test_connect_wait_two_namespaces(self): c = client.Client() c.eio.connect = mock.MagicMock() c._connect_event = mock.MagicMock() def mock_connect(timeout): assert timeout == 0.01 if c.namespaces == {}: c.namespaces = {'/bar': '123'} return True elif c.namespaces == {'/bar': '123'}: c.namespaces = {'/bar': '123', '/foo': '456'} return True return False c._connect_event.wait = mock_connect c.connect( 'url', namespaces=['/foo', '/bar'], wait=True, wait_timeout=0.01, ) assert c.connected is True assert c.namespaces == {'/bar': '123', '/foo': '456'} def test_connect_timeout(self): c = client.Client() c.eio.connect = mock.MagicMock() c.disconnect = mock.MagicMock() with pytest.raises(exceptions.ConnectionError): c.connect( 'url', wait=True, wait_timeout=0.01, ) c.disconnect.assert_called_once_with() def test_wait_no_reconnect(self): c = client.Client() c.eio.wait = mock.MagicMock() c.sleep = mock.MagicMock() c._reconnect_task = None c.wait() c.eio.wait.assert_called_once_with() c.sleep.assert_called_once_with(1) def test_wait_reconnect_failed(self): c = client.Client() c.eio.wait = mock.MagicMock() c.sleep = mock.MagicMock() c._reconnect_task = mock.MagicMock() states = ['disconnected'] def fake_join(): c.eio.state = states.pop(0) c._reconnect_task.join = fake_join c.wait() c.eio.wait.assert_called_once_with() c.sleep.assert_called_once_with(1) def test_wait_reconnect_successful(self): c = client.Client() c.eio.wait = mock.MagicMock() c.sleep = mock.MagicMock() c._reconnect_task = mock.MagicMock() states = ['connected', 'disconnected'] def fake_join(): c.eio.state = states.pop(0) c._reconnect_task.join = fake_join c.wait() assert c.eio.wait.call_count == 2 assert c.sleep.call_count == 2 def test_get_sid(self): c = client.Client() c.namespaces = {'/': '1', '/foo': '2'} assert c.get_sid() == '1' assert c.get_sid('/') == '1' assert c.get_sid('/foo') == '2' assert c.get_sid('/bar') is None def test_emit_no_arguments(self): c = client.Client() c.namespaces = {'/': '1'} c._send_packet = mock.MagicMock() c.emit('foo') expected_packet = packet.Packet( packet.EVENT, namespace='/', data=['foo'], id=None) assert c._send_packet.call_count == 1 assert ( c._send_packet.call_args_list[0][0][0].encode() == expected_packet.encode() ) def test_emit_one_argument(self): c = client.Client() c.namespaces = {'/': '1'} c._send_packet = mock.MagicMock() c.emit('foo', 'bar') expected_packet = packet.Packet( packet.EVENT, namespace='/', data=['foo', 'bar'], id=None, ) assert c._send_packet.call_count == 1 assert ( c._send_packet.call_args_list[0][0][0].encode() == expected_packet.encode() ) def test_emit_one_argument_list(self): c = client.Client() c.namespaces = {'/': '1'} c._send_packet = mock.MagicMock() c.emit('foo', ['bar', 'baz']) expected_packet = packet.Packet( packet.EVENT, namespace='/', data=['foo', ['bar', 'baz']], id=None, ) assert c._send_packet.call_count == 1 assert ( c._send_packet.call_args_list[0][0][0].encode() == expected_packet.encode() ) def test_emit_two_arguments(self): c = client.Client() c.namespaces = {'/': '1'} c._send_packet = mock.MagicMock() c.emit('foo', ('bar', 'baz')) expected_packet = packet.Packet( packet.EVENT, namespace='/', data=['foo', 'bar', 'baz'], id=None, ) assert c._send_packet.call_count == 1 assert ( c._send_packet.call_args_list[0][0][0].encode() == expected_packet.encode() ) def test_emit_namespace(self): c = client.Client() c.namespaces = ['/foo'] c._send_packet = mock.MagicMock() c.emit('foo', namespace='/foo') expected_packet = packet.Packet( packet.EVENT, namespace='/foo', data=['foo'], id=None) assert c._send_packet.call_count == 1 assert ( c._send_packet.call_args_list[0][0][0].encode() == expected_packet.encode() ) def test_emit_unknown_namespace(self): c = client.Client() c.namespaces = ['/foo'] with pytest.raises(exceptions.BadNamespaceError): c.emit('foo', namespace='/bar') def test_emit_with_callback(self): c = client.Client() c.namespaces = {'/': '1'} c._send_packet = mock.MagicMock() c._generate_ack_id = mock.MagicMock(return_value=123) c.emit('foo', callback='cb') expected_packet = packet.Packet( packet.EVENT, namespace='/', data=['foo'], id=123) assert c._send_packet.call_count == 1 assert ( c._send_packet.call_args_list[0][0][0].encode() == expected_packet.encode() ) c._generate_ack_id.assert_called_once_with('/', 'cb') def test_emit_namespace_with_callback(self): c = client.Client() c.namespaces = {'/foo': '1'} c._send_packet = mock.MagicMock() c._generate_ack_id = mock.MagicMock(return_value=123) c.emit('foo', namespace='/foo', callback='cb') expected_packet = packet.Packet( packet.EVENT, namespace='/foo', data=['foo'], id=123) assert c._send_packet.call_count == 1 assert ( c._send_packet.call_args_list[0][0][0].encode() == expected_packet.encode() ) c._generate_ack_id.assert_called_once_with('/foo', 'cb') def test_emit_binary(self): c = client.Client() c.namespaces = {'/': '1'} c._send_packet = mock.MagicMock() c.emit('foo', b'bar') expected_packet = packet.Packet( packet.EVENT, namespace='/', data=['foo', b'bar'], id=None, ) assert c._send_packet.call_count == 1 assert ( c._send_packet.call_args_list[0][0][0].encode() == expected_packet.encode() ) def test_emit_not_binary(self): c = client.Client() c.namespaces = {'/': '1'} c._send_packet = mock.MagicMock() c.emit('foo', 'bar') expected_packet = packet.Packet( packet.EVENT, namespace='/', data=['foo', 'bar'], id=None, ) assert c._send_packet.call_count == 1 assert ( c._send_packet.call_args_list[0][0][0].encode() == expected_packet.encode() ) def test_send(self): c = client.Client() c.emit = mock.MagicMock() c.send('data', 'namespace', 'callback') c.emit.assert_called_once_with( 'message', data='data', namespace='namespace', callback='callback' ) def test_send_with_defaults(self): c = client.Client() c.emit = mock.MagicMock() c.send('data') c.emit.assert_called_once_with( 'message', data='data', namespace=None, callback=None ) def test_call(self): c = client.Client() c.namespaces = {'/': '1'} def fake_event_wait(timeout=None): assert timeout == 60 c._generate_ack_id.call_args_list[0][0][1]('foo', 321) return True c._send_packet = mock.MagicMock() c._generate_ack_id = mock.MagicMock(return_value=123) c.eio = mock.MagicMock() c.eio.create_event.return_value.wait = fake_event_wait assert c.call('foo') == ('foo', 321) expected_packet = packet.Packet( packet.EVENT, namespace='/', data=['foo'], id=123) assert c._send_packet.call_count == 1 assert ( c._send_packet.call_args_list[0][0][0].encode() == expected_packet.encode() ) def test_call_with_timeout(self): c = client.Client() c.namespaces = {'/': '1'} def fake_event_wait(timeout=None): assert timeout == 12 return False c._send_packet = mock.MagicMock() c._generate_ack_id = mock.MagicMock(return_value=123) c.eio = mock.MagicMock() c.eio.create_event.return_value.wait = fake_event_wait with pytest.raises(exceptions.TimeoutError): c.call('foo', timeout=12) expected_packet = packet.Packet( packet.EVENT, namespace='/', data=['foo'], id=123) assert c._send_packet.call_count == 1 assert ( c._send_packet.call_args_list[0][0][0].encode() == expected_packet.encode() ) def test_disconnect(self): c = client.Client() c.connected = True c.namespaces = {'/': '1'} c._trigger_event = mock.MagicMock() c._send_packet = mock.MagicMock() c.eio = mock.MagicMock() c.eio.state = 'connected' c.disconnect() assert c.connected assert c._trigger_event.call_count == 0 assert c._send_packet.call_count == 1 expected_packet = packet.Packet(packet.DISCONNECT, namespace='/') assert ( c._send_packet.call_args_list[0][0][0].encode() == expected_packet.encode() ) c.eio.disconnect.assert_called_once_with() def test_disconnect_namespaces(self): c = client.Client() c.connected = True c.namespaces = {'/foo': '1', '/bar': '2'} c._trigger_event = mock.MagicMock() c._send_packet = mock.MagicMock() c.eio = mock.MagicMock() c.eio.state = 'connected' c.disconnect() assert c._trigger_event.call_count == 0 assert c._send_packet.call_count == 2 expected_packet = packet.Packet(packet.DISCONNECT, namespace='/foo') assert ( c._send_packet.call_args_list[0][0][0].encode() == expected_packet.encode() ) expected_packet = packet.Packet(packet.DISCONNECT, namespace='/bar') assert ( c._send_packet.call_args_list[1][0][0].encode() == expected_packet.encode() ) def test_transport(self): c = client.Client() c.eio.transport = mock.MagicMock(return_value='foo') assert c.transport() == 'foo' c.eio.transport.assert_called_once_with() def test_start_background_task(self): c = client.Client() c.eio.start_background_task = mock.MagicMock(return_value='foo') assert c.start_background_task('foo', 'bar', baz='baz') == 'foo' c.eio.start_background_task.assert_called_once_with( 'foo', 'bar', baz='baz' ) def test_sleep(self): c = client.Client() c.eio.sleep = mock.MagicMock() c.sleep(1.23) c.eio.sleep.assert_called_once_with(1.23) def test_send_packet(self): c = client.Client() c.eio.send = mock.MagicMock() c._send_packet(packet.Packet(packet.EVENT, 'foo')) c.eio.send.assert_called_once_with('2"foo"') def test_send_packet_binary(self): c = client.Client() c.eio.send = mock.MagicMock() c._send_packet(packet.Packet(packet.EVENT, b'foo')) assert c.eio.send.call_args_list == [ mock.call('51-{"_placeholder":true,"num":0}'), mock.call(b'foo'), ] or c.eio.send.call_args_list == [ mock.call('51-{"num":0,"_placeholder":true}'), mock.call(b'foo'), ] def test_send_packet_default_binary(self): c = client.Client() c.eio.send = mock.MagicMock() c._send_packet(packet.Packet(packet.EVENT, 'foo')) c.eio.send.assert_called_once_with('2"foo"') def test_generate_ack_id(self): c = client.Client() assert c._generate_ack_id('/', 'cb') == 1 assert c._generate_ack_id('/', 'cb') == 2 assert c._generate_ack_id('/', 'cb') == 3 assert c._generate_ack_id('/foo', 'cb') == 1 assert c._generate_ack_id('/bar', 'cb') == 1 assert c._generate_ack_id('/', 'cb') == 4 assert c._generate_ack_id('/bar', 'cb') == 2 def test_handle_connect(self): c = client.Client() c._connect_event = mock.MagicMock() c._trigger_event = mock.MagicMock() c._send_packet = mock.MagicMock() c._handle_connect('/', {'sid': '123'}) assert c.namespaces == {'/': '123'} c._connect_event.set.assert_called_once_with() c._trigger_event.assert_called_once_with('connect', namespace='/') c._send_packet.assert_not_called() def test_handle_connect_with_namespaces(self): c = client.Client() c.namespaces = {'/foo': '1', '/bar': '2'} c._connect_event = mock.MagicMock() c._trigger_event = mock.MagicMock() c._send_packet = mock.MagicMock() c._handle_connect('/', {'sid': '3'}) c._connect_event.set.assert_called_once_with() c._trigger_event.assert_called_once_with('connect', namespace='/') assert c.namespaces == {'/': '3', '/foo': '1', '/bar': '2'} def test_handle_connect_namespace(self): c = client.Client() c.namespaces = {'/foo': '1'} c._connect_event = mock.MagicMock() c._trigger_event = mock.MagicMock() c._send_packet = mock.MagicMock() c._handle_connect('/foo', {'sid': '123'}) c._handle_connect('/bar', {'sid': '2'}) assert c._trigger_event.call_count == 1 c._connect_event.set.assert_called_once_with() c._trigger_event.assert_called_once_with('connect', namespace='/bar') assert c.namespaces == {'/foo': '1', '/bar': '2'} def test_handle_disconnect(self): c = client.Client() c.namespace = {'/': '1'} c.connected = True c._trigger_event = mock.MagicMock() c._handle_disconnect('/') c._trigger_event.assert_any_call('disconnect', '/', c.reason.SERVER_DISCONNECT) c._trigger_event.assert_any_call('__disconnect_final', '/') assert not c.connected c._handle_disconnect('/') assert c._trigger_event.call_count == 2 def test_handle_disconnect_namespace(self): c = client.Client() c.connected = True c.namespaces = {'/foo': '1', '/bar': '2'} c._trigger_event = mock.MagicMock() c._handle_disconnect('/foo') c._trigger_event.assert_any_call('disconnect', '/foo', c.reason.SERVER_DISCONNECT) c._trigger_event.assert_any_call('__disconnect_final', '/foo') assert c.namespaces == {'/bar': '2'} assert c.connected c._handle_disconnect('/bar') c._trigger_event.assert_any_call('disconnect', '/bar', c.reason.SERVER_DISCONNECT) c._trigger_event.assert_any_call('__disconnect_final', '/bar') assert c.namespaces == {} assert not c.connected def test_handle_disconnect_unknown_namespace(self): c = client.Client() c.connected = True c.namespaces = {'/foo': '1', '/bar': '2'} c._trigger_event = mock.MagicMock() c._handle_disconnect('/baz') c._trigger_event.assert_any_call('disconnect', '/baz', c.reason.SERVER_DISCONNECT) c._trigger_event.assert_any_call('__disconnect_final', '/baz') assert c.namespaces == {'/foo': '1', '/bar': '2'} assert c.connected def test_handle_disconnect_default_namespace(self): c = client.Client() c.connected = True c.namespaces = {'/foo': '1', '/bar': '2'} c._trigger_event = mock.MagicMock() c._handle_disconnect('/') c._trigger_event.assert_any_call('disconnect', '/', c.reason.SERVER_DISCONNECT) c._trigger_event.assert_any_call('__disconnect_final', '/') assert c.namespaces == {'/foo': '1', '/bar': '2'} assert c.connected def test_handle_event(self): c = client.Client() c._trigger_event = mock.MagicMock() c._handle_event('/', None, ['foo', ('bar', 'baz')]) c._trigger_event.assert_called_once_with('foo', '/', ('bar', 'baz')) def test_handle_event_with_id_no_arguments(self): c = client.Client() c._trigger_event = mock.MagicMock(return_value=None) c._send_packet = mock.MagicMock() c._handle_event('/', 123, ['foo', ('bar', 'baz')]) c._trigger_event.assert_called_once_with('foo', '/', ('bar', 'baz')) assert c._send_packet.call_count == 1 expected_packet = packet.Packet( packet.ACK, namespace='/', id=123, data=[]) assert ( c._send_packet.call_args_list[0][0][0].encode() == expected_packet.encode() ) def test_handle_event_with_id_one_argument(self): c = client.Client() c._trigger_event = mock.MagicMock(return_value='ret') c._send_packet = mock.MagicMock() c._handle_event('/', 123, ['foo', ('bar', 'baz')]) c._trigger_event.assert_called_once_with('foo', '/', ('bar', 'baz')) assert c._send_packet.call_count == 1 expected_packet = packet.Packet( packet.ACK, namespace='/', id=123, data=['ret']) assert ( c._send_packet.call_args_list[0][0][0].encode() == expected_packet.encode() ) def test_handle_event_with_id_one_list_argument(self): c = client.Client() c._trigger_event = mock.MagicMock(return_value=['a', 'b']) c._send_packet = mock.MagicMock() c._handle_event('/', 123, ['foo', ('bar', 'baz')]) c._trigger_event.assert_called_once_with('foo', '/', ('bar', 'baz')) assert c._send_packet.call_count == 1 expected_packet = packet.Packet( packet.ACK, namespace='/', id=123, data=[['a', 'b']]) assert ( c._send_packet.call_args_list[0][0][0].encode() == expected_packet.encode() ) def test_handle_event_with_id_two_arguments(self): c = client.Client() c._trigger_event = mock.MagicMock(return_value=('a', 'b')) c._send_packet = mock.MagicMock() c._handle_event('/', 123, ['foo', ('bar', 'baz')]) c._trigger_event.assert_called_once_with('foo', '/', ('bar', 'baz')) assert c._send_packet.call_count == 1 expected_packet = packet.Packet( packet.ACK, namespace='/', id=123, data=['a', 'b']) assert ( c._send_packet.call_args_list[0][0][0].encode() == expected_packet.encode() ) def test_handle_ack(self): c = client.Client() mock_cb = mock.MagicMock() c.callbacks['/foo'] = {123: mock_cb} c._handle_ack('/foo', 123, ['bar', 'baz']) mock_cb.assert_called_once_with('bar', 'baz') assert 123 not in c.callbacks['/foo'] def test_handle_ack_not_found(self): c = client.Client() mock_cb = mock.MagicMock() c.callbacks['/foo'] = {123: mock_cb} c._handle_ack('/foo', 124, ['bar', 'baz']) mock_cb.assert_not_called() assert 123 in c.callbacks['/foo'] def test_handle_error(self): c = client.Client() c.connected = True c.namespaces = {'/foo': '1', '/bar': '2'} c._connect_event = mock.MagicMock() c._trigger_event = mock.MagicMock() c._handle_error('/', 'error') assert c.namespaces == {} assert not c.connected c._connect_event.set.assert_called_once_with() c._trigger_event.assert_called_once_with('connect_error', '/', 'error') def test_handle_error_with_no_arguments(self): c = client.Client() c.connected = True c.namespaces = {'/foo': '1', '/bar': '2'} c._connect_event = mock.MagicMock() c._trigger_event = mock.MagicMock() c._handle_error('/', None) assert c.namespaces == {} assert not c.connected c._connect_event.set.assert_called_once_with() c._trigger_event.assert_called_once_with('connect_error', '/') def test_handle_error_namespace(self): c = client.Client() c.connected = True c.namespaces = {'/foo': '1', '/bar': '2'} c._connect_event = mock.MagicMock() c._trigger_event = mock.MagicMock() c._handle_error('/bar', ['error', 'message']) assert c.namespaces == {'/foo': '1'} assert c.connected c._connect_event.set.assert_called_once_with() c._trigger_event.assert_called_once_with( 'connect_error', '/bar', 'error', 'message' ) def test_handle_error_namespace_with_no_arguments(self): c = client.Client() c.connected = True c.namespaces = {'/foo': '1', '/bar': '2'} c._connect_event = mock.MagicMock() c._trigger_event = mock.MagicMock() c._handle_error('/bar', None) assert c.namespaces == {'/foo': '1'} assert c.connected c._connect_event.set.assert_called_once_with() c._trigger_event.assert_called_once_with('connect_error', '/bar') def test_handle_error_unknown_namespace(self): c = client.Client() c.connected = True c.namespaces = {'/foo': '1', '/bar': '2'} c._connect_event = mock.MagicMock() c._handle_error('/baz', 'error') assert c.namespaces == {'/foo': '1', '/bar': '2'} assert c.connected c._connect_event.set.assert_called_once_with() def test_trigger_event(self): c = client.Client() handler = mock.MagicMock() catchall_handler = mock.MagicMock() c.on('foo', handler) c.on('*', catchall_handler) c._trigger_event('foo', '/', 1, '2') c._trigger_event('bar', '/', 1, '2', 3) c._trigger_event('connect', '/') # should not trigger handler.assert_called_once_with(1, '2') catchall_handler.assert_called_once_with('bar', 1, '2', 3) def test_trigger_event_namespace(self): c = client.Client() handler = mock.MagicMock() catchall_handler = mock.MagicMock() c.on('foo', handler, namespace='/bar') c.on('*', catchall_handler, namespace='/bar') c._trigger_event('foo', '/bar', 1, '2') c._trigger_event('bar', '/bar', 1, '2', 3) handler.assert_called_once_with(1, '2') catchall_handler.assert_called_once_with('bar', 1, '2', 3) def test_trigger_event_with_catchall_namespace(self): c = client.Client() connect_star_handler = mock.MagicMock() msg_foo_handler = mock.MagicMock() msg_star_handler = mock.MagicMock() star_foo_handler = mock.MagicMock() star_star_handler = mock.MagicMock() c.on('connect', connect_star_handler, namespace='*') c.on('msg', msg_foo_handler, namespace='/foo') c.on('msg', msg_star_handler, namespace='*') c.on('*', star_foo_handler, namespace='/foo') c.on('*', star_star_handler, namespace='*') c._trigger_event('connect', '/bar') c._trigger_event('msg', '/foo', 'a', 'b') c._trigger_event('msg', '/bar', 'a', 'b') c._trigger_event('my message', '/foo', 'a', 'b', 'c') c._trigger_event('my message', '/bar', 'a', 'b', 'c') c._trigger_event('disconnect', '/bar') connect_star_handler.assert_called_once_with('/bar') msg_foo_handler.assert_called_once_with('a', 'b') msg_star_handler.assert_called_once_with('/bar', 'a', 'b') star_foo_handler.assert_called_once_with( 'my message', 'a', 'b', 'c') star_star_handler.assert_called_once_with( 'my message', '/bar', 'a', 'b', 'c') def test_trigger_event_with_catchall_namespace_handler(self): result = {} class MyNamespace(namespace.ClientNamespace): def on_connect(self, ns): result['result'] = (ns,) def on_disconnect(self, ns, reason): result['result'] = ('disconnect', ns, reason) def on_foo(self, ns, data): result['result'] = (ns, data) def on_bar(self, ns): result['result'] = 'bar' + ns def on_baz(self, ns, data1, data2): result['result'] = (ns, data1, data2) c = client.Client() c.register_namespace(MyNamespace('*')) c._trigger_event('connect', '/foo') assert result['result'] == ('/foo',) c._trigger_event('foo', '/foo', 'a') assert result['result'] == ('/foo', 'a') c._trigger_event('bar', '/foo') assert result['result'] == 'bar/foo' c._trigger_event('baz', '/foo', 'a', 'b') assert result['result'] == ('/foo', 'a', 'b') c._trigger_event('disconnect', '/foo', 'bar') assert result['result'] == ('disconnect', '/foo', 'bar') def test_trigger_event_class_namespace(self): c = client.Client() result = [] class MyNamespace(namespace.ClientNamespace): def on_foo(self, a, b): result.append(a) result.append(b) c.register_namespace(MyNamespace('/')) c._trigger_event('foo', '/', 1, '2') assert result == [1, '2'] def test_trigger_event_unknown_namespace(self): c = client.Client() result = [] class MyNamespace(namespace.ClientNamespace): def on_foo(self, a, b): result.append(a) result.append(b) c.register_namespace(MyNamespace('/')) c._trigger_event('foo', '/bar', 1, '2') assert result == [] @mock.patch('socketio.client.random.random', side_effect=[1, 0, 0.5]) def test_handle_reconnect(self, random): c = client.Client() c._reconnect_task = 'foo' c._reconnect_abort = c.eio.create_event() c._reconnect_abort.wait = mock.MagicMock(return_value=False) c.connect = mock.MagicMock( side_effect=[ValueError, exceptions.ConnectionError, None] ) c._handle_reconnect() assert c._reconnect_abort.wait.call_count == 3 assert c._reconnect_abort.wait.call_args_list == [ mock.call(1.5), mock.call(1.5), mock.call(4.0), ] assert c._reconnect_task is None @mock.patch('socketio.client.random.random', side_effect=[1, 0, 0.5]) def test_handle_reconnect_max_delay(self, random): c = client.Client(reconnection_delay_max=3) c._reconnect_task = 'foo' c._reconnect_abort = c.eio.create_event() c._reconnect_abort.wait = mock.MagicMock(return_value=False) c.connect = mock.MagicMock( side_effect=[ValueError, exceptions.ConnectionError, None] ) c._handle_reconnect() assert c._reconnect_abort.wait.call_count == 3 assert c._reconnect_abort.wait.call_args_list == [ mock.call(1.5), mock.call(1.5), mock.call(3.0), ] assert c._reconnect_task is None @mock.patch('socketio.client.random.random', side_effect=[1, 0, 0.5]) def test_handle_reconnect_max_attempts(self, random): c = client.Client(reconnection_attempts=2) c.connection_namespaces = ['/'] c._reconnect_task = 'foo' c._reconnect_abort = c.eio.create_event() c._reconnect_abort.wait = mock.MagicMock(return_value=False) c._trigger_event = mock.MagicMock() c.connect = mock.MagicMock( side_effect=[ValueError, exceptions.ConnectionError, None] ) c._handle_reconnect() assert c._reconnect_abort.wait.call_count == 2 assert c._reconnect_abort.wait.call_args_list == [ mock.call(1.5), mock.call(1.5), ] assert c._reconnect_task == 'foo' c._trigger_event.assert_called_once_with('__disconnect_final', namespace='/') @mock.patch('socketio.client.random.random', side_effect=[1, 0, 0.5]) def test_handle_reconnect_aborted(self, random): c = client.Client() c.connection_namespaces = ['/'] c._reconnect_task = 'foo' c._reconnect_abort = c.eio.create_event() c._reconnect_abort.wait = mock.MagicMock(side_effect=[False, True]) c._trigger_event = mock.MagicMock() c.connect = mock.MagicMock(side_effect=exceptions.ConnectionError) c._handle_reconnect() assert c._reconnect_abort.wait.call_count == 2 assert c._reconnect_abort.wait.call_args_list == [ mock.call(1.5), mock.call(1.5), ] assert c._reconnect_task == 'foo' c._trigger_event.assert_called_once_with('__disconnect_final', namespace='/') def test_shutdown_disconnect(self): c = client.Client() c.connected = True c.namespaces = {'/': '1'} c._trigger_event = mock.MagicMock() c._send_packet = mock.MagicMock() c.eio = mock.MagicMock() c.eio.state = 'connected' c.shutdown() assert c._trigger_event.call_count == 0 assert c._send_packet.call_count == 1 expected_packet = packet.Packet(packet.DISCONNECT, namespace='/') assert ( c._send_packet.call_args_list[0][0][0].encode() == expected_packet.encode() ) c.eio.disconnect.assert_called_once_with() def test_shutdown_disconnect_namespaces(self): c = client.Client() c.connected = True c.namespaces = {'/foo': '1', '/bar': '2'} c._trigger_event = mock.MagicMock() c._send_packet = mock.MagicMock() c.eio = mock.MagicMock() c.eio.state = 'connected' c.shutdown() assert c._trigger_event.call_count == 0 assert c._send_packet.call_count == 2 expected_packet = packet.Packet(packet.DISCONNECT, namespace='/foo') assert ( c._send_packet.call_args_list[0][0][0].encode() == expected_packet.encode() ) expected_packet = packet.Packet(packet.DISCONNECT, namespace='/bar') assert ( c._send_packet.call_args_list[1][0][0].encode() == expected_packet.encode() ) @mock.patch('socketio.client.random.random', side_effect=[1, 0, 0.5]) def test_shutdown_reconnect(self, random): c = client.Client() c.connection_namespaces = ['/'] c._reconnect_task = mock.MagicMock() c._trigger_event = mock.MagicMock() c.connect = mock.MagicMock(side_effect=exceptions.ConnectionError) task = c.start_background_task(c._handle_reconnect) time.sleep(0.1) c.shutdown() task.join() c._trigger_event.assert_called_once_with('__disconnect_final', namespace='/') c._reconnect_task.join.assert_called_once_with() def test_handle_eio_connect(self): c = client.Client() c.connection_namespaces = ['/', '/foo'] c.connection_auth = 'auth' c._send_packet = mock.MagicMock() c.eio.sid = 'foo' assert c.sid is None c._handle_eio_connect() assert c.sid == 'foo' assert c._send_packet.call_count == 2 expected_packet = packet.Packet( packet.CONNECT, data='auth', namespace='/') assert ( c._send_packet.call_args_list[0][0][0].encode() == expected_packet.encode() ) expected_packet = packet.Packet( packet.CONNECT, data='auth', namespace='/foo') assert ( c._send_packet.call_args_list[1][0][0].encode() == expected_packet.encode() ) def test_handle_eio_connect_function(self): c = client.Client() c.connection_namespaces = ['/', '/foo'] c.connection_auth = lambda: 'auth' c._send_packet = mock.MagicMock() c.eio.sid = 'foo' assert c.sid is None c._handle_eio_connect() assert c.sid == 'foo' assert c._send_packet.call_count == 2 expected_packet = packet.Packet( packet.CONNECT, data='auth', namespace='/') assert ( c._send_packet.call_args_list[0][0][0].encode() == expected_packet.encode() ) expected_packet = packet.Packet( packet.CONNECT, data='auth', namespace='/foo') assert ( c._send_packet.call_args_list[1][0][0].encode() == expected_packet.encode() ) def test_handle_eio_message(self): c = client.Client() c._handle_connect = mock.MagicMock() c._handle_disconnect = mock.MagicMock() c._handle_event = mock.MagicMock() c._handle_ack = mock.MagicMock() c._handle_error = mock.MagicMock() c._handle_eio_message('0{"sid":"123"}') c._handle_connect.assert_called_with(None, {'sid': '123'}) c._handle_eio_message('0/foo,{"sid":"123"}') c._handle_connect.assert_called_with('/foo', {'sid': '123'}) c._handle_eio_message('1') c._handle_disconnect.assert_called_with(None) c._handle_eio_message('1/foo') c._handle_disconnect.assert_called_with('/foo') c._handle_eio_message('2["foo"]') c._handle_event.assert_called_with(None, None, ['foo']) c._handle_eio_message('3/foo,["bar"]') c._handle_ack.assert_called_with('/foo', None, ['bar']) c._handle_eio_message('4') c._handle_error.assert_called_with(None, None) c._handle_eio_message('4"foo"') c._handle_error.assert_called_with(None, 'foo') c._handle_eio_message('4["foo"]') c._handle_error.assert_called_with(None, ['foo']) c._handle_eio_message('4/foo') c._handle_error.assert_called_with('/foo', None) c._handle_eio_message('4/foo,["foo","bar"]') c._handle_error.assert_called_with('/foo', ['foo', 'bar']) c._handle_eio_message('51-{"_placeholder":true,"num":0}') assert c._binary_packet.packet_type == packet.BINARY_EVENT c._handle_eio_message(b'foo') c._handle_event.assert_called_with(None, None, b'foo') c._handle_eio_message( '62-/foo,{"1":{"_placeholder":true,"num":1},' '"2":{"_placeholder":true,"num":0}}' ) assert c._binary_packet.packet_type == packet.BINARY_ACK c._handle_eio_message(b'bar') c._handle_eio_message(b'foo') c._handle_ack.assert_called_with( '/foo', None, {'1': b'foo', '2': b'bar'} ) with pytest.raises(ValueError): c._handle_eio_message('9') def test_eio_disconnect(self): c = client.Client() c.namespaces = {'/': '1'} c.connected = True c._trigger_event = mock.MagicMock() c.start_background_task = mock.MagicMock() c.sid = 'foo' c.eio.state = 'connected' c._handle_eio_disconnect('foo') c._trigger_event.assert_called_once_with('disconnect', '/', 'foo') assert c.sid is None assert not c.connected def test_eio_disconnect_namespaces(self): c = client.Client() c.connected = True c.namespaces = {'/': '1', '/foo': '2', '/bar': '3'} c._trigger_event = mock.MagicMock() c.start_background_task = mock.MagicMock() c.sid = 'foo' c.eio.state = 'connected' c._handle_eio_disconnect(c.reason.CLIENT_DISCONNECT) c._trigger_event.assert_any_call('disconnect', '/foo', c.reason.CLIENT_DISCONNECT) c._trigger_event.assert_any_call('disconnect', '/bar', c.reason.CLIENT_DISCONNECT) c._trigger_event.assert_any_call('disconnect', '/', c.reason.CLIENT_DISCONNECT) assert c.sid is None assert not c.connected def test_eio_disconnect_reconnect(self): c = client.Client(reconnection=True) c.start_background_task = mock.MagicMock() c.eio.state = 'connected' c._handle_eio_disconnect(c.reason.CLIENT_DISCONNECT) c.start_background_task.assert_called_once_with(c._handle_reconnect) def test_eio_disconnect_self_disconnect(self): c = client.Client(reconnection=True) c.start_background_task = mock.MagicMock() c.eio.state = 'disconnected' c._handle_eio_disconnect(c.reason.CLIENT_DISCONNECT) c.start_background_task.assert_not_called() def test_eio_disconnect_no_reconnect(self): c = client.Client(reconnection=False) c.namespaces = {'/': '1'} c.connected = True c._trigger_event = mock.MagicMock() c.start_background_task = mock.MagicMock() c.sid = 'foo' c.eio.state = 'connected' c._handle_eio_disconnect(c.reason.TRANSPORT_ERROR) c._trigger_event.assert_any_call('disconnect', '/', c.reason.TRANSPORT_ERROR) c._trigger_event.assert_any_call('__disconnect_final', '/') assert c.sid is None assert not c.connected c.start_background_task.assert_not_called() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1734189874.0 python_socketio-5.13.0/tests/common/test_manager.py0000664000175000017500000003643314727321462022107 0ustar00miguelmiguelfrom unittest import mock import pytest from socketio import manager from socketio import packet class TestBaseManager: def setup_method(self): id = 0 def generate_id(): nonlocal id id += 1 return str(id) mock_server = mock.MagicMock() mock_server.eio.generate_id = generate_id mock_server.packet_class = packet.Packet self.bm = manager.Manager() self.bm.set_server(mock_server) self.bm.initialize() def test_connect(self): sid = self.bm.connect('123', '/foo') assert None in self.bm.rooms['/foo'] assert sid in self.bm.rooms['/foo'] assert sid in self.bm.rooms['/foo'][None] assert sid in self.bm.rooms['/foo'][sid] assert dict(self.bm.rooms['/foo'][None]) == {sid: '123'} assert dict(self.bm.rooms['/foo'][sid]) == {sid: '123'} assert self.bm.sid_from_eio_sid('123', '/foo') == sid assert self.bm.sid_from_eio_sid('1234', '/foo') is None assert self.bm.sid_from_eio_sid('123', '/bar') is None assert self.bm.eio_sid_from_sid(sid, '/foo') == '123' assert self.bm.eio_sid_from_sid('x', '/foo') is None assert self.bm.eio_sid_from_sid(sid, '/bar') is None def test_pre_disconnect(self): sid1 = self.bm.connect('123', '/foo') sid2 = self.bm.connect('456', '/foo') assert self.bm.is_connected(sid1, '/foo') assert self.bm.pre_disconnect(sid1, '/foo') == '123' assert self.bm.pending_disconnect == {'/foo': [sid1]} assert not self.bm.is_connected(sid1, '/foo') assert self.bm.pre_disconnect(sid2, '/foo') == '456' assert self.bm.pending_disconnect == {'/foo': [sid1, sid2]} assert not self.bm.is_connected(sid2, '/foo') self.bm.disconnect(sid1, '/foo') assert self.bm.pending_disconnect == {'/foo': [sid2]} self.bm.disconnect(sid2, '/foo') assert self.bm.pending_disconnect == {} def test_disconnect(self): sid1 = self.bm.connect('123', '/foo') sid2 = self.bm.connect('456', '/foo') self.bm.enter_room(sid1, '/foo', 'bar') self.bm.enter_room(sid2, '/foo', 'baz') self.bm.disconnect(sid1, '/foo') assert dict(self.bm.rooms['/foo'][None]) == {sid2: '456'} assert dict(self.bm.rooms['/foo'][sid2]) == {sid2: '456'} assert dict(self.bm.rooms['/foo']['baz']) == {sid2: '456'} def test_disconnect_default_namespace(self): sid1 = self.bm.connect('123', '/') sid2 = self.bm.connect('123', '/foo') sid3 = self.bm.connect('456', '/') sid4 = self.bm.connect('456', '/foo') assert self.bm.is_connected(sid1, '/') assert self.bm.is_connected(sid2, '/foo') assert not self.bm.is_connected(sid2, '/') assert not self.bm.is_connected(sid1, '/foo') self.bm.disconnect(sid1, '/') assert not self.bm.is_connected(sid1, '/') assert self.bm.is_connected(sid2, '/foo') self.bm.disconnect(sid2, '/foo') assert not self.bm.is_connected(sid2, '/foo') assert dict(self.bm.rooms['/'][None]) == {sid3: '456'} assert dict(self.bm.rooms['/'][sid3]) == {sid3: '456'} assert dict(self.bm.rooms['/foo'][None]) == {sid4: '456'} assert dict(self.bm.rooms['/foo'][sid4]) == {sid4: '456'} def test_disconnect_twice(self): sid1 = self.bm.connect('123', '/') sid2 = self.bm.connect('123', '/foo') sid3 = self.bm.connect('456', '/') sid4 = self.bm.connect('456', '/foo') self.bm.disconnect(sid1, '/') self.bm.disconnect(sid2, '/foo') self.bm.disconnect(sid1, '/') self.bm.disconnect(sid2, '/foo') assert dict(self.bm.rooms['/'][None]) == {sid3: '456'} assert dict(self.bm.rooms['/'][sid3]) == {sid3: '456'} assert dict(self.bm.rooms['/foo'][None]) == {sid4: '456'} assert dict(self.bm.rooms['/foo'][sid4]) == {sid4: '456'} def test_disconnect_all(self): sid1 = self.bm.connect('123', '/foo') sid2 = self.bm.connect('456', '/foo') self.bm.enter_room(sid1, '/foo', 'bar') self.bm.enter_room(sid2, '/foo', 'baz') self.bm.disconnect(sid1, '/foo') self.bm.disconnect(sid2, '/foo') assert self.bm.rooms == {} def test_disconnect_with_callbacks(self): sid1 = self.bm.connect('123', '/') sid2 = self.bm.connect('123', '/foo') sid3 = self.bm.connect('456', '/foo') self.bm._generate_ack_id(sid1, 'f') self.bm._generate_ack_id(sid2, 'g') self.bm._generate_ack_id(sid3, 'h') self.bm.disconnect(sid2, '/foo') assert sid2 not in self.bm.callbacks self.bm.disconnect(sid1, '/') assert sid1 not in self.bm.callbacks assert sid3 in self.bm.callbacks def test_disconnect_bad_namespace(self): self.bm.connect('123', '/') self.bm.connect('123', '/foo') self.bm.disconnect('123', '/bar') # should not assert def test_enter_room_bad_namespace(self): sid = self.bm.connect('123', '/') with pytest.raises(ValueError): self.bm.enter_room(sid, '/foo', 'bar') def test_trigger_callback(self): sid1 = self.bm.connect('123', '/') sid2 = self.bm.connect('123', '/foo') cb = mock.MagicMock() id1 = self.bm._generate_ack_id(sid1, cb) id2 = self.bm._generate_ack_id(sid2, cb) id3 = self.bm._generate_ack_id(sid1, cb) self.bm.trigger_callback(sid1, id1, ['foo']) self.bm.trigger_callback(sid1, id3, ['bar']) self.bm.trigger_callback(sid2, id2, ['bar', 'baz']) assert cb.call_count == 3 cb.assert_any_call('foo') cb.assert_any_call('bar') cb.assert_any_call('bar', 'baz') def test_invalid_callback(self): sid = self.bm.connect('123', '/') cb = mock.MagicMock() id = self.bm._generate_ack_id(sid, cb) # these should not raise an exception self.bm.trigger_callback('xxx', id, ['foo']) self.bm.trigger_callback(sid, id + 1, ['foo']) assert cb.call_count == 0 def test_get_namespaces(self): assert list(self.bm.get_namespaces()) == [] self.bm.connect('123', '/') self.bm.connect('123', '/foo') namespaces = list(self.bm.get_namespaces()) assert len(namespaces) == 2 assert '/' in namespaces assert '/foo' in namespaces def test_get_participants(self): sid1 = self.bm.connect('123', '/') sid2 = self.bm.connect('456', '/') sid3 = self.bm.connect('789', '/') self.bm.disconnect(sid3, '/') assert sid3 not in self.bm.rooms['/'][None] participants = list(self.bm.get_participants('/', None)) assert len(participants) == 2 assert (sid1, '123') in participants assert (sid2, '456') in participants assert (sid3, '789') not in participants def test_leave_invalid_room(self): sid = self.bm.connect('123', '/foo') self.bm.leave_room(sid, '/foo', 'baz') self.bm.leave_room(sid, '/bar', 'baz') def test_no_room(self): rooms = self.bm.get_rooms('123', '/foo') assert [] == rooms def test_close_room(self): sid1 = self.bm.connect('123', '/foo') self.bm.connect('456', '/foo') self.bm.connect('789', '/foo') self.bm.enter_room(sid1, '/foo', 'bar') self.bm.enter_room(sid1, '/foo', 'bar') self.bm.close_room('bar', '/foo') assert 'bar' not in self.bm.rooms['/foo'] def test_close_invalid_room(self): self.bm.close_room('bar', '/foo') def test_rooms(self): sid = self.bm.connect('123', '/foo') self.bm.enter_room(sid, '/foo', 'bar') r = self.bm.get_rooms(sid, '/foo') assert len(r) == 2 assert sid in r assert 'bar' in r def test_emit_to_sid(self): sid = self.bm.connect('123', '/foo') self.bm.connect('456', '/foo') self.bm.emit('my event', {'foo': 'bar'}, namespace='/foo', to=sid) assert self.bm.server._send_eio_packet.call_count == 1 assert self.bm.server._send_eio_packet.call_args_list[0][0][0] == '123' pkt = self.bm.server._send_eio_packet.call_args_list[0][0][1] assert pkt.encode() == '42/foo,["my event",{"foo":"bar"}]' def test_emit_to_room(self): sid1 = self.bm.connect('123', '/foo') self.bm.enter_room(sid1, '/foo', 'bar') sid2 = self.bm.connect('456', '/foo') self.bm.enter_room(sid2, '/foo', 'bar') self.bm.connect('789', '/foo') self.bm.emit('my event', {'foo': 'bar'}, namespace='/foo', room='bar') assert self.bm.server._send_eio_packet.call_count == 2 assert self.bm.server._send_eio_packet.call_args_list[0][0][0] == '123' assert self.bm.server._send_eio_packet.call_args_list[1][0][0] == '456' pkt = self.bm.server._send_eio_packet.call_args_list[0][0][1] assert pkt == self.bm.server._send_eio_packet.call_args_list[1][0][1] assert pkt.encode() == '42/foo,["my event",{"foo":"bar"}]' def test_emit_to_rooms(self): sid1 = self.bm.connect('123', '/foo') self.bm.enter_room(sid1, '/foo', 'bar') sid2 = self.bm.connect('456', '/foo') self.bm.enter_room(sid2, '/foo', 'bar') self.bm.enter_room(sid2, '/foo', 'baz') sid3 = self.bm.connect('789', '/foo') self.bm.enter_room(sid3, '/foo', 'baz') self.bm.emit('my event', {'foo': 'bar'}, namespace='/foo', room=['bar', 'baz']) assert self.bm.server._send_eio_packet.call_count == 3 assert self.bm.server._send_eio_packet.call_args_list[0][0][0] == '123' assert self.bm.server._send_eio_packet.call_args_list[1][0][0] == '456' assert self.bm.server._send_eio_packet.call_args_list[2][0][0] == '789' pkt = self.bm.server._send_eio_packet.call_args_list[0][0][1] assert pkt == self.bm.server._send_eio_packet.call_args_list[1][0][1] assert pkt == self.bm.server._send_eio_packet.call_args_list[2][0][1] assert pkt.encode() == '42/foo,["my event",{"foo":"bar"}]' def test_emit_to_all(self): sid1 = self.bm.connect('123', '/foo') self.bm.enter_room(sid1, '/foo', 'bar') sid2 = self.bm.connect('456', '/foo') self.bm.enter_room(sid2, '/foo', 'bar') self.bm.connect('789', '/foo') self.bm.connect('abc', '/bar') self.bm.emit('my event', {'foo': 'bar'}, namespace='/foo') assert self.bm.server._send_eio_packet.call_count == 3 assert self.bm.server._send_eio_packet.call_args_list[0][0][0] == '123' assert self.bm.server._send_eio_packet.call_args_list[1][0][0] == '456' assert self.bm.server._send_eio_packet.call_args_list[2][0][0] == '789' pkt = self.bm.server._send_eio_packet.call_args_list[0][0][1] assert pkt == self.bm.server._send_eio_packet.call_args_list[1][0][1] assert pkt == self.bm.server._send_eio_packet.call_args_list[2][0][1] assert pkt.encode() == '42/foo,["my event",{"foo":"bar"}]' def test_emit_to_all_skip_one(self): sid1 = self.bm.connect('123', '/foo') self.bm.enter_room(sid1, '/foo', 'bar') sid2 = self.bm.connect('456', '/foo') self.bm.enter_room(sid2, '/foo', 'bar') self.bm.connect('789', '/foo') self.bm.connect('abc', '/bar') self.bm.emit( 'my event', {'foo': 'bar'}, namespace='/foo', skip_sid=sid2 ) assert self.bm.server._send_eio_packet.call_count == 2 assert self.bm.server._send_eio_packet.call_args_list[0][0][0] == '123' assert self.bm.server._send_eio_packet.call_args_list[1][0][0] == '789' pkt = self.bm.server._send_eio_packet.call_args_list[0][0][1] assert pkt == self.bm.server._send_eio_packet.call_args_list[1][0][1] assert pkt.encode() == '42/foo,["my event",{"foo":"bar"}]' def test_emit_to_all_skip_two(self): sid1 = self.bm.connect('123', '/foo') self.bm.enter_room(sid1, '/foo', 'bar') sid2 = self.bm.connect('456', '/foo') self.bm.enter_room(sid2, '/foo', 'bar') sid3 = self.bm.connect('789', '/foo') self.bm.connect('abc', '/bar') self.bm.emit( 'my event', {'foo': 'bar'}, namespace='/foo', skip_sid=[sid1, sid3], ) assert self.bm.server._send_eio_packet.call_count == 1 assert self.bm.server._send_eio_packet.call_args_list[0][0][0] == '456' pkt = self.bm.server._send_eio_packet.call_args_list[0][0][1] assert pkt.encode() == '42/foo,["my event",{"foo":"bar"}]' def test_emit_with_callback(self): sid = self.bm.connect('123', '/foo') self.bm._generate_ack_id = mock.MagicMock() self.bm._generate_ack_id.return_value = 11 self.bm.emit( 'my event', {'foo': 'bar'}, namespace='/foo', callback='cb' ) self.bm._generate_ack_id.assert_called_once_with(sid, 'cb') assert self.bm.server._send_packet.call_count == 1 assert self.bm.server._send_packet.call_args_list[0][0][0] == '123' pkt = self.bm.server._send_packet.call_args_list[0][0][1] assert pkt.encode() == '2/foo,11["my event",{"foo":"bar"}]' def test_emit_to_invalid_room(self): self.bm.emit('my event', {'foo': 'bar'}, namespace='/', room='123') def test_emit_to_invalid_namespace(self): self.bm.emit('my event', {'foo': 'bar'}, namespace='/foo') def test_emit_with_tuple(self): sid = self.bm.connect('123', '/foo') self.bm.emit('my event', ('foo', 'bar'), namespace='/foo', room=sid) assert self.bm.server._send_eio_packet.call_count == 1 assert self.bm.server._send_eio_packet.call_args_list[0][0][0] == '123' pkt = self.bm.server._send_eio_packet.call_args_list[0][0][1] assert pkt.encode() == '42/foo,["my event","foo","bar"]' def test_emit_with_list(self): sid = self.bm.connect('123', '/foo') self.bm.emit('my event', ['foo', 'bar'], namespace='/foo', room=sid) assert self.bm.server._send_eio_packet.call_count == 1 assert self.bm.server._send_eio_packet.call_args_list[0][0][0] == '123' pkt = self.bm.server._send_eio_packet.call_args_list[0][0][1] assert pkt.encode() == '42/foo,["my event",["foo","bar"]]' def test_emit_with_none(self): sid = self.bm.connect('123', '/foo') self.bm.emit('my event', None, namespace='/foo', room=sid) assert self.bm.server._send_eio_packet.call_count == 1 assert self.bm.server._send_eio_packet.call_args_list[0][0][0] == '123' pkt = self.bm.server._send_eio_packet.call_args_list[0][0][1] assert pkt.encode() == '42/foo,["my event"]' def test_emit_binary(self): sid = self.bm.connect('123', '/') self.bm.emit('my event', b'my binary data', namespace='/', room=sid) assert self.bm.server._send_eio_packet.call_count == 2 assert self.bm.server._send_eio_packet.call_args_list[0][0][0] == '123' pkt = self.bm.server._send_eio_packet.call_args_list[0][0][1] assert pkt.encode() == '451-["my event",{"_placeholder":true,"num":0}]' assert self.bm.server._send_eio_packet.call_args_list[1][0][0] == '123' pkt = self.bm.server._send_eio_packet.call_args_list[1][0][1] assert pkt.encode() == b'my binary data' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1734189874.0 python_socketio-5.13.0/tests/common/test_middleware.py0000664000175000017500000000241614727321462022604 0ustar00miguelmiguelfrom unittest import mock from socketio import middleware class TestMiddleware: def test_wsgi_routing(self): mock_wsgi_app = mock.MagicMock() mock_sio_app = 'foo' m = middleware.Middleware(mock_sio_app, mock_wsgi_app) environ = {'PATH_INFO': '/foo'} start_response = "foo" m(environ, start_response) mock_wsgi_app.assert_called_once_with(environ, start_response) def test_sio_routing(self): mock_wsgi_app = 'foo' mock_sio_app = mock.Mock() mock_sio_app.handle_request = mock.MagicMock() m = middleware.Middleware(mock_sio_app, mock_wsgi_app) environ = {'PATH_INFO': '/socket.io/'} start_response = "foo" m(environ, start_response) mock_sio_app.handle_request.assert_called_once_with( environ, start_response ) def test_404(self): mock_wsgi_app = None mock_sio_app = mock.Mock() m = middleware.Middleware(mock_sio_app, mock_wsgi_app) environ = {'PATH_INFO': '/foo/bar'} start_response = mock.MagicMock() r = m(environ, start_response) assert r == [b'Not Found'] start_response.assert_called_once_with( "404 Not Found", [('Content-Type', 'text/plain')] ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1734189874.0 python_socketio-5.13.0/tests/common/test_msgpack_packet.py0000664000175000017500000000256614727321462023451 0ustar00miguelmiguelfrom socketio import msgpack_packet from socketio import packet class TestMsgPackPacket: def test_encode_decode(self): p = msgpack_packet.MsgPackPacket( packet.CONNECT, data={'auth': {'token': '123'}}, namespace='/foo') p2 = msgpack_packet.MsgPackPacket(encoded_packet=p.encode()) assert p.packet_type == p2.packet_type assert p.data == p2.data assert p.id == p2.id assert p.namespace == p2.namespace def test_encode_decode_with_id(self): p = msgpack_packet.MsgPackPacket( packet.EVENT, data=['ev', 42], id=123, namespace='/foo') p2 = msgpack_packet.MsgPackPacket(encoded_packet=p.encode()) assert p.packet_type == p2.packet_type assert p.data == p2.data assert p.id == p2.id assert p.namespace == p2.namespace def test_encode_binary_event_packet(self): p = msgpack_packet.MsgPackPacket(packet.EVENT, data={'foo': b'bar'}) assert p.packet_type == packet.EVENT p2 = msgpack_packet.MsgPackPacket(encoded_packet=p.encode()) assert p2.data == {'foo': b'bar'} def test_encode_binary_ack_packet(self): p = msgpack_packet.MsgPackPacket(packet.ACK, data={'foo': b'bar'}) assert p.packet_type == packet.ACK p2 = msgpack_packet.MsgPackPacket(encoded_packet=p.encode()) assert p2.data == {'foo': b'bar'} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1734543553.0 python_socketio-5.13.0/tests/common/test_namespace.py0000664000175000017500000002327214730604301022414 0ustar00miguelmiguelfrom unittest import mock from socketio import namespace class TestNamespace: def test_connect_event(self): result = {} class MyNamespace(namespace.Namespace): def on_connect(self, sid, environ): result['result'] = (sid, environ) ns = MyNamespace('/foo') ns._set_server(mock.MagicMock()) ns.trigger_event('connect', 'sid', {'foo': 'bar'}) assert result['result'] == ('sid', {'foo': 'bar'}) def test_disconnect_event(self): result = {} class MyNamespace(namespace.Namespace): def on_disconnect(self, sid, reason): result['result'] = (sid, reason) ns = MyNamespace('/foo') ns._set_server(mock.MagicMock()) ns.trigger_event('disconnect', 'sid', 'foo') assert result['result'] == ('sid', 'foo') def test_legacy_disconnect_event(self): result = {} class MyNamespace(namespace.Namespace): def on_disconnect(self, sid): result['result'] = sid ns = MyNamespace('/foo') ns._set_server(mock.MagicMock()) ns.trigger_event('disconnect', 'sid', 'foo') assert result['result'] == 'sid' def test_event(self): result = {} class MyNamespace(namespace.Namespace): def on_custom_message(self, sid, data): result['result'] = (sid, data) ns = MyNamespace('/foo') ns._set_server(mock.MagicMock()) ns.trigger_event('custom_message', 'sid', {'data': 'data'}) assert result['result'] == ('sid', {'data': 'data'}) def test_event_not_found(self): result = {} class MyNamespace(namespace.Namespace): def on_custom_message(self, sid, data): result['result'] = (sid, data) ns = MyNamespace('/foo') ns._set_server(mock.MagicMock()) ns.trigger_event('another_custom_message', 'sid', {'data': 'data'}) assert result == {} def test_emit(self): ns = namespace.Namespace('/foo') ns._set_server(mock.MagicMock()) ns.emit('ev', data='data', to='room', skip_sid='skip', callback='cb') ns.server.emit.assert_called_with( 'ev', data='data', to='room', room=None, skip_sid='skip', namespace='/foo', callback='cb', ignore_queue=False, ) ns.emit( 'ev', data='data', room='room', skip_sid='skip', namespace='/bar', callback='cb', ignore_queue=True, ) ns.server.emit.assert_called_with( 'ev', data='data', to=None, room='room', skip_sid='skip', namespace='/bar', callback='cb', ignore_queue=True, ) def test_send(self): ns = namespace.Namespace('/foo') ns._set_server(mock.MagicMock()) ns.send(data='data', to='room', skip_sid='skip', callback='cb') ns.server.send.assert_called_with( 'data', to='room', room=None, skip_sid='skip', namespace='/foo', callback='cb', ignore_queue=False, ) ns.send( data='data', room='room', skip_sid='skip', namespace='/bar', callback='cb', ignore_queue=True, ) ns.server.send.assert_called_with( 'data', to=None, room='room', skip_sid='skip', namespace='/bar', callback='cb', ignore_queue=True, ) def test_call(self): ns = namespace.Namespace('/foo') ns._set_server(mock.MagicMock()) ns.call('ev', data='data', to='sid') ns.server.call.assert_called_with( 'ev', data='data', to='sid', sid=None, namespace='/foo', timeout=None, ignore_queue=False, ) ns.call( 'ev', data='data', sid='sid', namespace='/bar', timeout=45, ignore_queue=True, ) ns.server.call.assert_called_with( 'ev', data='data', to=None, sid='sid', namespace='/bar', timeout=45, ignore_queue=True, ) def test_enter_room(self): ns = namespace.Namespace('/foo') ns._set_server(mock.MagicMock()) ns.enter_room('sid', 'room') ns.server.enter_room.assert_called_with( 'sid', 'room', namespace='/foo' ) ns.enter_room('sid', 'room', namespace='/bar') ns.server.enter_room.assert_called_with( 'sid', 'room', namespace='/bar' ) def test_leave_room(self): ns = namespace.Namespace('/foo') ns._set_server(mock.MagicMock()) ns.leave_room('sid', 'room') ns.server.leave_room.assert_called_with( 'sid', 'room', namespace='/foo' ) ns.leave_room('sid', 'room', namespace='/bar') ns.server.leave_room.assert_called_with( 'sid', 'room', namespace='/bar' ) def test_close_room(self): ns = namespace.Namespace('/foo') ns._set_server(mock.MagicMock()) ns.close_room('room') ns.server.close_room.assert_called_with('room', namespace='/foo') ns.close_room('room', namespace='/bar') ns.server.close_room.assert_called_with('room', namespace='/bar') def test_rooms(self): ns = namespace.Namespace('/foo') ns._set_server(mock.MagicMock()) ns.rooms('sid') ns.server.rooms.assert_called_with('sid', namespace='/foo') ns.rooms('sid', namespace='/bar') ns.server.rooms.assert_called_with('sid', namespace='/bar') def test_session(self): ns = namespace.Namespace('/foo') ns._set_server(mock.MagicMock()) ns.get_session('sid') ns.server.get_session.assert_called_with('sid', namespace='/foo') ns.get_session('sid', namespace='/bar') ns.server.get_session.assert_called_with('sid', namespace='/bar') ns.save_session('sid', {'a': 'b'}) ns.server.save_session.assert_called_with( 'sid', {'a': 'b'}, namespace='/foo' ) ns.save_session('sid', {'a': 'b'}, namespace='/bar') ns.server.save_session.assert_called_with( 'sid', {'a': 'b'}, namespace='/bar' ) ns.session('sid') ns.server.session.assert_called_with('sid', namespace='/foo') ns.session('sid', namespace='/bar') ns.server.session.assert_called_with('sid', namespace='/bar') def test_disconnect(self): ns = namespace.Namespace('/foo') ns._set_server(mock.MagicMock()) ns.disconnect('sid') ns.server.disconnect.assert_called_with('sid', namespace='/foo') ns.disconnect('sid', namespace='/bar') ns.server.disconnect.assert_called_with('sid', namespace='/bar') def test_disconnect_event_client(self): result = {} class MyNamespace(namespace.ClientNamespace): def on_disconnect(self, reason): result['result'] = reason ns = MyNamespace('/foo') ns._set_client(mock.MagicMock()) ns.trigger_event('disconnect', 'foo') assert result['result'] == 'foo' def test_legacy_disconnect_event_client(self): result = {} class MyNamespace(namespace.ClientNamespace): def on_disconnect(self): result['result'] = 'ok' ns = MyNamespace('/foo') ns._set_client(mock.MagicMock()) ns.trigger_event('disconnect', 'foo') assert result['result'] == 'ok' def test_event_not_found_client(self): result = {} class MyNamespace(namespace.ClientNamespace): def on_custom_message(self, sid, data): result['result'] = (sid, data) ns = MyNamespace('/foo') ns._set_client(mock.MagicMock()) ns.trigger_event('another_custom_message', 'sid', {'data': 'data'}) assert result == {} def test_emit_client(self): ns = namespace.ClientNamespace('/foo') ns._set_client(mock.MagicMock()) ns.emit('ev', data='data', callback='cb') ns.client.emit.assert_called_with( 'ev', data='data', namespace='/foo', callback='cb' ) ns.emit('ev', data='data', namespace='/bar', callback='cb') ns.client.emit.assert_called_with( 'ev', data='data', namespace='/bar', callback='cb' ) def test_send_client(self): ns = namespace.ClientNamespace('/foo') ns._set_client(mock.MagicMock()) ns.send(data='data', callback='cb') ns.client.send.assert_called_with( 'data', namespace='/foo', callback='cb' ) ns.send(data='data', namespace='/bar', callback='cb') ns.client.send.assert_called_with( 'data', namespace='/bar', callback='cb' ) def test_call_client(self): ns = namespace.ClientNamespace('/foo') ns._set_client(mock.MagicMock()) ns.call('ev', data='data') ns.client.call.assert_called_with( 'ev', data='data', namespace='/foo', timeout=None) ns.call('ev', data='data', namespace='/bar', timeout=45) ns.client.call.assert_called_with( 'ev', data='data', namespace='/bar', timeout=45 ) def test_disconnect_client(self): ns = namespace.ClientNamespace('/foo') ns._set_client(mock.MagicMock()) ns.disconnect() ns.client.disconnect.assert_called_with() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1734189874.0 python_socketio-5.13.0/tests/common/test_packet.py0000664000175000017500000002342614727321462021742 0ustar00miguelmiguelimport pytest from socketio import packet class TestPacket: def test_encode_default_packet(self): pkt = packet.Packet() assert pkt.packet_type == packet.EVENT assert pkt.data is None assert pkt.namespace is None assert pkt.id is None assert pkt.attachment_count == 0 assert pkt.encode() == '2' def test_decode_default_packet(self): pkt = packet.Packet(encoded_packet='2') assert pkt.encode(), '2' def test_encode_text_event_packet(self): pkt = packet.Packet( packet_type=packet.EVENT, data=['foo'] ) assert pkt.packet_type == packet.EVENT assert pkt.data == ['foo'] assert pkt.encode() == '2["foo"]' def test_decode_text_event_packet(self): pkt = packet.Packet(encoded_packet='2["foo"]') assert pkt.packet_type == packet.EVENT assert pkt.data == ['foo'] assert pkt.encode() == '2["foo"]' def test_decode_empty_event_packet(self): pkt = packet.Packet(encoded_packet='1') assert pkt.packet_type == packet.DISCONNECT # same thing, but with a numeric payload pkt = packet.Packet(encoded_packet=1) assert pkt.packet_type == packet.DISCONNECT def test_encode_binary_event_packet(self): pkt = packet.Packet(packet_type=packet.EVENT, data=b'1234') assert pkt.packet_type == packet.BINARY_EVENT assert pkt.data == b'1234' a = ['51-{"_placeholder":true,"num":0}', b'1234'] b = ['51-{"num":0,"_placeholder":true}', b'1234'] encoded_packet = pkt.encode() assert encoded_packet == a or encoded_packet == b def test_decode_binary_event_packet(self): pkt = packet.Packet(encoded_packet='51-{"_placeholder":true,"num":0}') assert pkt.add_attachment(b'1234') assert pkt.packet_type == packet.BINARY_EVENT assert pkt.data == b'1234' def test_encode_text_ack_packet(self): pkt = packet.Packet( packet_type=packet.ACK, data=['foo'] ) assert pkt.packet_type == packet.ACK assert pkt.data == ['foo'] assert pkt.encode() == '3["foo"]' def test_decode_text_ack_packet(self): pkt = packet.Packet(encoded_packet='3["foo"]') assert pkt.packet_type == packet.ACK assert pkt.data == ['foo'] assert pkt.encode() == '3["foo"]' def test_encode_binary_ack_packet(self): pkt = packet.Packet(packet_type=packet.ACK, data=b'1234') assert pkt.packet_type == packet.BINARY_ACK assert pkt.data == b'1234' a = ['61-{"_placeholder":true,"num":0}', b'1234'] b = ['61-{"num":0,"_placeholder":true}', b'1234'] encoded_packet = pkt.encode() assert encoded_packet == a or encoded_packet == b def test_decode_binary_ack_packet(self): pkt = packet.Packet(encoded_packet='61-{"_placeholder":true,"num":0}') assert pkt.add_attachment(b'1234') assert pkt.packet_type == packet.BINARY_ACK assert pkt.data == b'1234' def test_invalid_binary_packet(self): with pytest.raises(ValueError): packet.Packet(packet_type=packet.CONNECT_ERROR, data=b'123') def test_encode_namespace(self): pkt = packet.Packet( packet_type=packet.EVENT, data=['foo'], namespace='/bar', ) assert pkt.namespace == '/bar' assert pkt.encode() == '2/bar,["foo"]' def test_decode_namespace(self): pkt = packet.Packet(encoded_packet='2/bar,["foo"]') assert pkt.namespace == '/bar' assert pkt.encode() == '2/bar,["foo"]' def test_decode_namespace_with_query_string(self): # some Socket.IO clients mistakenly attach the query string to the # namespace pkt = packet.Packet(encoded_packet='2/bar?a=b,["foo"]') assert pkt.namespace == '/bar' assert pkt.encode() == '2/bar,["foo"]' def test_encode_namespace_no_data(self): pkt = packet.Packet(packet_type=packet.EVENT, namespace='/bar') assert pkt.encode() == '2/bar,' def test_decode_namespace_no_data(self): pkt = packet.Packet(encoded_packet='2/bar,') assert pkt.namespace == '/bar' assert pkt.data is None assert pkt.encode() == '2/bar,' def test_encode_namespace_with_hyphens(self): pkt = packet.Packet( packet_type=packet.EVENT, data=['foo'], namespace='/b-a-r', ) assert pkt.namespace == '/b-a-r' assert pkt.encode() == '2/b-a-r,["foo"]' def test_decode_namespace_with_hyphens(self): pkt = packet.Packet(encoded_packet='2/b-a-r,["foo"]') assert pkt.namespace == '/b-a-r' assert pkt.encode() == '2/b-a-r,["foo"]' def test_encode_event_with_hyphens(self): pkt = packet.Packet( packet_type=packet.EVENT, data=['f-o-o'] ) assert pkt.namespace is None assert pkt.encode() == '2["f-o-o"]' def test_decode_event_with_hyphens(self): pkt = packet.Packet(encoded_packet='2["f-o-o"]') assert pkt.namespace is None assert pkt.encode() == '2["f-o-o"]' def test_encode_id(self): pkt = packet.Packet( packet_type=packet.EVENT, data=['foo'], id=123 ) assert pkt.id == 123 assert pkt.encode() == '2123["foo"]' def test_decode_id(self): pkt = packet.Packet(encoded_packet='2123["foo"]') assert pkt.id == 123 assert pkt.encode() == '2123["foo"]' def test_decode_id_long(self): pkt = packet.Packet(encoded_packet='2' + '1' * 100 + '["foo"]') assert pkt.id == int('1' * 100) assert pkt.data == ['foo'] def test_decode_id_too_long(self): with pytest.raises(ValueError): packet.Packet(encoded_packet='2' + '1' * 101) with pytest.raises(ValueError): packet.Packet(encoded_packet='2' + '1' * 101 + '["foo"]') def test_encode_id_no_data(self): pkt = packet.Packet(packet_type=packet.EVENT, id=123) assert pkt.id == 123 assert pkt.data is None assert pkt.encode() == '2123' def test_decode_id_no_data(self): pkt = packet.Packet(encoded_packet='2123') assert pkt.id == 123 assert pkt.data is None assert pkt.encode() == '2123' def test_encode_namespace_and_id(self): pkt = packet.Packet( packet_type=packet.EVENT, data=['foo'], namespace='/bar', id=123, ) assert pkt.namespace == '/bar' assert pkt.id == 123 assert pkt.encode() == '2/bar,123["foo"]' def test_decode_namespace_and_id(self): pkt = packet.Packet(encoded_packet='2/bar,123["foo"]') assert pkt.namespace == '/bar' assert pkt.id == 123 assert pkt.encode() == '2/bar,123["foo"]' def test_encode_many_binary(self): pkt = packet.Packet( packet_type=packet.EVENT, data={'a': '123', 'b': b'456', 'c': [b'789', 123]}, ) assert pkt.packet_type == packet.BINARY_EVENT ep = pkt.encode() assert len(ep) == 3 assert b'456' in ep assert b'789' in ep def test_encode_many_binary_ack(self): pkt = packet.Packet( packet_type=packet.ACK, data={'a': '123', 'b': b'456', 'c': [b'789', 123]}, ) assert pkt.packet_type == packet.BINARY_ACK ep = pkt.encode() assert len(ep) == 3 assert b'456' in ep assert b'789' in ep def test_decode_many_binary(self): pkt = packet.Packet( encoded_packet=( '52-{"a":"123","b":{"_placeholder":true,"num":0},' '"c":[{"_placeholder":true,"num":1},123]}' ) ) assert not pkt.add_attachment(b'456') assert pkt.add_attachment(b'789') assert pkt.packet_type == packet.BINARY_EVENT assert pkt.data['a'] == '123' assert pkt.data['b'] == b'456' assert pkt.data['c'] == [b'789', 123] def test_decode_many_binary_ack(self): pkt = packet.Packet( encoded_packet=( '62-{"a":"123","b":{"_placeholder":true,"num":0},' '"c":[{"_placeholder":true,"num":1},123]}' ) ) assert not pkt.add_attachment(b'456') assert pkt.add_attachment(b'789') assert pkt.packet_type == packet.BINARY_ACK assert pkt.data['a'] == '123' assert pkt.data['b'] == b'456' assert pkt.data['c'] == [b'789', 123] def test_decode_too_many_binary_packets(self): pkt = packet.Packet( encoded_packet=( '62-{"a":"123","b":{"_placeholder":true,"num":0},' '"c":[{"_placeholder":true,"num":1},123]}' ) ) assert not pkt.add_attachment(b'456') assert pkt.add_attachment(b'789') with pytest.raises(ValueError): pkt.add_attachment(b'123') def test_decode_attachment_count_too_long(self): with pytest.raises(ValueError): packet.Packet(encoded_packet='6' + ('1' * 11) + '-{"a":"123"}') def test_decode_dash_in_payload(self): pkt = packet.Packet(encoded_packet='6{"a":"0123456789-"}') assert pkt.data["a"] == "0123456789-" assert pkt.attachment_count == 0 def test_data_is_binary_list(self): pkt = packet.Packet() assert not pkt._data_is_binary(['foo']) assert not pkt._data_is_binary([]) assert pkt._data_is_binary([b'foo']) assert pkt._data_is_binary(['foo', b'bar']) def test_data_is_binary_dict(self): pkt = packet.Packet() assert not pkt._data_is_binary({'a': 'foo'}) assert not pkt._data_is_binary({}) assert pkt._data_is_binary({'a': b'foo'}) assert pkt._data_is_binary({'a': 'foo', 'b': b'bar'}) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1734189874.0 python_socketio-5.13.0/tests/common/test_pubsub_manager.py0000664000175000017500000004622514727321462023467 0ustar00miguelmiguelimport functools import logging from unittest import mock import pytest from socketio import manager from socketio import pubsub_manager from socketio import packet class TestPubSubManager: def setup_method(self): id = 0 def generate_id(): nonlocal id id += 1 return str(id) mock_server = mock.MagicMock() mock_server.eio.generate_id = generate_id mock_server.packet_class = packet.Packet self.pm = pubsub_manager.PubSubManager() self.pm._publish = mock.MagicMock() self.pm.set_server(mock_server) self.pm.host_id = '123456' self.pm.initialize() def test_default_init(self): assert self.pm.channel == 'socketio' self.pm.server.start_background_task.assert_called_once_with( self.pm._thread ) def test_custom_init(self): pubsub = pubsub_manager.PubSubManager(channel='foo') assert pubsub.channel == 'foo' assert len(pubsub.host_id) == 32 def test_write_only_init(self): mock_server = mock.MagicMock() pm = pubsub_manager.PubSubManager(write_only=True) pm.set_server(mock_server) pm.initialize() assert pm.channel == 'socketio' assert len(pm.host_id) == 32 assert pm.server.start_background_task.call_count == 0 def test_write_only_default_logger(self): pm = pubsub_manager.PubSubManager(write_only=True) pm.initialize() assert pm.channel == 'socketio' assert len(pm.host_id) == 32 assert pm._get_logger() == logging.getLogger('socketio') def test_write_only_with_provided_logger(self): test_logger = logging.getLogger('new_logger') pm = pubsub_manager.PubSubManager(write_only=True, logger=test_logger) pm.initialize() assert pm.channel == 'socketio' assert len(pm.host_id) == 32 assert pm._get_logger() == test_logger def test_emit(self): self.pm.emit('foo', 'bar') self.pm._publish.assert_called_once_with( { 'method': 'emit', 'event': 'foo', 'data': 'bar', 'namespace': '/', 'room': None, 'skip_sid': None, 'callback': None, 'host_id': '123456', } ) def test_emit_with_to(self): sid = "ferris" self.pm.emit('foo', 'bar', to=sid) self.pm._publish.assert_called_once_with( { 'method': 'emit', 'event': 'foo', 'data': 'bar', 'namespace': '/', 'room': sid, 'skip_sid': None, 'callback': None, 'host_id': '123456', } ) def test_emit_with_namespace(self): self.pm.emit('foo', 'bar', namespace='/baz') self.pm._publish.assert_called_once_with( { 'method': 'emit', 'event': 'foo', 'data': 'bar', 'namespace': '/baz', 'room': None, 'skip_sid': None, 'callback': None, 'host_id': '123456', } ) def test_emit_with_room(self): self.pm.emit('foo', 'bar', room='baz') self.pm._publish.assert_called_once_with( { 'method': 'emit', 'event': 'foo', 'data': 'bar', 'namespace': '/', 'room': 'baz', 'skip_sid': None, 'callback': None, 'host_id': '123456', } ) def test_emit_with_skip_sid(self): self.pm.emit('foo', 'bar', skip_sid='baz') self.pm._publish.assert_called_once_with( { 'method': 'emit', 'event': 'foo', 'data': 'bar', 'namespace': '/', 'room': None, 'skip_sid': 'baz', 'callback': None, 'host_id': '123456', } ) def test_emit_with_callback(self): with mock.patch.object( self.pm, '_generate_ack_id', return_value='123' ): self.pm.emit('foo', 'bar', room='baz', callback='cb') self.pm._publish.assert_called_once_with( { 'method': 'emit', 'event': 'foo', 'data': 'bar', 'namespace': '/', 'room': 'baz', 'skip_sid': None, 'callback': ('baz', '/', '123'), 'host_id': '123456', } ) def test_emit_with_callback_without_server(self): standalone_pm = pubsub_manager.PubSubManager() with pytest.raises(RuntimeError): standalone_pm.emit('foo', 'bar', callback='cb') def test_emit_with_callback_missing_room(self): with mock.patch.object( self.pm, '_generate_ack_id', return_value='123' ): with pytest.raises(ValueError): self.pm.emit('foo', 'bar', callback='cb') def test_emit_with_ignore_queue(self): sid = self.pm.connect('123', '/') self.pm.emit( 'foo', 'bar', room=sid, namespace='/', ignore_queue=True ) self.pm._publish.assert_not_called() assert self.pm.server._send_eio_packet.call_count == 1 assert self.pm.server._send_eio_packet.call_args_list[0][0][0] == '123' pkt = self.pm.server._send_eio_packet.call_args_list[0][0][1] assert pkt.encode() == '42["foo","bar"]' def test_can_disconnect(self): sid = self.pm.connect('123', '/') assert self.pm.can_disconnect(sid, '/') self.pm.can_disconnect(sid, '/foo') self.pm._publish.assert_called_once_with( {'method': 'disconnect', 'sid': sid, 'namespace': '/foo', 'host_id': '123456'} ) def test_disconnect(self): self.pm.disconnect('foo') self.pm._publish.assert_called_once_with( {'method': 'disconnect', 'sid': 'foo', 'namespace': '/', 'host_id': '123456'} ) def test_disconnect_ignore_queue(self): sid = self.pm.connect('123', '/') self.pm.pre_disconnect(sid, '/') self.pm.disconnect(sid, ignore_queue=True) self.pm._publish.assert_not_called() assert not self.pm.is_connected(sid, '/') def test_enter_room(self): sid = self.pm.connect('123', '/') self.pm.enter_room(sid, '/', 'foo') self.pm.enter_room('456', '/', 'foo') assert sid in self.pm.rooms['/']['foo'] assert self.pm.rooms['/']['foo'][sid] == '123' self.pm._publish.assert_called_once_with( {'method': 'enter_room', 'sid': '456', 'room': 'foo', 'namespace': '/', 'host_id': '123456'} ) def test_leave_room(self): sid = self.pm.connect('123', '/') self.pm.leave_room(sid, '/', 'foo') self.pm.leave_room('456', '/', 'foo') assert 'foo' not in self.pm.rooms['/'] self.pm._publish.assert_called_once_with( {'method': 'leave_room', 'sid': '456', 'room': 'foo', 'namespace': '/', 'host_id': '123456'} ) def test_close_room(self): self.pm.close_room('foo') self.pm._publish.assert_called_once_with( {'method': 'close_room', 'room': 'foo', 'namespace': '/', 'host_id': '123456'} ) def test_close_room_with_namespace(self): self.pm.close_room('foo', '/bar') self.pm._publish.assert_called_once_with( {'method': 'close_room', 'room': 'foo', 'namespace': '/bar', 'host_id': '123456'} ) def test_handle_emit(self): with mock.patch.object(manager.Manager, 'emit') as super_emit: self.pm._handle_emit({'event': 'foo', 'data': 'bar'}) super_emit.assert_called_once_with( 'foo', 'bar', namespace=None, room=None, skip_sid=None, callback=None, ) def test_handle_emit_with_namespace(self): with mock.patch.object(manager.Manager, 'emit') as super_emit: self.pm._handle_emit( {'event': 'foo', 'data': 'bar', 'namespace': '/baz'} ) super_emit.assert_called_once_with( 'foo', 'bar', namespace='/baz', room=None, skip_sid=None, callback=None, ) def test_handle_emit_with_room(self): with mock.patch.object(manager.Manager, 'emit') as super_emit: self.pm._handle_emit( {'event': 'foo', 'data': 'bar', 'room': 'baz'} ) super_emit.assert_called_once_with( 'foo', 'bar', namespace=None, room='baz', skip_sid=None, callback=None, ) def test_handle_emit_with_skip_sid(self): with mock.patch.object(manager.Manager, 'emit') as super_emit: self.pm._handle_emit( {'event': 'foo', 'data': 'bar', 'skip_sid': '123'} ) super_emit.assert_called_once_with( 'foo', 'bar', namespace=None, room=None, skip_sid='123', callback=None, ) def test_handle_emit_with_remote_callback(self): with mock.patch.object(manager.Manager, 'emit') as super_emit: self.pm._handle_emit( { 'event': 'foo', 'data': 'bar', 'namespace': '/baz', 'callback': ('sid', '/baz', 123), 'host_id': 'x', } ) assert super_emit.call_count == 1 assert super_emit.call_args[0] == ('foo', 'bar') assert super_emit.call_args[1]['namespace'] == '/baz' assert super_emit.call_args[1]['room'] is None assert super_emit.call_args[1]['skip_sid'] is None assert isinstance( super_emit.call_args[1]['callback'], functools.partial ) super_emit.call_args[1]['callback']('one', 2, 'three') self.pm._publish.assert_called_once_with( { 'method': 'callback', 'host_id': 'x', 'sid': 'sid', 'namespace': '/baz', 'id': 123, 'args': ('one', 2, 'three'), } ) def test_handle_emit_with_local_callback(self): with mock.patch.object(manager.Manager, 'emit') as super_emit: self.pm._handle_emit( { 'event': 'foo', 'data': 'bar', 'namespace': '/baz', 'callback': ('sid', '/baz', 123), 'host_id': self.pm.host_id, } ) assert super_emit.call_count == 1 assert super_emit.call_args[0] == ('foo', 'bar') assert super_emit.call_args[1]['namespace'] == '/baz' assert super_emit.call_args[1]['room'] is None assert super_emit.call_args[1]['skip_sid'] is None assert isinstance( super_emit.call_args[1]['callback'], functools.partial ) super_emit.call_args[1]['callback']('one', 2, 'three') self.pm._publish.assert_not_called() def test_handle_callback(self): host_id = self.pm.host_id with mock.patch.object(self.pm, 'trigger_callback') as trigger: self.pm._handle_callback( { 'method': 'callback', 'host_id': host_id, 'sid': 'sid', 'namespace': '/', 'id': 123, 'args': ('one', 2), } ) trigger.assert_called_once_with('sid', 123, ('one', 2)) def test_handle_callback_bad_host_id(self): with mock.patch.object(self.pm, 'trigger_callback') as trigger: self.pm._handle_callback( { 'method': 'callback', 'host_id': 'bad', 'sid': 'sid', 'namespace': '/', 'id': 123, 'args': ('one', 2), } ) assert trigger.call_count == 0 def test_handle_callback_missing_args(self): host_id = self.pm.host_id with mock.patch.object(self.pm, 'trigger_callback') as trigger: self.pm._handle_callback( { 'method': 'callback', 'host_id': host_id, 'sid': 'sid', 'namespace': '/', 'id': 123, } ) self.pm._handle_callback( { 'method': 'callback', 'host_id': host_id, 'sid': 'sid', 'namespace': '/', } ) self.pm._handle_callback( {'method': 'callback', 'host_id': host_id, 'sid': 'sid'} ) self.pm._handle_callback( {'method': 'callback', 'host_id': host_id} ) assert trigger.call_count == 0 def test_handle_disconnect(self): self.pm._handle_disconnect( {'method': 'disconnect', 'sid': '123', 'namespace': '/foo'} ) self.pm.server.disconnect.assert_called_once_with( sid='123', namespace='/foo', ignore_queue=True ) def test_handle_enter_room(self): sid = self.pm.connect('123', '/') with mock.patch.object( manager.Manager, 'enter_room' ) as super_enter_room: self.pm._handle_enter_room({ 'method': 'enter_room', 'sid': sid, 'namespace': '/', 'room': 'foo'}) self.pm._handle_enter_room({ 'method': 'enter_room', 'sid': '456', 'namespace': '/', 'room': 'foo'}) super_enter_room.assert_called_once_with(sid, '/', 'foo') def test_handle_leave_room(self): sid = self.pm.connect('123', '/') with mock.patch.object( manager.Manager, 'leave_room' ) as super_leave_room: self.pm._handle_leave_room({ 'method': 'leave_room', 'sid': sid, 'namespace': '/', 'room': 'foo'}) self.pm._handle_leave_room({ 'method': 'leave_room', 'sid': '456', 'namespace': '/', 'room': 'foo'}) super_leave_room.assert_called_once_with(sid, '/', 'foo') def test_handle_close_room(self): with mock.patch.object( manager.Manager, 'close_room' ) as super_close_room: self.pm._handle_close_room({'method': 'close_room', 'room': 'foo'}) super_close_room.assert_called_once_with( room='foo', namespace=None ) def test_handle_close_room_with_namespace(self): with mock.patch.object( manager.Manager, 'close_room' ) as super_close_room: self.pm._handle_close_room( {'method': 'close_room', 'room': 'foo', 'namespace': '/bar'} ) super_close_room.assert_called_once_with( room='foo', namespace='/bar' ) def test_background_thread(self): self.pm._handle_emit = mock.MagicMock() self.pm._handle_callback = mock.MagicMock() self.pm._handle_disconnect = mock.MagicMock() self.pm._handle_enter_room = mock.MagicMock() self.pm._handle_leave_room = mock.MagicMock() self.pm._handle_close_room = mock.MagicMock() host_id = self.pm.host_id def messages(): import pickle yield {'method': 'emit', 'value': 'foo', 'host_id': 'x'} yield {'missing': 'method', 'host_id': 'x'} yield '{"method": "callback", "value": "bar", "host_id": "x"}' yield {'method': 'disconnect', 'sid': '123', 'namespace': '/foo', 'host_id': 'x'} yield {'method': 'bogus', 'host_id': 'x'} yield pickle.dumps({'method': 'close_room', 'value': 'baz', 'host_id': 'x'}) yield {'method': 'enter_room', 'sid': '123', 'namespace': '/foo', 'room': 'room', 'host_id': 'x'} yield {'method': 'leave_room', 'sid': '123', 'namespace': '/foo', 'room': 'room', 'host_id': 'x'} yield 'bad json' yield b'bad pickled' # these should not publish anything on the queue, as they come from # the same host yield {'method': 'emit', 'value': 'foo', 'host_id': host_id} yield {'method': 'callback', 'value': 'bar', 'host_id': host_id} yield {'method': 'disconnect', 'sid': '123', 'namespace': '/foo', 'host_id': host_id} yield pickle.dumps({'method': 'close_room', 'value': 'baz', 'host_id': host_id}) self.pm._listen = mock.MagicMock(side_effect=messages) try: self.pm._thread() except StopIteration: pass self.pm._handle_emit.assert_called_once_with( {'method': 'emit', 'value': 'foo', 'host_id': 'x'} ) self.pm._handle_callback.assert_any_call( {'method': 'callback', 'value': 'bar', 'host_id': 'x'} ) self.pm._handle_callback.assert_any_call( {'method': 'callback', 'value': 'bar', 'host_id': host_id} ) self.pm._handle_disconnect.assert_called_once_with( {'method': 'disconnect', 'sid': '123', 'namespace': '/foo', 'host_id': 'x'} ) self.pm._handle_enter_room.assert_called_once_with( {'method': 'enter_room', 'sid': '123', 'namespace': '/foo', 'room': 'room', 'host_id': 'x'} ) self.pm._handle_leave_room.assert_called_once_with( {'method': 'leave_room', 'sid': '123', 'namespace': '/foo', 'room': 'room', 'host_id': 'x'} ) self.pm._handle_close_room.assert_called_once_with( {'method': 'close_room', 'value': 'baz', 'host_id': 'x'} ) def test_background_thread_exception(self): self.pm._handle_emit = mock.MagicMock(side_effect=[ValueError(), None]) def messages(): yield {'method': 'emit', 'value': 'foo', 'host_id': 'x'} yield {'method': 'emit', 'value': 'bar', 'host_id': 'x'} self.pm._listen = mock.MagicMock(side_effect=messages) try: self.pm._thread() except StopIteration: pass self.pm._handle_emit.assert_any_call( {'method': 'emit', 'value': 'foo', 'host_id': 'x'} ) self.pm._handle_emit.assert_called_with( {'method': 'emit', 'value': 'bar', 'host_id': 'x'} ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744472116.0 python_socketio-5.13.0/tests/common/test_redis_manager.py0000664000175000017500000000216414776504064023274 0ustar00miguelmiguelimport pytest from socketio.redis_manager import parse_redis_sentinel_url class TestPubSubManager: def test_sentinel_url_parser(self): with pytest.raises(ValueError): parse_redis_sentinel_url('redis://localhost:6379/0') assert parse_redis_sentinel_url( 'redis+sentinel://localhost:6379' ) == ( [('localhost', 6379)], None, {} ) assert parse_redis_sentinel_url( 'redis+sentinel://192.168.0.1:6379,192.168.0.2:6379/' ) == ( [('192.168.0.1', 6379), ('192.168.0.2', 6379)], None, {} ) assert parse_redis_sentinel_url( 'redis+sentinel://h1:6379,h2:6379/0' ) == ( [('h1', 6379), ('h2', 6379)], None, {'db': 0} ) assert parse_redis_sentinel_url( 'redis+sentinel://user:password@h1:6379,h2:6379,h1:6380/0/myredis' ) == ( [('h1', 6379), ('h2', 6379), ('h1', 6380)], 'myredis', {'username': 'user', 'password': 'password', 'db': 0} ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1734543553.0 python_socketio-5.13.0/tests/common/test_server.py0000664000175000017500000011307114730604301021763 0ustar00miguelmiguelimport logging from unittest import mock from engineio import json from engineio import packet as eio_packet import pytest from socketio import exceptions from socketio import msgpack_packet from socketio import namespace from socketio import packet from socketio import server @mock.patch('socketio.server.engineio.Server', **{ 'return_value.generate_id.side_effect': [str(i) for i in range(1, 10)]}) class TestServer: def teardown_method(self): # restore JSON encoder, in case a test changed it packet.Packet.json = json def test_create(self, eio): mgr = mock.MagicMock() s = server.Server( client_manager=mgr, async_handlers=True, foo='bar' ) s.handle_request({}, None) s.handle_request({}, None) eio.assert_called_once_with(**{'foo': 'bar', 'async_handlers': False}) assert s.manager == mgr assert s.eio.on.call_count == 3 assert s.async_handlers assert s.packet_class == packet.Packet def test_on_event(self, eio): s = server.Server() @s.on('connect') def foo(): pass def bar(reason): pass s.on('disconnect', bar) s.on('disconnect', bar, namespace='/foo') assert s.handlers['/']['connect'] == foo assert s.handlers['/']['disconnect'] == bar assert s.handlers['/foo']['disconnect'] == bar def test_event(self, eio): s = server.Server() @s.event def connect(): pass @s.event def foo(): pass @s.event() def bar(): pass @s.event(namespace='/foo') def disconnect(): pass assert s.handlers['/']['connect'] == connect assert s.handlers['/']['foo'] == foo assert s.handlers['/']['bar'] == bar assert s.handlers['/foo']['disconnect'] == disconnect def test_emit(self, eio): mgr = mock.MagicMock() s = server.Server(client_manager=mgr) s.emit( 'my event', {'foo': 'bar'}, to='room', skip_sid='123', namespace='/foo', callback='cb', ) s.manager.emit.assert_called_once_with( 'my event', {'foo': 'bar'}, '/foo', room='room', skip_sid='123', callback='cb', ignore_queue=False, ) s.emit( 'my event', {'foo': 'bar'}, room='room', skip_sid='123', namespace='/foo', callback='cb', ignore_queue=True, ) s.manager.emit.assert_called_with( 'my event', {'foo': 'bar'}, '/foo', room='room', skip_sid='123', callback='cb', ignore_queue=True, ) def test_emit_default_namespace(self, eio): mgr = mock.MagicMock() s = server.Server(client_manager=mgr) s.emit( 'my event', {'foo': 'bar'}, to='room', skip_sid='123', callback='cb', ) s.manager.emit.assert_called_once_with( 'my event', {'foo': 'bar'}, '/', room='room', skip_sid='123', callback='cb', ignore_queue=False, ) s.emit( 'my event', {'foo': 'bar'}, room='room', skip_sid='123', callback='cb', ignore_queue=True, ) s.manager.emit.assert_called_with( 'my event', {'foo': 'bar'}, '/', room='room', skip_sid='123', callback='cb', ignore_queue=True, ) def test_send(self, eio): mgr = mock.MagicMock() s = server.Server(client_manager=mgr) s.send( 'foo', to='room', skip_sid='123', namespace='/foo', callback='cb' ) s.manager.emit.assert_called_once_with( 'message', 'foo', '/foo', room='room', skip_sid='123', callback='cb', ignore_queue=False, ) s.send( 'foo', room='room', skip_sid='123', namespace='/foo', callback='cb', ignore_queue=True, ) s.manager.emit.assert_called_with( 'message', 'foo', '/foo', room='room', skip_sid='123', callback='cb', ignore_queue=True, ) def test_call(self, eio): mgr = mock.MagicMock() s = server.Server(client_manager=mgr) def fake_event_wait(timeout=None): assert timeout == 60 s.manager.emit.call_args_list[0][1]['callback']('foo', 321) return True s.eio.create_event.return_value.wait = fake_event_wait assert s.call('foo', sid='123') == ('foo', 321) def test_call_with_timeout(self, eio): mgr = mock.MagicMock() s = server.Server(client_manager=mgr) def fake_event_wait(timeout=None): assert timeout == 12 return False s.eio.create_event.return_value.wait = fake_event_wait with pytest.raises(exceptions.TimeoutError): s.call('foo', sid='123', timeout=12) def test_call_with_broadcast(self, eio): s = server.Server() with pytest.raises(ValueError): s.call('foo') def test_call_without_async_handlers(self, eio): mgr = mock.MagicMock() s = server.Server(client_manager=mgr, async_handlers=False) with pytest.raises(RuntimeError): s.call('foo', sid='123', timeout=12) def test_enter_room(self, eio): mgr = mock.MagicMock() s = server.Server(client_manager=mgr) s.enter_room('123', 'room', namespace='/foo') s.manager.enter_room.assert_called_once_with('123', '/foo', 'room') def test_enter_room_default_namespace(self, eio): mgr = mock.MagicMock() s = server.Server(client_manager=mgr) s.enter_room('123', 'room') s.manager.enter_room.assert_called_once_with('123', '/', 'room') def test_leave_room(self, eio): mgr = mock.MagicMock() s = server.Server(client_manager=mgr) s.leave_room('123', 'room', namespace='/foo') s.manager.leave_room.assert_called_once_with('123', '/foo', 'room') def test_leave_room_default_namespace(self, eio): mgr = mock.MagicMock() s = server.Server(client_manager=mgr) s.leave_room('123', 'room') s.manager.leave_room.assert_called_once_with('123', '/', 'room') def test_close_room(self, eio): mgr = mock.MagicMock() s = server.Server(client_manager=mgr) s.close_room('room', namespace='/foo') s.manager.close_room.assert_called_once_with('room', '/foo') def test_close_room_default_namespace(self, eio): mgr = mock.MagicMock() s = server.Server(client_manager=mgr) s.close_room('room') s.manager.close_room.assert_called_once_with('room', '/') def test_rooms(self, eio): mgr = mock.MagicMock() s = server.Server(client_manager=mgr) s.rooms('123', namespace='/foo') s.manager.get_rooms.assert_called_once_with('123', '/foo') def test_rooms_default_namespace(self, eio): mgr = mock.MagicMock() s = server.Server(client_manager=mgr) s.rooms('123') s.manager.get_rooms.assert_called_once_with('123', '/') def test_handle_request(self, eio): s = server.Server() s.handle_request('environ', 'start_response') s.eio.handle_request.assert_called_once_with( 'environ', 'start_response' ) def test_send_packet(self, eio): s = server.Server() s._send_packet('123', packet.Packet( packet.EVENT, ['my event', 'my data'], namespace='/foo')) s.eio.send.assert_called_once_with( '123', '2/foo,["my event","my data"]' ) def test_send_eio_packet(self, eio): s = server.Server() s._send_eio_packet('123', eio_packet.Packet( eio_packet.MESSAGE, 'hello')) assert s.eio.send_packet.call_count == 1 assert s.eio.send_packet.call_args_list[0][0][0] == '123' pkt = s.eio.send_packet.call_args_list[0][0][1] assert pkt.encode() == '4hello' def test_transport(self, eio): s = server.Server() s.eio.transport = mock.MagicMock(return_value='polling') sid_foo = s.manager.connect('123', '/foo') assert s.transport(sid_foo, '/foo') == 'polling' s.eio.transport.assert_called_once_with('123') def test_handle_connect(self, eio): s = server.Server() s.manager.initialize = mock.MagicMock() handler = mock.MagicMock() s.on('connect', handler) s._handle_eio_connect('123', 'environ') s._handle_eio_message('123', '0') assert s.manager.is_connected('1', '/') handler.assert_called_once_with('1', 'environ') s.eio.send.assert_called_once_with('123', '0{"sid":"1"}') assert s.manager.initialize.call_count == 1 s._handle_eio_connect('456', 'environ') assert s.manager.initialize.call_count == 1 def test_handle_connect_with_auth(self, eio): s = server.Server() s.manager.initialize = mock.MagicMock() handler = mock.MagicMock() s.on('connect', handler) s._handle_eio_connect('123', 'environ') s._handle_eio_message('123', '0{"token":"abc"}') assert s.manager.is_connected('1', '/') handler.assert_called_with('1', 'environ', {'token': 'abc'}) s.eio.send.assert_called_once_with('123', '0{"sid":"1"}') assert s.manager.initialize.call_count == 1 s._handle_eio_connect('456', 'environ') assert s.manager.initialize.call_count == 1 def test_handle_connect_with_auth_none(self, eio): s = server.Server() s.manager.initialize = mock.MagicMock() handler = mock.MagicMock(side_effect=[TypeError, None]) s.on('connect', handler) s._handle_eio_connect('123', 'environ') s._handle_eio_message('123', '0') assert s.manager.is_connected('1', '/') handler.assert_called_with('1', 'environ', None) s.eio.send.assert_called_once_with('123', '0{"sid":"1"}') assert s.manager.initialize.call_count == 1 s._handle_eio_connect('456', 'environ') assert s.manager.initialize.call_count == 1 def test_handle_connect_with_default_implied_namespaces(self, eio): s = server.Server() s._handle_eio_connect('123', 'environ') s._handle_eio_message('123', '0') s._handle_eio_message('123', '0/foo,') assert s.manager.is_connected('1', '/') assert not s.manager.is_connected('2', '/foo') def test_handle_connect_with_implied_namespaces(self, eio): s = server.Server(namespaces=['/foo']) s._handle_eio_connect('123', 'environ') s._handle_eio_message('123', '0') s._handle_eio_message('123', '0/foo,') assert not s.manager.is_connected('1', '/') assert s.manager.is_connected('1', '/foo') def test_handle_connect_with_all_implied_namespaces(self, eio): s = server.Server(namespaces='*') s._handle_eio_connect('123', 'environ') s._handle_eio_message('123', '0') s._handle_eio_message('123', '0/foo,') assert s.manager.is_connected('1', '/') assert s.manager.is_connected('2', '/foo') def test_handle_connect_namespace(self, eio): s = server.Server() handler = mock.MagicMock() s.on('connect', handler, namespace='/foo') s._handle_eio_connect('123', 'environ') s._handle_eio_message('123', '0/foo,') assert s.manager.is_connected('1', '/foo') handler.assert_called_once_with('1', 'environ') s.eio.send.assert_called_once_with('123', '0/foo,{"sid":"1"}') def test_handle_connect_namespace_twice(self, eio): s = server.Server() handler = mock.MagicMock() s.on('connect', handler, namespace='/foo') s._handle_eio_connect('123', 'environ') s._handle_eio_message('123', '0/foo,') s._handle_eio_message('123', '0/foo,') assert s.manager.is_connected('1', '/foo') handler.assert_called_once_with('1', 'environ') s.eio.send.assert_any_call('123', '0/foo,{"sid":"1"}') s.eio.send.assert_any_call('123', '4/foo,"Unable to connect"') def test_handle_connect_always_connect(self, eio): s = server.Server(always_connect=True) s.manager.initialize = mock.MagicMock() handler = mock.MagicMock() s.on('connect', handler) s._handle_eio_connect('123', 'environ') s._handle_eio_message('123', '0') assert s.manager.is_connected('1', '/') handler.assert_called_once_with('1', 'environ') s.eio.send.assert_called_once_with('123', '0{"sid":"1"}') assert s.manager.initialize.call_count == 1 s._handle_eio_connect('456', 'environ') assert s.manager.initialize.call_count == 1 def test_handle_connect_rejected(self, eio): s = server.Server() handler = mock.MagicMock(return_value=False) s.on('connect', handler) s._handle_eio_connect('123', 'environ') s._handle_eio_message('123', '0') assert not s.manager.is_connected('1', '/') handler.assert_called_once_with('1', 'environ') assert not s.manager.is_connected('1', '/') s.eio.send.assert_called_once_with( '123', '4{"message":"Connection rejected by server"}') assert s.environ == {'123': 'environ'} def test_handle_connect_namespace_rejected(self, eio): s = server.Server() handler = mock.MagicMock(return_value=False) s.on('connect', handler, namespace='/foo') s._handle_eio_connect('123', 'environ') s._handle_eio_message('123', '0/foo,') assert not s.manager.is_connected('1', '/foo') handler.assert_called_once_with('1', 'environ') assert not s.manager.is_connected('1', '/foo') s.eio.send.assert_called_once_with( '123', '4/foo,{"message":"Connection rejected by server"}') assert s.environ == {'123': 'environ'} def test_handle_connect_rejected_always_connect(self, eio): s = server.Server(always_connect=True) handler = mock.MagicMock(return_value=False) s.on('connect', handler) s._handle_eio_connect('123', 'environ') s._handle_eio_message('123', '0') assert not s.manager.is_connected('1', '/') handler.assert_called_once_with('1', 'environ') s.eio.send.assert_any_call('123', '0{"sid":"1"}') s.eio.send.assert_any_call( '123', '1{"message":"Connection rejected by server"}') assert s.environ == {'123': 'environ'} def test_handle_connect_namespace_rejected_always_connect(self, eio): s = server.Server(always_connect=True) handler = mock.MagicMock(return_value=False) s.on('connect', handler, namespace='/foo') s._handle_eio_connect('123', 'environ') s._handle_eio_message('123', '0/foo,') assert not s.manager.is_connected('1', '/foo') handler.assert_called_once_with('1', 'environ') s.eio.send.assert_any_call('123', '0/foo,{"sid":"1"}') s.eio.send.assert_any_call( '123', '1/foo,{"message":"Connection rejected by server"}') assert s.environ == {'123': 'environ'} def test_handle_connect_rejected_with_exception(self, eio): s = server.Server() handler = mock.MagicMock( side_effect=exceptions.ConnectionRefusedError('fail_reason') ) s.on('connect', handler) s._handle_eio_connect('123', 'environ') s._handle_eio_message('123', '0') assert not s.manager.is_connected('1', '/') handler.assert_called_once_with('1', 'environ') s.eio.send.assert_called_once_with('123', '4{"message":"fail_reason"}') assert s.environ == {'123': 'environ'} def test_handle_connect_rejected_with_empty_exception(self, eio): s = server.Server() handler = mock.MagicMock( side_effect=exceptions.ConnectionRefusedError() ) s.on('connect', handler) s._handle_eio_connect('123', 'environ') s._handle_eio_message('123', '0') assert not s.manager.is_connected('1', '/') handler.assert_called_once_with('1', 'environ') s.eio.send.assert_called_once_with( '123', '4{"message":"Connection rejected by server"}') assert s.environ == {'123': 'environ'} def test_handle_connect_namespace_rejected_with_exception(self, eio): s = server.Server() handler = mock.MagicMock( side_effect=exceptions.ConnectionRefusedError('fail_reason', 1) ) s.on('connect', handler, namespace='/foo') s._handle_eio_connect('123', 'environ') s._handle_eio_message('123', '0/foo,') assert not s.manager.is_connected('1', '/foo') s.eio.send.assert_called_once_with( '123', '4/foo,{"message":"fail_reason","data":1}' ) assert s.environ == {'123': 'environ'} def test_handle_connect_namespace_rejected_with_empty_exception(self, eio): s = server.Server() handler = mock.MagicMock( side_effect=exceptions.ConnectionRefusedError() ) s.on('connect', handler, namespace='/foo') s._handle_eio_connect('123', 'environ') s._handle_eio_message('123', '0/foo,') assert not s.manager.is_connected('1', '/foo') s.eio.send.assert_called_once_with( '123', '4/foo,{"message":"Connection rejected by server"}') assert s.environ == {'123': 'environ'} def test_handle_disconnect(self, eio): s = server.Server() s.manager.disconnect = mock.MagicMock() handler = mock.MagicMock() s.on('disconnect', handler) s._handle_eio_connect('123', 'environ') s._handle_eio_message('123', '0') s._handle_eio_disconnect('123', 'foo') handler.assert_called_once_with('1', 'foo') s.manager.disconnect.assert_called_once_with('1', '/', ignore_queue=True) assert s.environ == {} def test_handle_legacy_disconnect(self, eio): s = server.Server() s.manager.disconnect = mock.MagicMock() handler = mock.MagicMock(side_effect=[TypeError, None]) s.on('disconnect', handler) s._handle_eio_connect('123', 'environ') s._handle_eio_message('123', '0') s._handle_eio_disconnect('123', 'foo') handler.assert_called_with('1') s.manager.disconnect.assert_called_once_with('1', '/', ignore_queue=True) assert s.environ == {} def test_handle_disconnect_namespace(self, eio): s = server.Server() handler = mock.MagicMock() s.on('disconnect', handler) handler_namespace = mock.MagicMock() s.on('disconnect', handler_namespace, namespace='/foo') s._handle_eio_connect('123', 'environ') s._handle_eio_message('123', '0/foo,') s._handle_eio_disconnect('123', 'foo') handler.assert_not_called() handler_namespace.assert_called_once_with('1', 'foo') assert s.environ == {} def test_handle_disconnect_only_namespace(self, eio): s = server.Server() handler = mock.MagicMock() s.on('disconnect', handler) handler_namespace = mock.MagicMock() s.on('disconnect', handler_namespace, namespace='/foo') s._handle_eio_connect('123', 'environ') s._handle_eio_message('123', '0/foo,') s._handle_eio_message('123', '1/foo,') assert handler.call_count == 0 handler_namespace.assert_called_once_with( '1', s.reason.CLIENT_DISCONNECT) assert s.environ == {'123': 'environ'} def test_handle_disconnect_unknown_client(self, eio): mgr = mock.MagicMock() s = server.Server(client_manager=mgr) s._handle_eio_disconnect('123', 'foo') def test_handle_event(self, eio): s = server.Server(async_handlers=False) s.manager.connect('123', '/') handler = mock.MagicMock() catchall_handler = mock.MagicMock() s.on('msg', handler) s.on('*', catchall_handler) s._handle_eio_message('123', '2["msg","a","b"]') s._handle_eio_message('123', '2["my message","a","b","c"]') handler.assert_called_once_with('1', 'a', 'b') catchall_handler.assert_called_once_with( 'my message', '1', 'a', 'b', 'c') def test_handle_event_with_namespace(self, eio): s = server.Server(async_handlers=False) s.manager.connect('123', '/foo') handler = mock.MagicMock() catchall_handler = mock.MagicMock() s.on('msg', handler, namespace='/foo') s.on('*', catchall_handler, namespace='/foo') s._handle_eio_message('123', '2/foo,["msg","a","b"]') s._handle_eio_message('123', '2/foo,["my message","a","b","c"]') handler.assert_called_once_with('1', 'a', 'b') catchall_handler.assert_called_once_with( 'my message', '1', 'a', 'b', 'c') def test_handle_event_with_catchall_namespace(self, eio): s = server.Server(async_handlers=False) sid_foo = s.manager.connect('123', '/foo') sid_bar = s.manager.connect('123', '/bar') sid_baz = s.manager.connect('123', '/baz') connect_star_handler = mock.MagicMock() msg_foo_handler = mock.MagicMock() msg_star_handler = mock.MagicMock() star_foo_handler = mock.MagicMock() star_star_handler = mock.MagicMock() my_message_baz_handler = mock.MagicMock() s.on('connect', connect_star_handler, namespace='*') s.on('msg', msg_foo_handler, namespace='/foo') s.on('msg', msg_star_handler, namespace='*') s.on('*', star_foo_handler, namespace='/foo') s.on('*', star_star_handler, namespace='*') s.on('my message', my_message_baz_handler, namespace='/baz') s._trigger_event('connect', '/bar', sid_bar) s._handle_eio_message('123', '2/foo,["msg","a","b"]') s._handle_eio_message('123', '2/bar,["msg","a","b"]') s._handle_eio_message('123', '2/foo,["my message","a","b","c"]') s._handle_eio_message('123', '2/bar,["my message","a","b","c"]') s._trigger_event('disconnect', '/bar', sid_bar, s.reason.CLIENT_DISCONNECT) connect_star_handler.assert_called_once_with('/bar', sid_bar) msg_foo_handler.assert_called_once_with(sid_foo, 'a', 'b') msg_star_handler.assert_called_once_with('/bar', sid_bar, 'a', 'b') star_foo_handler.assert_called_once_with( 'my message', sid_foo, 'a', 'b', 'c') star_star_handler.assert_called_once_with( 'my message', '/bar', sid_bar, 'a', 'b', 'c') s._handle_eio_message('123', '2/baz,["my message","a","b","c"]') s._handle_eio_message('123', '2/baz,["msg","a","b"]') my_message_baz_handler.assert_called_once_with(sid_baz, 'a', 'b', 'c') msg_star_handler.assert_called_with('/baz', sid_baz, 'a', 'b') def test_handle_event_with_disconnected_namespace(self, eio): s = server.Server(async_handlers=False) s.manager.connect('123', '/foo') handler = mock.MagicMock() s.on('my message', handler, namespace='/bar') s._handle_eio_message('123', '2/bar,["my message","a","b","c"]') handler.assert_not_called() def test_handle_event_binary(self, eio): s = server.Server(async_handlers=False) s.manager.connect('123', '/') handler = mock.MagicMock() s.on('my message', handler) s._handle_eio_message( '123', '52-["my message","a",' '{"_placeholder":true,"num":1},' '{"_placeholder":true,"num":0}]', ) s._handle_eio_message('123', b'foo') s._handle_eio_message('123', b'bar') handler.assert_called_once_with('1', 'a', b'bar', b'foo') def test_handle_event_binary_ack(self, eio): s = server.Server() s.manager.trigger_callback = mock.MagicMock() sid = s.manager.connect('123', '/') s._handle_eio_message( '123', '61-321["my message","a",' '{"_placeholder":true,"num":0}]' ) s._handle_eio_message('123', b'foo') s.manager.trigger_callback.assert_called_once_with( sid, 321, ['my message', 'a', b'foo'] ) def test_handle_event_with_ack(self, eio): s = server.Server(async_handlers=False) sid = s.manager.connect('123', '/') handler = mock.MagicMock(return_value='foo') s.on('my message', handler) s._handle_eio_message('123', '21000["my message","foo"]') handler.assert_called_once_with(sid, 'foo') s.eio.send.assert_called_once_with('123', '31000["foo"]') def test_handle_unknown_event_with_ack(self, eio): s = server.Server(async_handlers=False) s.manager.connect('123', '/') handler = mock.MagicMock(return_value='foo') s.on('my message', handler) s._handle_eio_message('123', '21000["another message","foo"]') s.eio.send.assert_not_called() def test_handle_event_with_ack_none(self, eio): s = server.Server(async_handlers=False) sid = s.manager.connect('123', '/') handler = mock.MagicMock(return_value=None) s.on('my message', handler) s._handle_eio_message('123', '21000["my message","foo"]') handler.assert_called_once_with(sid, 'foo') s.eio.send.assert_called_once_with('123', '31000[]') def test_handle_event_with_ack_tuple(self, eio): s = server.Server(async_handlers=False) handler = mock.MagicMock(return_value=(1, '2', True)) s.on('my message', handler) sid = s.manager.connect('123', '/') s._handle_eio_message('123', '21000["my message","a","b","c"]') handler.assert_called_once_with(sid, 'a', 'b', 'c') s.eio.send.assert_called_with( '123', '31000[1,"2",true]' ) def test_handle_event_with_ack_list(self, eio): s = server.Server(async_handlers=False) handler = mock.MagicMock(return_value=[1, '2', True]) s.on('my message', handler) sid = s.manager.connect('123', '/') s._handle_eio_message('123', '21000["my message","a","b","c"]') handler.assert_called_once_with(sid, 'a', 'b', 'c') s.eio.send.assert_called_with( '123', '31000[[1,"2",true]]' ) def test_handle_event_with_ack_binary(self, eio): s = server.Server(async_handlers=False) handler = mock.MagicMock(return_value=b'foo') s.on('my message', handler) sid = s.manager.connect('123', '/') s._handle_eio_message('123', '21000["my message","foo"]') handler.assert_any_call(sid, 'foo') def test_handle_error_packet(self, eio): s = server.Server() with pytest.raises(ValueError): s._handle_eio_message('123', '4') def test_handle_invalid_packet(self, eio): s = server.Server() with pytest.raises(ValueError): s._handle_eio_message('123', '9') def test_send_with_ack(self, eio): s = server.Server() s.handlers['/'] = {} s._handle_eio_connect('123', 'environ') s._handle_eio_message('123', '0') cb = mock.MagicMock() id1 = s.manager._generate_ack_id('1', cb) id2 = s.manager._generate_ack_id('1', cb) s._send_packet('123', packet.Packet( packet.EVENT, ['my event', 'foo'], id=id1)) s._send_packet('123', packet.Packet( packet.EVENT, ['my event', 'bar'], id=id2)) s._handle_eio_message('123', '31["foo",2]') cb.assert_called_once_with('foo', 2) def test_send_with_ack_namespace(self, eio): s = server.Server() s.handlers['/foo'] = {} s._handle_eio_connect('123', 'environ') s._handle_eio_message('123', '0/foo,') cb = mock.MagicMock() id = s.manager._generate_ack_id('1', cb) s._send_packet('123', packet.Packet( packet.EVENT, ['my event', 'foo'], namespace='/foo', id=id)) s._handle_eio_message('123', '3/foo,1["foo",2]') cb.assert_called_once_with('foo', 2) def test_session(self, eio): fake_session = {} def fake_get_session(eio_sid): assert eio_sid == '123' return fake_session def fake_save_session(eio_sid, session): global fake_session assert eio_sid == '123' fake_session = session s = server.Server() s.handlers['/'] = {} s.handlers['/ns'] = {} s.eio.get_session = fake_get_session s.eio.save_session = fake_save_session s._handle_eio_connect('123', 'environ') s._handle_eio_message('123', '0') s._handle_eio_message('123', '0/ns') sid = s.manager.sid_from_eio_sid('123', '/') sid2 = s.manager.sid_from_eio_sid('123', '/ns') s.save_session(sid, {'foo': 'bar'}) with s.session(sid) as session: assert session == {'foo': 'bar'} session['foo'] = 'baz' session['bar'] = 'foo' assert s.get_session(sid) == {'foo': 'baz', 'bar': 'foo'} assert fake_session == {'/': {'foo': 'baz', 'bar': 'foo'}} with s.session(sid2, namespace='/ns') as session: assert session == {} session['a'] = 'b' assert s.get_session(sid2, namespace='/ns') == {'a': 'b'} assert fake_session == { '/': {'foo': 'baz', 'bar': 'foo'}, '/ns': {'a': 'b'}, } def test_disconnect(self, eio): s = server.Server() s.handlers['/'] = {} s._handle_eio_connect('123', 'environ') s._handle_eio_message('123', '0') s.disconnect('1') s.eio.send.assert_any_call('123', '1') def test_disconnect_ignore_queue(self, eio): s = server.Server() s.handlers['/'] = {} s._handle_eio_connect('123', 'environ') s._handle_eio_message('123', '0') s.disconnect('1', ignore_queue=True) s.eio.send.assert_any_call('123', '1') def test_disconnect_namespace(self, eio): s = server.Server() s.handlers['/foo'] = {} s._handle_eio_connect('123', 'environ') s._handle_eio_message('123', '0/foo,') s.disconnect('1', namespace='/foo') s.eio.send.assert_any_call('123', '1/foo,') def test_disconnect_twice(self, eio): s = server.Server() s._handle_eio_connect('123', 'environ') s._handle_eio_message('123', '0') s.disconnect('1') calls = s.eio.send.call_count s.disconnect('1') assert calls == s.eio.send.call_count def test_disconnect_twice_namespace(self, eio): s = server.Server() s._handle_eio_connect('123', 'environ') s._handle_eio_message('123', '0/foo,') s.disconnect('123', namespace='/foo') calls = s.eio.send.call_count s.disconnect('123', namespace='/foo') assert calls == s.eio.send.call_count def test_namespace_handler(self, eio): result = {} class MyNamespace(namespace.Namespace): def on_connect(self, sid, environ): result['result'] = (sid, environ) def on_disconnect(self, sid, reason): result['result'] = ('disconnect', sid, reason) def on_foo(self, sid, data): result['result'] = (sid, data) def on_bar(self, sid): result['result'] = 'bar' def on_baz(self, sid, data1, data2): result['result'] = (data1, data2) s = server.Server(async_handlers=False) s.register_namespace(MyNamespace('/foo')) s._handle_eio_connect('123', 'environ') s._handle_eio_message('123', '0/foo,') assert result['result'] == ('1', 'environ') s._handle_eio_message('123', '2/foo,["foo","a"]') assert result['result'] == ('1', 'a') s._handle_eio_message('123', '2/foo,["bar"]') assert result['result'] == 'bar' s._handle_eio_message('123', '2/foo,["baz","a","b"]') assert result['result'] == ('a', 'b') s.disconnect('1', '/foo') assert result['result'] == ('disconnect', '1', s.reason.SERVER_DISCONNECT) def test_catchall_namespace_handler(self, eio): result = {} class MyNamespace(namespace.Namespace): def on_connect(self, ns, sid, environ): result['result'] = (sid, ns, environ) def on_disconnect(self, ns, sid): result['result'] = ('disconnect', sid, ns) def on_foo(self, ns, sid, data): result['result'] = (sid, ns, data) def on_bar(self, ns, sid): result['result'] = 'bar' + ns def on_baz(self, ns, sid, data1, data2): result['result'] = (ns, data1, data2) s = server.Server(async_handlers=False, namespaces='*') s.register_namespace(MyNamespace('*')) s._handle_eio_connect('123', 'environ') s._handle_eio_message('123', '0/foo,') assert result['result'] == ('1', '/foo', 'environ') s._handle_eio_message('123', '2/foo,["foo","a"]') assert result['result'] == ('1', '/foo', 'a') s._handle_eio_message('123', '2/foo,["bar"]') assert result['result'] == 'bar/foo' s._handle_eio_message('123', '2/foo,["baz","a","b"]') assert result['result'] == ('/foo', 'a', 'b') s.disconnect('1', '/foo') assert result['result'] == ('disconnect', '1', '/foo') def test_bad_namespace_handler(self, eio): class Dummy: pass class AsyncNS(namespace.Namespace): def is_asyncio_based(self): return True s = server.Server() with pytest.raises(ValueError): s.register_namespace(123) with pytest.raises(ValueError): s.register_namespace(Dummy) with pytest.raises(ValueError): s.register_namespace(Dummy()) with pytest.raises(ValueError): s.register_namespace(namespace.Namespace) with pytest.raises(ValueError): s.register_namespace(AsyncNS()) def test_get_environ(self, eio): s = server.Server() s.handlers['/'] = {} s._handle_eio_connect('123', 'environ') s._handle_eio_message('123', '0') sid = s.manager.sid_from_eio_sid('123', '/') assert s.get_environ(sid) == 'environ' assert s.get_environ('foo') is None def test_logger(self, eio): s = server.Server(logger=False) assert s.logger.getEffectiveLevel() == logging.ERROR s.logger.setLevel(logging.NOTSET) s = server.Server(logger=True) assert s.logger.getEffectiveLevel() == logging.INFO s.logger.setLevel(logging.WARNING) s = server.Server(logger=True) assert s.logger.getEffectiveLevel() == logging.WARNING s.logger.setLevel(logging.NOTSET) s = server.Server(logger='foo') assert s.logger == 'foo' def test_engineio_logger(self, eio): server.Server(engineio_logger='foo') eio.assert_called_once_with( **{'logger': 'foo', 'async_handlers': False} ) def test_msgpack(self, eio): s = server.Server(serializer='msgpack') assert s.packet_class == msgpack_packet.MsgPackPacket def test_custom_serializer(self, eio): class CustomPacket(packet.Packet): pass s = server.Server(serializer=CustomPacket) assert s.packet_class == CustomPacket def test_custom_json(self, eio): # Warning: this test cannot run in parallel with other tests, as it # changes the JSON encoding/decoding functions class CustomJSON: @staticmethod def dumps(*args, **kwargs): return '*** encoded ***' @staticmethod def loads(*args, **kwargs): return '+++ decoded +++' server.Server(json=CustomJSON) eio.assert_called_once_with( **{'json': CustomJSON, 'async_handlers': False} ) pkt = packet.Packet( packet_type=packet.EVENT, data={'foo': 'bar'}, ) assert pkt.encode() == '2*** encoded ***' pkt2 = packet.Packet(encoded_packet=pkt.encode()) assert pkt2.data == '+++ decoded +++' # restore the default JSON module packet.Packet.json = json def test_async_handlers(self, eio): s = server.Server(async_handlers=True) sid = s.manager.connect('123', '/') s._handle_eio_message('123', '2["my message","a","b","c"]') s.eio.start_background_task.assert_called_once_with( s._handle_event_internal, s, sid, '123', ['my message', 'a', 'b', 'c'], '/', None, ) def test_shutdown(self, eio): s = server.Server() s.shutdown() s.eio.shutdown.assert_called_once_with() def test_start_background_task(self, eio): s = server.Server() s.start_background_task('foo', 'bar', baz='baz') s.eio.start_background_task.assert_called_once_with( 'foo', 'bar', baz='baz' ) def test_sleep(self, eio): s = server.Server() s.sleep(1.23) s.eio.sleep.assert_called_once_with(1.23) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1738799821.0 python_socketio-5.13.0/tests/common/test_simple_client.py0000664000175000017500000001425714750775315023332 0ustar00miguelmiguelfrom unittest import mock import pytest from socketio import SimpleClient from socketio.exceptions import SocketIOError, TimeoutError, DisconnectedError class TestSimpleClient: def test_constructor(self): client = SimpleClient(1, '2', a='3', b=4) assert client.client_args == (1, '2') assert client.client_kwargs == {'a': '3', 'b': 4} assert client.client is None assert client.input_buffer == [] assert not client.connected def test_connect(self): mock_client = mock.MagicMock() original_client_class = SimpleClient.client_class SimpleClient.client_class = mock_client client = SimpleClient(123, a='b') client.connect('url', headers='h', auth='a', transports='t', namespace='n', socketio_path='s', wait_timeout='w') mock_client.assert_called_once_with(123, a='b') assert client.client == mock_client() mock_client().connect.assert_called_once_with( 'url', headers='h', auth='a', transports='t', namespaces=['n'], socketio_path='s', wait_timeout='w') mock_client().event.call_count == 3 mock_client().on.assert_called_once_with('*', namespace='n') assert client.namespace == 'n' assert not client.input_event.is_set() SimpleClient.client_class = original_client_class def test_connect_context_manager(self): mock_client = mock.MagicMock() original_client_class = SimpleClient.client_class SimpleClient.client_class = mock_client with SimpleClient(123, a='b') as client: client.connect('url', headers='h', auth='a', transports='t', namespace='n', socketio_path='s', wait_timeout='w') mock_client.assert_called_once_with(123, a='b') assert client.client == mock_client() mock_client().connect.assert_called_once_with( 'url', headers='h', auth='a', transports='t', namespaces=['n'], socketio_path='s', wait_timeout='w') mock_client().event.call_count == 3 mock_client().on.assert_called_once_with('*', namespace='n') assert client.namespace == 'n' assert not client.input_event.is_set() SimpleClient.client_class = original_client_class def test_connect_twice(self): client = SimpleClient(123, a='b') client.client = mock.MagicMock() client.connected = True with pytest.raises(RuntimeError): client.connect('url') def test_properties(self): client = SimpleClient() client.client = mock.MagicMock(transport='websocket') client.client.get_sid.return_value = 'sid' client.connected_event.set() client.connected = True assert client.sid == 'sid' assert client.transport == 'websocket' def test_emit(self): client = SimpleClient() client.client = mock.MagicMock() client.namespace = '/ns' client.connected_event.set() client.connected = True client.emit('foo', 'bar') client.client.emit.assert_called_once_with('foo', 'bar', namespace='/ns') def test_emit_disconnected(self): client = SimpleClient() client.connected_event.set() client.connected = False with pytest.raises(DisconnectedError): client.emit('foo', 'bar') def test_emit_retries(self): client = SimpleClient() client.connected_event.set() client.connected = True client.client = mock.MagicMock() client.client.emit.side_effect = [SocketIOError(), None] client.emit('foo', 'bar') client.client.emit.assert_called_with('foo', 'bar', namespace='/') def test_call(self): client = SimpleClient() client.client = mock.MagicMock() client.client.call.return_value = 'result' client.namespace = '/ns' client.connected_event.set() client.connected = True assert client.call('foo', 'bar') == 'result' client.client.call.assert_called_once_with('foo', 'bar', namespace='/ns', timeout=60) def test_call_disconnected(self): client = SimpleClient() client.connected_event.set() client.connected = False with pytest.raises(DisconnectedError): client.call('foo', 'bar') def test_call_retries(self): client = SimpleClient() client.connected_event.set() client.connected = True client.client = mock.MagicMock() client.client.call.side_effect = [SocketIOError(), 'result'] assert client.call('foo', 'bar') == 'result' client.client.call.assert_called_with('foo', 'bar', namespace='/', timeout=60) def test_receive_with_input_buffer(self): client = SimpleClient() client.input_buffer = ['foo', 'bar'] assert client.receive() == 'foo' assert client.receive() == 'bar' def test_receive_without_input_buffer(self): client = SimpleClient() client.connected_event.set() client.connected = True client.input_event = mock.MagicMock() def fake_wait(timeout=None): client.input_buffer = ['foo'] return True client.input_event.wait = fake_wait assert client.receive() == 'foo' def test_receive_with_timeout(self): client = SimpleClient() client.connected_event.set() client.connected = True with pytest.raises(TimeoutError): client.receive(timeout=0.01) def test_receive_disconnected(self): client = SimpleClient() client.connected_event.set() client.connected = False with pytest.raises(DisconnectedError): client.receive() def test_disconnect(self): client = SimpleClient() mc = mock.MagicMock() client.client = mc client.connected = True client.disconnect() client.disconnect() mc.disconnect.assert_called_once_with() assert client.client is None ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744472442.0600872 python_socketio-5.13.0/tests/performance/0000775000175000017500000000000014776504572020075 5ustar00miguelmiguel././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704499102.0 python_socketio-5.13.0/tests/performance/README.md0000644000175000017500000000016314546113636021342 0ustar00miguelmiguelPerformance =========== This directory contains several scripts and tools to test the performance of the project. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704499102.0 python_socketio-5.13.0/tests/performance/binary_packet.py0000644000175000017500000000075314546113636023255 0ustar00miguelmiguelimport time from socketio import packet def test(): p = packet.Packet(packet.EVENT, {'foo': b'bar'}) start = time.time() count = 0 while True: eps = p.encode() p = packet.Packet(encoded_packet=eps[0]) for ep in eps[1:]: p.add_attachment(ep) count += 1 if time.time() - start >= 5: break return count if __name__ == '__main__': count = test() print('binary_packet:', count, 'packets processed.') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704499102.0 python_socketio-5.13.0/tests/performance/json_packet.py0000644000175000017500000000062714546113636022742 0ustar00miguelmiguelimport time from socketio import packet def test(): p = packet.Packet(packet.EVENT, {'foo': 'bar'}) start = time.time() count = 0 while True: p = packet.Packet(encoded_packet=p.encode()) count += 1 if time.time() - start >= 5: break return count if __name__ == '__main__': count = test() print('json_packet:', count, 'packets processed.') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704499102.0 python_socketio-5.13.0/tests/performance/namespace_packet.py0000644000175000017500000000064714546113636023727 0ustar00miguelmiguelimport time from socketio import packet def test(): p = packet.Packet(packet.EVENT, 'hello', namespace='/foo') start = time.time() count = 0 while True: p = packet.Packet(encoded_packet=p.encode()) count += 1 if time.time() - start >= 5: break return count if __name__ == '__main__': count = test() print('namespace_packet:', count, 'packets processed.') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730029804.0 python_socketio-5.13.0/tests/performance/run.sh0000775000175000017500000000027214707424354021232 0ustar00miguelmiguel#!/bin/bash python text_packet.py python binary_packet.py python json_packet.py python namespace_packet.py python server_receive.py python server_send.py python server_send_broadcast.py ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704499102.0 python_socketio-5.13.0/tests/performance/server_receive.py0000644000175000017500000000073614546113636023453 0ustar00miguelmiguelimport time import socketio def test(): s = socketio.Server(async_handlers=False) start = time.time() count = 0 s._handle_eio_connect('123', 'environ') s._handle_eio_message('123', '0') while True: s._handle_eio_message('123', '2["test","hello"]') count += 1 if time.time() - start >= 5: break return count if __name__ == '__main__': count = test() print('server_receive:', count, 'packets received.') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704499102.0 python_socketio-5.13.0/tests/performance/server_send.py0000644000175000017500000000107414546113636022756 0ustar00miguelmiguelimport time import socketio class Server(socketio.Server): def _send_packet(self, eio_sid, pkt): pass def _send_eio_packet(self, eio_sid, eio_pkt): pass def test(): s = Server() start = time.time() count = 0 s._handle_eio_connect('123', 'environ') s._handle_eio_message('123', '0') while True: s.emit('test', 'hello') count += 1 if time.time() - start >= 5: break return count if __name__ == '__main__': count = test() print('server_send:', count, 'packets received.') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704499102.0 python_socketio-5.13.0/tests/performance/server_send_broadcast.py0000644000175000017500000000113714546113636025000 0ustar00miguelmiguelimport time import socketio class Server(socketio.Server): def _send_packet(self, eio_sid, pkt): pass def _send_eio_packet(self, eio_sid, eio_pkt): pass def test(): s = Server() start = time.time() count = 0 for i in range(100): s._handle_eio_connect(str(i), 'environ') s._handle_eio_message(str(i), '0') while True: s.emit('test', 'hello') count += 1 if time.time() - start >= 5: break return count if __name__ == '__main__': count = test() print('server_send:', count, 'packets received.') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704499102.0 python_socketio-5.13.0/tests/performance/text_packet.py0000644000175000017500000000062014546113636022746 0ustar00miguelmiguelimport time from socketio import packet def test(): p = packet.Packet(packet.EVENT, 'hello') start = time.time() count = 0 while True: p = packet.Packet(encoded_packet=p.encode()) count += 1 if time.time() - start >= 5: break return count if __name__ == '__main__': count = test() print('text_packet:', count, 'packets processed.') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704499102.0 python_socketio-5.13.0/tests/web_server.py0000644000175000017500000000524114546113636020301 0ustar00miguelmiguelimport threading import time from socketserver import ThreadingMixIn from wsgiref.simple_server import make_server, WSGIServer, WSGIRequestHandler import requests import socketio class SocketIOWebServer: """A simple web server used for running Socket.IO servers in tests. :param sio: a Socket.IO server instance. Note 1: This class is not production-ready and is intended for testing. Note 2: This class only supports the "threading" async_mode, with WebSocket support provided by the simple-websocket package. """ def __init__(self, sio): if sio.async_mode != 'threading': raise ValueError('The async_mode must be "threading"') def http_app(environ, start_response): start_response('200 OK', [('Content-Type', 'text/plain')]) return [b'OK'] self.sio = sio self.app = socketio.WSGIApp(sio, http_app) self.httpd = None self.thread = None def start(self, port=8900): """Start the web server. :param port: the port to listen on. Defaults to 8900. The server is started in a background thread. """ class ThreadingWSGIServer(ThreadingMixIn, WSGIServer): pass class WebSocketRequestHandler(WSGIRequestHandler): def get_environ(self): env = super().get_environ() # pass the raw socket to the WSGI app so that it can be used # by WebSocket connections (hack copied from gunicorn) env['gunicorn.socket'] = self.connection return env self.httpd = make_server('', port, self._app_wrapper, ThreadingWSGIServer, WebSocketRequestHandler) self.thread = threading.Thread(target=self.httpd.serve_forever) self.thread.start() # wait for the server to start while True: try: r = requests.get(f'http://localhost:{port}/') r.raise_for_status() if r.text == 'OK': break except: time.sleep(0.1) def stop(self): """Stop the web server.""" self.sio.shutdown() self.httpd.shutdown() self.httpd.server_close() self.thread.join() self.httpd = None self.thread = None def _app_wrapper(self, environ, start_response): try: return self.app(environ, start_response) except StopIteration: # end the WebSocket request without sending a response # (this is a hack that was copied from gunicorn's threaded worker) start_response('200 OK', []) return [] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1734467514.0 python_socketio-5.13.0/tox.ini0000664000175000017500000000144214730357672015743 0ustar00miguelmiguel[tox] envlist=flake8,py{38,39,310,311,312,313},docs skip_missing_interpreters=True [gh-actions] python = 3.8: py38 3.9: py39 3.10: py310 3.11: py311 3.12: py312 3.13: py313 pypy-3: pypy3 [testenv] commands= pip install -e . pytest -p no:logging --timeout=60 --cov=socketio --cov-branch --cov-report=term-missing --cov-report=xml deps= simple-websocket uvicorn requests websocket-client aiohttp msgpack pytest pytest-asyncio pytest-timeout pytest-cov [testenv:flake8] deps= flake8 commands= flake8 --exclude=".*" --exclude="examples/server/wsgi/django_socketio" --ignore=W503,E402,E722 src/socketio tests examples [testenv:docs] changedir=docs deps= sphinx allowlist_externals= make commands= make html