diff --git a/docs/howto/index.rst b/docs/howto/index.rst index ddbe67d3..863c1c63 100644 --- a/docs/howto/index.rst +++ b/docs/howto/index.rst @@ -8,6 +8,14 @@ In a hurry? Check out these examples. quickstart +Upgrading from the legacy :mod:`asyncio` implementation to the new one? +Read this. + +.. toctree:: + :titlesonly: + + upgrade + If you're stuck, perhaps you'll find the answer here. .. toctree:: diff --git a/docs/howto/upgrade.rst b/docs/howto/upgrade.rst new file mode 100644 index 00000000..bb4c59bc --- /dev/null +++ b/docs/howto/upgrade.rst @@ -0,0 +1,357 @@ +Upgrade to the new :mod:`asyncio` implementation +================================================ + +.. currentmodule:: websockets + +The new :mod:`asyncio` implementation is a rewrite of the original +implementation of websockets. + +It provides a very similar API. However, there are a few differences. + +The recommended upgrade process is: + +1. Make sure that your application doesn't use any `deprecated APIs`_. If it + doesn't raise any warnings, you can skip this step. +2. Check if your application depends on `missing features`_. If it does, you + should stick to the original implementation until they're added. +3. `Update import paths`_. For straightforward usage of websockets, this could + be the only step you need to take. Upgrading could be transparent. +4. `Review API changes`_ and adapt your application to preserve its current + functionality or take advantage of improvements in the new implementation. + +In the interest of brevity, only :func:`~asyncio.client.connect` and +:func:`~asyncio.server.serve` are discussed below but everything also applies +to :func:`~asyncio.client.unix_connect` and :func:`~asyncio.server.unix_serve` +respectively. + +.. admonition:: What will happen to the original implementation? + :class: hint + + The original implementation is now considered legacy. + + The next steps are: + + 1. Deprecating it once the new implementation reaches feature parity. + 2. Maintaining it for five years per the :ref:`backwards-compatibility + policy `. + 3. Removing it. This is expected to happen around 2030. + +.. _deprecated APIs: + +Deprecated APIs +--------------- + +Here's the list of deprecated behaviors that the original implementation still +supports and that the new implementation doesn't reproduce. + +If you're seeing a :class:`DeprecationWarning`, follow upgrade instructions from +the release notes of the version in which the feature was deprecated. + +* The ``path`` argument of connection handlers — unnecessary since :ref:`10.1` + and deprecated in :ref:`13.0`. +* The ``loop`` and ``legacy_recv`` arguments of :func:`~client.connect` and + :func:`~server.serve`, which were removed — deprecated in :ref:`10.0`. +* The ``timeout`` and ``klass`` arguments of :func:`~client.connect` and + :func:`~server.serve`, which were renamed to ``close_timeout`` and + ``create_protocol`` — deprecated in :ref:`7.0` and :ref:`3.4` respectively. +* An empty string in the ``origins`` argument of :func:`~server.serve` — + deprecated in :ref:`7.0`. +* The ``host``, ``port``, and ``secure`` attributes of connections — deprecated + in :ref:`8.0`. + +.. _missing features: + +Missing features +---------------- + +.. admonition:: All features listed below will be provided in a future release. + :class: tip + + If your application relies on one of them, you should stick to the original + implementation until the new implementation supports it in a future release. + +Broadcast +......... + +The new implementation doesn't support :doc:`broadcasting messages +<../topics/broadcast>` yet. + +Keepalive +......... + +The new implementation doesn't provide a :ref:`keepalive mechanism ` +yet. + +As a consequence, :func:`~asyncio.client.connect` and +:func:`~asyncio.server.serve` don't accept the ``ping_interval`` and +``ping_timeout`` arguments and the +:attr:`~legacy.protocol.WebSocketCommonProtocol.latency` property doesn't exist. + +HTTP Basic Authentication +......................... + +On the server side, :func:`~asyncio.server.serve` doesn't provide HTTP Basic +Authentication yet. + +For the avoidance of doubt, on the client side, :func:`~asyncio.client.connect` +performs HTTP Basic Authentication. + +Following redirects +................... + +The new implementation of :func:`~asyncio.client.connect` doesn't follow HTTP +redirects yet. + +Automatic reconnection +...................... + +The new implementation of :func:`~asyncio.client.connect` doesn't provide +automatic reconnection yet. + +In other words, the following pattern isn't supported:: + + from websockets.asyncio.client import connect + + async for websocket in connect(...): # this doesn't work yet + ... + +Configuring buffers +................... + +The new implementation doesn't provide a way to configure read and write buffers +yet. + +In practice, :func:`~asyncio.client.connect` and :func:`~asyncio.server.serve` +don't accept the ``max_queue``, ``read_limit``, and ``write_limit`` arguments. + +Here's the most likely outcome: + +* ``max_queue`` will be implemented but its semantics will change from "maximum + number of messages" to "maximum number of frames", which makes a difference + when messages are fragmented. +* ``read_limit`` won't be implemented because the buffer that it configured was + removed from the new implementation. The queue that ``max_queue`` configures + is the only read buffer now. +* ``write_limit`` will be implemented as in the original implementation. + Alternatively, the same functionality could be exposed with a different API. + +.. _Update import paths: + +Import paths +------------ + +For context, the ``websockets`` package is structured as follows: + +* The new implementation is found in the ``websockets.asyncio`` package. +* The original implementation was moved to the ``websockets.legacy`` package. +* The ``websockets`` package provides aliases for convenience. +* The ``websockets.client`` and ``websockets.server`` packages provide aliases + for backwards-compatibility with earlier versions of websockets. +* Currently, all aliases point to the original implementation. In the future, + they will point to the new implementation or they will be deprecated. + +To upgrade to the new :mod:`asyncio` implementation, change import paths as +shown in the tables below. + +.. |br| raw:: html + +
+ +Client APIs +........... + ++-------------------------------------------------------------------+-----------------------------------------------------+ +| Legacy :mod:`asyncio` implementation | New :mod:`asyncio` implementation | ++===================================================================+=====================================================+ +| ``websockets.connect()`` |br| | :func:`websockets.asyncio.client.connect` | +| :func:`websockets.client.connect` |br| | | +| ``websockets.legacy.client.connect()`` | | ++-------------------------------------------------------------------+-----------------------------------------------------+ +| ``websockets.unix_connect()`` |br| | :func:`websockets.asyncio.client.unix_connect` | +| :func:`websockets.client.unix_connect` |br| | | +| ``websockets.legacy.client.unix_connect()`` | | ++-------------------------------------------------------------------+-----------------------------------------------------+ +| ``websockets.WebSocketClientProtocol`` |br| | :class:`websockets.asyncio.client.ClientConnection` | +| :class:`websockets.client.WebSocketClientProtocol` |br| | | +| ``websockets.legacy.client.WebSocketClientProtocol`` | | ++-------------------------------------------------------------------+-----------------------------------------------------+ + +Server APIs +........... + ++-------------------------------------------------------------------+-----------------------------------------------------+ +| Legacy :mod:`asyncio` implementation | New :mod:`asyncio` implementation | ++===================================================================+=====================================================+ +| ``websockets.serve()`` |br| | :func:`websockets.asyncio.server.serve` | +| :func:`websockets.server.serve` |br| | | +| ``websockets.legacy.server.serve()`` | | ++-------------------------------------------------------------------+-----------------------------------------------------+ +| ``websockets.unix_serve()`` |br| | :func:`websockets.asyncio.server.unix_serve` | +| :func:`websockets.server.unix_serve` |br| | | +| ``websockets.legacy.server.unix_serve()`` | | ++-------------------------------------------------------------------+-----------------------------------------------------+ +| ``websockets.WebSocketServer`` |br| | :class:`websockets.asyncio.server.WebSocketServer` | +| :class:`websockets.server.WebSocketServer` |br| | | +| ``websockets.legacy.server.WebSocketServer`` | | ++-------------------------------------------------------------------+-----------------------------------------------------+ +| ``websockets.WebSocketServerProtocol`` |br| | :class:`websockets.asyncio.server.ServerConnection` | +| :class:`websockets.server.WebSocketServerProtocol` |br| | | +| ``websockets.legacy.server.WebSocketServerProtocol`` | | ++-------------------------------------------------------------------+-----------------------------------------------------+ +| :func:`websockets.broadcast` |br| | *not available yet* | +| ``websockets.legacy.protocol.broadcast()`` | | ++-------------------------------------------------------------------+-----------------------------------------------------+ +| ``websockets.BasicAuthWebSocketServerProtocol`` |br| | *not available yet* | +| :class:`websockets.auth.BasicAuthWebSocketServerProtocol` |br| | | +| ``websockets.legacy.auth.BasicAuthWebSocketServerProtocol`` | | ++-------------------------------------------------------------------+-----------------------------------------------------+ +| ``websockets.basic_auth_protocol_factory()`` |br| | *not available yet* | +| :func:`websockets.auth.basic_auth_protocol_factory` |br| | | +| ``websockets.legacy.auth.basic_auth_protocol_factory()`` | | ++-------------------------------------------------------------------+-----------------------------------------------------+ + +.. _Review API changes: + +API changes +----------- + +Controlling UTF-8 decoding +.......................... + +The new implementation of the :meth:`~asyncio.connection.Connection.recv` method +provides the ``decode`` argument to control UTF-8 decoding of messages. This +didn't exist in the original implementation. + +If you're calling :meth:`~str.encode` on a :class:`str` object returned by +:meth:`~asyncio.connection.Connection.recv`, using ``decode=False`` and removing +:meth:`~str.encode` saves a round-trip of UTF-8 decoding and encoding for text +messages. + +You can also force UTF-8 decoding of binary messages with ``decode=True``. This +is rarely useful and has no performance benefits over decoding a :class:`bytes` +object returned by :meth:`~asyncio.connection.Connection.recv`. + +Receiving fragmented messages +............................. + +The new implementation provides the +:meth:`~asyncio.connection.Connection.recv_streaming` method for receiving a +fragmented message frame by frame. There was no way to do this in the original +implementation. + +Depending on your use case, adopting this method may improve performance when +streaming large messages. Specifically, it could reduce memory usage. + +Customizing the opening handshake +................................. + +On the client side, if you're adding headers to the handshake request sent by +:func:`~client.connect` with the ``extra_headers`` argument, you must rename it +to ``additional_headers``. + +On the server side, if you're customizing how :func:`~server.serve` processes +the opening handshake with the ``process_request``, ``extra_headers``, or +``select_subprotocol``, you must update your code. ``process_response`` and +``select_subprotocol`` have new signatures; ``process_response`` replaces +``extra_headers`` and provides more flexibility. + +``process_request`` +~~~~~~~~~~~~~~~~~~~ + +The signature of ``process_request`` changed. This is easiest to illustrate with +an example:: + + import http + + # Original implementation + + def process_request(path, request_headers): + return http.HTTPStatus.OK, [], b"OK\n" + + serve(..., process_request=process_request, ...) + + # New implementation + + def process_request(connection, request): + return connection.protocol.reject(http.HTTPStatus.OK, "OK\n") + + serve(..., process_request=process_request, ...) + +``connection`` is always available in ``process_request``. In the original +implementation, you had to write a subclass of +:class:`~server.WebSocketServerProtocol` and pass it in the ``create_protocol`` +argument to make the connection object available in a ``process_request`` +method. This pattern isn't useful anymore; you can replace it with a +``process_request`` function or coroutine. + +``path`` and ``headers`` are available as attributes of the ``request`` object. + +``process_response`` +~~~~~~~~~~~~~~~~~~~~ + +``process_request`` replaces ``extra_headers`` and provides more flexibility. +In the most basic case, you would adapt your code as follows:: + + # Original implementation + + serve(..., extra_headers=HEADERS, ...) + + # New implementation + + def process_response(connection, request, response): + response.headers.update(HEADERS) + return response + + serve(..., process_response=process_response, ...) + +``connection`` is always available in ``process_response``, similar to +``process_request``. In the original implementation, there was no way to make +the connection object available. + +In addition, the ``request`` and ``response`` objects are available, which +enables a broader range of use cases (e.g., logging) and makes +``process_response`` more useful than ``extra_headers``. + +``select_subprotocol`` +~~~~~~~~~~~~~~~~~~~~~~ + +The signature of ``select_subprotocol`` changed. Here's an example:: + + # Original implementation + + def select_subprotocol(client_subprotocols, server_subprotocols): + if "chat" in client_subprotocols: + return "chat" + + # New implementation + + def select_subprotocol(connection, subprotocols): + if "chat" in subprotocols + return "chat" + + serve(..., select_subprotocol=select_subprotocol, ...) + +``connection`` is always available in ``select_subprotocol``. This brings the +same benefits as in ``process_request``. It may remove the need to subclass of +:class:`~server.WebSocketServerProtocol`. + +The ``subprotocols`` argument contains the list of subprotocols offered by the +client. The list of subprotocols supported by the server was removed because +``select_subprotocols`` already knows which subprotocols it may select and under +which conditions. + +Miscellaneous changes +..................... + +The first argument of :func:`~asyncio.server.serve` is called ``handler`` instead +of ``ws_handler``. It's usually passed as a positional argument, making this +change transparent. If you're passing it as a keyword argument, you must update +its name. + +The keyword argument of :func:`~asyncio.server.serve` for customizing the +creation of the connection object is called ``create_connection`` instead of +``create_protocol``. It must return a :class:`~asyncio.server.ServerConnection` +instead of a :class:`~server.WebSocketServerProtocol`. If you were customizing +connection objects, you should check the new implementation and possibly redo +your customization. Keep in mind that the changes to ``process_request`` and +``select_subprotocol`` remove most use cases for ``create_connection``. diff --git a/docs/project/changelog.rst b/docs/project/changelog.rst index 00b055dd..f033f563 100644 --- a/docs/project/changelog.rst +++ b/docs/project/changelog.rst @@ -25,6 +25,8 @@ fixing regressions shortly after a release. Only documented APIs are public. Undocumented, private APIs may change without notice. +.. _13.0: + 13.0 ---- @@ -66,10 +68,10 @@ New features This new implementation is intended to be a drop-in replacement for the current implementation. It will become the default in a future release. - Please try it and report any issue that you encounter! - See :func:`websockets.asyncio.client.connect` and - :func:`websockets.asyncio.server.serve` for details. + Please try it and report any issue that you encounter! The :doc:`upgrade + guide <../howto/upgrade>` explains everything you need to know about the + upgrade process. * Validated compatibility with Python 3.12 and 3.13. @@ -79,6 +81,8 @@ New features If you were monkey-patching constants, be aware that they were renamed, which will break your configuration. You must switch to the environment variables. +.. _12.0: + 12.0 ---- @@ -135,6 +139,8 @@ Bug fixes * Restored the C extension in the source distribution. +.. _11.0: + 11.0 ---- @@ -211,6 +217,8 @@ Improvements * Set ``server_hostname`` automatically on TLS connections when providing a ``sock`` argument to :func:`~sync.client.connect`. +.. _10.4: + 10.4 ---- @@ -237,6 +245,8 @@ Improvements * Improved FAQ. +.. _10.3: + 10.3 ---- @@ -259,6 +269,8 @@ Improvements * Reduced noise in logs when :mod:`ssl` or :mod:`zlib` raise exceptions. +.. _10.2: + 10.2 ---- @@ -279,6 +291,8 @@ Bug fixes * Avoided leaking open sockets when :func:`~client.connect` is canceled. +.. _10.1: + 10.1 ---- @@ -328,6 +342,8 @@ Bug fixes * Avoided half-closing TCP connections that are already closed. +.. _10.0: + 10.0 ---- @@ -434,6 +450,8 @@ Bug fixes * Avoided a crash when receiving a ping while the connection is closing. +.. _9.1: + 9.1 --- @@ -472,6 +490,8 @@ Bug fixes * Fixed issues with the packaging of the 9.0 release. +.. _9.0: + 9.0 --- @@ -549,6 +569,8 @@ Bug fixes * Ensured cancellation always propagates, even on Python versions where :exc:`~asyncio.CancelledError` inherits :exc:`Exception`. +.. _8.1: + 8.1 --- @@ -583,6 +605,8 @@ Bug fixes * Restored the ability to import ``WebSocketProtocolError`` from ``websockets``. +.. _8.0: + 8.0 --- @@ -692,6 +716,8 @@ Bug fixes * Avoided a crash when a ``extra_headers`` callable returns :obj:`None`. +.. _7.0: + 7.0 --- @@ -786,6 +812,8 @@ Bug fixes :meth:`~legacy.protocol.WebSocketCommonProtocol.recv`: canceling it at the wrong time could result in messages being dropped. +.. _6.0: + 6.0 --- @@ -840,6 +868,8 @@ Bug fixes * Fixed a regression in 5.0 that broke some invocations of :func:`~server.serve` and :func:`~client.connect`. +.. _5.0: + 5.0 --- @@ -925,6 +955,8 @@ Bug fixes * Fixed issues with the packaging of the 4.0 release. +.. _4.0: + 4.0 --- @@ -984,6 +1016,8 @@ Bug fixes * Stopped leaking pending tasks when :meth:`~asyncio.Task.cancel` is called on a connection while it's being closed. +.. _3.4: + 3.4 --- @@ -1027,6 +1061,8 @@ Bug fixes * Providing a ``sock`` argument to :func:`~client.connect` no longer crashes. +.. _3.3: + 3.3 --- @@ -1047,6 +1083,8 @@ Bug fixes * Avoided crashing on concurrent writes on slow connections. +.. _3.2: + 3.2 --- @@ -1063,6 +1101,8 @@ Improvements * Made server shutdown more robust. +.. _3.1: + 3.1 --- @@ -1078,6 +1118,8 @@ Bug fixes * Avoided a warning when closing a connection before the opening handshake. +.. _3.0: + 3.0 --- @@ -1135,6 +1177,8 @@ Improvements * Improved documentation. +.. _2.7: + 2.7 --- @@ -1150,6 +1194,8 @@ Improvements * Refreshed documentation. +.. _2.6: + 2.6 --- @@ -1167,6 +1213,8 @@ Bug fixes * Avoided TCP fragmentation of small frames. +.. _2.5: + 2.5 --- @@ -1200,6 +1248,8 @@ Bug fixes * Canceling :meth:`~legacy.protocol.WebSocketCommonProtocol.recv` no longer drops the next message. +.. _2.4: + 2.4 --- @@ -1213,6 +1263,8 @@ New features * Added ``loop`` argument to :func:`~client.connect` and :func:`~server.serve`. +.. _2.3: + 2.3 --- @@ -1223,6 +1275,8 @@ Improvements * Improved compliance of close codes. +.. _2.2: + 2.2 --- @@ -1233,6 +1287,8 @@ New features * Added support for limiting message size. +.. _2.1: + 2.1 --- @@ -1247,6 +1303,8 @@ New features .. _Origin: https://www.rfc-editor.org/rfc/rfc6455.html#section-10.2 +.. _2.0: + 2.0 --- @@ -1275,6 +1333,8 @@ New features * Added flow control for outgoing data. +.. _1.0: + 1.0 --- diff --git a/docs/reference/asyncio/client.rst b/docs/reference/asyncio/client.rst index 5086015b..f9ce2f2d 100644 --- a/docs/reference/asyncio/client.rst +++ b/docs/reference/asyncio/client.rst @@ -1,5 +1,5 @@ -Client (:mod:`asyncio`) -======================= +Client (legacy :mod:`asyncio`) +============================== .. automodule:: websockets.client diff --git a/docs/reference/asyncio/common.rst b/docs/reference/asyncio/common.rst index dc7a54ee..aee77447 100644 --- a/docs/reference/asyncio/common.rst +++ b/docs/reference/asyncio/common.rst @@ -1,7 +1,7 @@ :orphan: -Both sides (:mod:`asyncio`) -=========================== +Both sides (legacy :mod:`asyncio`) +================================== .. automodule:: websockets.legacy.protocol diff --git a/docs/reference/asyncio/server.rst b/docs/reference/asyncio/server.rst index 10631791..4bd52b40 100644 --- a/docs/reference/asyncio/server.rst +++ b/docs/reference/asyncio/server.rst @@ -1,15 +1,15 @@ -Server (:mod:`asyncio`) -======================= +Server (legacy :mod:`asyncio`) +============================== .. automodule:: websockets.server Starting a server ----------------- -.. autofunction:: serve(ws_handler, host=None, port=None, *, create_protocol=None, logger=None, compression="deflate", origins=None, extensions=None, subprotocols=None, extra_headers=None, server_header="Python/x.y.z websockets/X.Y", process_request=None, select_subprotocol=None, ping_interval=20, ping_timeout=20, close_timeout=10, max_size=2 ** 20, max_queue=2 ** 5, read_limit=2 ** 16, write_limit=2 ** 16, **kwds) +.. autofunction:: serve(ws_handler, host=None, port=None, *, create_protocol=None, logger=None, compression="deflate", origins=None, extensions=None, subprotocols=None, extra_headers=None, server_header="Python/x.y.z websockets/X.Y", process_request=None, select_subprotocol=None, open_timeout=10, ping_interval=20, ping_timeout=20, close_timeout=10, max_size=2 ** 20, max_queue=2 ** 5, read_limit=2 ** 16, write_limit=2 ** 16, **kwds) :async: -.. autofunction:: unix_serve(ws_handler, path=None, *, create_protocol=None, logger=None, compression="deflate", origins=None, extensions=None, subprotocols=None, extra_headers=None, server_header="Python/x.y.z websockets/X.Y", process_request=None, select_subprotocol=None, ping_interval=20, ping_timeout=20, close_timeout=10, max_size=2 ** 20, max_queue=2 ** 5, read_limit=2 ** 16, write_limit=2 ** 16, **kwds) +.. autofunction:: unix_serve(ws_handler, path=None, *, create_protocol=None, logger=None, compression="deflate", origins=None, extensions=None, subprotocols=None, extra_headers=None, server_header="Python/x.y.z websockets/X.Y", process_request=None, select_subprotocol=None, open_timeout=10, ping_interval=20, ping_timeout=20, close_timeout=10, max_size=2 ** 20, max_queue=2 ** 5, read_limit=2 ** 16, write_limit=2 ** 16, **kwds) :async: Stopping a server @@ -34,7 +34,7 @@ Stopping a server Using a connection ------------------ -.. autoclass:: WebSocketServerProtocol(ws_handler, ws_server, *, logger=None, origins=None, extensions=None, subprotocols=None, extra_headers=None, server_header="Python/x.y.z websockets/X.Y", process_request=None, select_subprotocol=None, ping_interval=20, ping_timeout=20, close_timeout=10, max_size=2 ** 20, max_queue=2 ** 5, read_limit=2 ** 16, write_limit=2 ** 16) +.. autoclass:: WebSocketServerProtocol(ws_handler, ws_server, *, logger=None, origins=None, extensions=None, subprotocols=None, extra_headers=None, server_header="Python/x.y.z websockets/X.Y", process_request=None, select_subprotocol=None, open_timeout=10, ping_interval=20, ping_timeout=20, close_timeout=10, max_size=2 ** 20, max_queue=2 ** 5, read_limit=2 ** 16, write_limit=2 ** 16) .. automethod:: recv diff --git a/docs/reference/new-asyncio/client.rst b/docs/reference/new-asyncio/client.rst index 552d83b2..196bda2b 100644 --- a/docs/reference/new-asyncio/client.rst +++ b/docs/reference/new-asyncio/client.rst @@ -1,5 +1,5 @@ -Client (:mod:`asyncio` - new) -============================= +Client (new :mod:`asyncio`) +=========================== .. automodule:: websockets.asyncio.client diff --git a/docs/reference/new-asyncio/common.rst b/docs/reference/new-asyncio/common.rst index ba23552d..4fa97dcf 100644 --- a/docs/reference/new-asyncio/common.rst +++ b/docs/reference/new-asyncio/common.rst @@ -1,7 +1,7 @@ :orphan: -Both sides (:mod:`asyncio` - new) -================================= +Both sides (new :mod:`asyncio`) +=============================== .. automodule:: websockets.asyncio.connection diff --git a/docs/reference/new-asyncio/server.rst b/docs/reference/new-asyncio/server.rst index f3446fb8..c43673d3 100644 --- a/docs/reference/new-asyncio/server.rst +++ b/docs/reference/new-asyncio/server.rst @@ -1,5 +1,5 @@ -Server (:mod:`asyncio` - new) -============================= +Server (new :mod:`asyncio`) +=========================== .. automodule:: websockets.asyncio.server diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index dfa7065e..a1ba59a3 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -21,8 +21,8 @@ cryptocurrency css ctrl deserialize -django dev +django Dockerfile dyno formatter @@ -44,6 +44,7 @@ linkerd liveness lookups MiB +middleware mutex mypy nginx @@ -77,8 +78,8 @@ uple uvicorn uvloop virtualenv -WebSocket websocket +WebSocket websockets ws wsgi