picows 2.2.0__tar.gz → 2.3.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. {picows-2.2.0/picows.egg-info → picows-2.3.1}/PKG-INFO +1 -2
  2. {picows-2.2.0 → picows-2.3.1}/README.md +0 -1
  3. picows-2.3.1/examples/websocket_and_health_check.py +59 -0
  4. {picows-2.2.0 → picows-2.3.1}/picows/api.py +70 -136
  5. {picows-2.2.0 → picows-2.3.1}/picows/common.py +22 -0
  6. {picows-2.2.0 → picows-2.3.1}/picows/picows.pxd +3 -0
  7. {picows-2.2.0 → picows-2.3.1}/picows/picows.pyx +339 -220
  8. picows-2.3.1/picows/proxy.py +261 -0
  9. {picows-2.2.0 → picows-2.3.1}/picows/version.py +1 -1
  10. {picows-2.2.0 → picows-2.3.1/picows.egg-info}/PKG-INFO +1 -2
  11. {picows-2.2.0 → picows-2.3.1}/picows.egg-info/SOURCES.txt +4 -0
  12. {picows-2.2.0 → picows-2.3.1}/pyproject.toml +1 -1
  13. picows-2.3.1/tests/test_handshake_parsing.py +410 -0
  14. picows-2.3.1/tests/test_proxy.py +143 -0
  15. {picows-2.2.0 → picows-2.3.1}/tests/test_redirects_and_proxies.py +81 -8
  16. {picows-2.2.0 → picows-2.3.1}/tests/test_websockets_server_handshake.py +33 -0
  17. {picows-2.2.0 → picows-2.3.1}/tests/test_ws_logic.py +153 -8
  18. {picows-2.2.0 → picows-2.3.1}/tests/utils.py +11 -0
  19. {picows-2.2.0 → picows-2.3.1}/LICENSE +0 -0
  20. {picows-2.2.0 → picows-2.3.1}/MANIFEST.in +0 -0
  21. {picows-2.2.0 → picows-2.3.1}/examples/__init__.py +0 -0
  22. {picows-2.2.0 → picows-2.3.1}/examples/broadcast_client.py +0 -0
  23. {picows-2.2.0 → picows-2.3.1}/examples/broadcast_server.py +0 -0
  24. {picows-2.2.0 → picows-2.3.1}/examples/echo_client.py +0 -0
  25. {picows-2.2.0 → picows-2.3.1}/examples/echo_client_async_callbacks.py +0 -0
  26. {picows-2.2.0 → picows-2.3.1}/examples/echo_client_async_iteration.py +0 -0
  27. {picows-2.2.0 → picows-2.3.1}/examples/echo_client_cython.pyx +0 -0
  28. {picows-2.2.0 → picows-2.3.1}/examples/echo_client_cython_runner.py +0 -0
  29. {picows-2.2.0 → picows-2.3.1}/examples/echo_client_fragmented_msg.py +0 -0
  30. {picows-2.2.0 → picows-2.3.1}/examples/echo_client_threaded.py +0 -0
  31. {picows-2.2.0 → picows-2.3.1}/examples/echo_client_websockets.py +0 -0
  32. {picows-2.2.0 → picows-2.3.1}/examples/echo_server.py +0 -0
  33. {picows-2.2.0 → picows-2.3.1}/examples/echo_server_ssl.py +0 -0
  34. {picows-2.2.0 → picows-2.3.1}/examples/echo_server_threaded.py +0 -0
  35. {picows-2.2.0 → picows-2.3.1}/examples/echo_server_websockes.py +0 -0
  36. {picows-2.2.0 → picows-2.3.1}/examples/graceful_shutdown.py +0 -0
  37. {picows-2.2.0 → picows-2.3.1}/examples/okx_roundtrip_time.py +0 -0
  38. {picows-2.2.0 → picows-2.3.1}/examples/perf_test.py +0 -0
  39. {picows-2.2.0 → picows-2.3.1}/examples/reconnecting_client.py +0 -0
  40. {picows-2.2.0 → picows-2.3.1}/examples/slow_client_backpressure.py +0 -0
  41. {picows-2.2.0 → picows-2.3.1}/examples/subprotocol_negotiation.py +0 -0
  42. {picows-2.2.0 → picows-2.3.1}/llms.txt +0 -0
  43. {picows-2.2.0 → picows-2.3.1}/picows/__init__.py +0 -0
  44. {picows-2.2.0 → picows-2.3.1}/picows/compat.h +0 -0
  45. {picows-2.2.0 → picows-2.3.1}/picows/mask_avx2.c +0 -0
  46. {picows-2.2.0 → picows-2.3.1}/picows/mask_avx512.c +0 -0
  47. {picows-2.2.0 → picows-2.3.1}/picows/mask_dispatch.c +0 -0
  48. {picows-2.2.0 → picows-2.3.1}/picows/mask_neon.c +0 -0
  49. {picows-2.2.0 → picows-2.3.1}/picows/mask_sse2.c +0 -0
  50. {picows-2.2.0 → picows-2.3.1}/picows/picows.pyi +0 -0
  51. {picows-2.2.0 → picows-2.3.1}/picows/py.typed +0 -0
  52. {picows-2.2.0 → picows-2.3.1}/picows/url.py +0 -0
  53. {picows-2.2.0 → picows-2.3.1}/picows/websockets/__init__.py +0 -0
  54. {picows-2.2.0 → picows-2.3.1}/picows/websockets/asyncio/__init__.py +0 -0
  55. {picows-2.2.0 → picows-2.3.1}/picows/websockets/asyncio/client.py +0 -0
  56. {picows-2.2.0 → picows-2.3.1}/picows/websockets/asyncio/connection.py +0 -0
  57. {picows-2.2.0 → picows-2.3.1}/picows/websockets/asyncio/negotiation.py +0 -0
  58. {picows-2.2.0 → picows-2.3.1}/picows/websockets/asyncio/router.py +0 -0
  59. {picows-2.2.0 → picows-2.3.1}/picows/websockets/asyncio/server.py +0 -0
  60. {picows-2.2.0 → picows-2.3.1}/picows/websockets/asyncio/utils.py +0 -0
  61. {picows-2.2.0 → picows-2.3.1}/picows/websockets/compat.py +0 -0
  62. {picows-2.2.0 → picows-2.3.1}/picows/websockets/exceptions.py +0 -0
  63. {picows-2.2.0 → picows-2.3.1}/picows/websockets/protocol.py +0 -0
  64. {picows-2.2.0 → picows-2.3.1}/picows/websockets/typing.py +0 -0
  65. {picows-2.2.0 → picows-2.3.1}/picows.egg-info/dependency_links.txt +0 -0
  66. {picows-2.2.0 → picows-2.3.1}/picows.egg-info/requires.txt +0 -0
  67. {picows-2.2.0 → picows-2.3.1}/picows.egg-info/top_level.txt +0 -0
  68. {picows-2.2.0 → picows-2.3.1}/requirements.txt +0 -0
  69. {picows-2.2.0 → picows-2.3.1}/setup.cfg +0 -0
  70. {picows-2.2.0 → picows-2.3.1}/setup.py +0 -0
  71. {picows-2.2.0 → picows-2.3.1}/tests/__init__.py +0 -0
  72. {picows-2.2.0 → picows-2.3.1}/tests/conftest.py +0 -0
  73. {picows-2.2.0 → picows-2.3.1}/tests/fixtures.py +0 -0
  74. {picows-2.2.0 → picows-2.3.1}/tests/picows_test.crt +0 -0
  75. {picows-2.2.0 → picows-2.3.1}/tests/picows_test.key +0 -0
  76. {picows-2.2.0 → picows-2.3.1}/tests/test_autoping.py +0 -0
  77. {picows-2.2.0 → picows-2.3.1}/tests/test_basics.py +0 -0
  78. {picows-2.2.0 → picows-2.3.1}/tests/test_benchmarks.py +0 -0
  79. {picows-2.2.0 → picows-2.3.1}/tests/test_redirect_location.py +0 -0
  80. {picows-2.2.0 → picows-2.3.1}/tests/test_websockets_client.py +0 -0
  81. {picows-2.2.0 → picows-2.3.1}/tests/test_websockets_compat_types.py +0 -0
  82. {picows-2.2.0 → picows-2.3.1}/tests/test_websockets_exceptions.py +0 -0
  83. {picows-2.2.0 → picows-2.3.1}/tests/test_websockets_negotiation.py +0 -0
  84. {picows-2.2.0 → picows-2.3.1}/tests/test_websockets_ping_pong.py +0 -0
  85. {picows-2.2.0 → picows-2.3.1}/tests/test_websockets_recv.py +0 -0
  86. {picows-2.2.0 → picows-2.3.1}/tests/test_websockets_send.py +0 -0
  87. {picows-2.2.0 → picows-2.3.1}/tests/test_websockets_server.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: picows
3
- Version: 2.2.0
3
+ Version: 2.3.1
4
4
  Summary: Ultra-fast websocket client and server for asyncio
5
5
  Author-email: Taras Kozlov <tarasko.projects@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/tarasko/picows
@@ -43,7 +43,6 @@ Dynamic: license-file
43
43
  <a href='https://pypistats.org/packages/picows' target="_blank"><img alt='downloads' src='https://img.shields.io/pypi/dm/picows'></a>
44
44
  <a href='https://picows.readthedocs.io/en/latest/' target="_blank"><img alt='docs' src='https://readthedocs.org/projects/picows/badge/?version=latest'></a>
45
45
  <a href='https://codspeed.io/tarasko/picows?utm_source=badge' target="_blank"><img alt='codspeed' src='https://img.shields.io/endpoint?url=https://codspeed.io/badge.json'></a>
46
- <a href='https://deepwiki.com/tarasko/picows' target="_blank"><img alt='codspeed' src='https://deepwiki.com/badge.svg'></a>
47
46
  </p>
48
47
 
49
48
  <div align="center">
@@ -8,7 +8,6 @@
8
8
  <a href='https://pypistats.org/packages/picows' target="_blank"><img alt='downloads' src='https://img.shields.io/pypi/dm/picows'></a>
9
9
  <a href='https://picows.readthedocs.io/en/latest/' target="_blank"><img alt='docs' src='https://readthedocs.org/projects/picows/badge/?version=latest'></a>
10
10
  <a href='https://codspeed.io/tarasko/picows?utm_source=badge' target="_blank"><img alt='codspeed' src='https://img.shields.io/endpoint?url=https://codspeed.io/badge.json'></a>
11
- <a href='https://deepwiki.com/tarasko/picows' target="_blank"><img alt='codspeed' src='https://deepwiki.com/badge.svg'></a>
12
11
  </p>
13
12
 
14
13
  <div align="center">
@@ -0,0 +1,59 @@
1
+ # This example shows how to serve a WebSocket endpoint and an HTTP health check on the same port.
2
+
3
+ import asyncio
4
+ from logging import basicConfig
5
+
6
+ import picows
7
+ from picows import (
8
+ WSFrame,
9
+ WSListener,
10
+ WSMsgType,
11
+ WSTransport,
12
+ WSUpgradeRequest,
13
+ WSUpgradeResponse,
14
+ WSUpgradeResponseWithListener,
15
+ ws_create_server,
16
+ )
17
+
18
+
19
+ class EchoListener(WSListener):
20
+ def on_ws_frame(self, transport: WSTransport, frame: WSFrame):
21
+ if frame.msg_type == WSMsgType.CLOSE:
22
+ transport.send_close(frame.get_close_code(), frame.get_close_message())
23
+ transport.disconnect()
24
+ return
25
+
26
+ transport.send(
27
+ frame.msg_type,
28
+ frame.get_payload_as_memoryview(),
29
+ frame.fin,
30
+ frame.rsv1,
31
+ frame.rsv2,
32
+ frame.rsv3,
33
+ )
34
+
35
+
36
+ def create_health_check_response() -> WSUpgradeResponseWithListener:
37
+ response = WSUpgradeResponse.create_ok_response(b"I'm healthy")
38
+ return WSUpgradeResponseWithListener(response, None)
39
+
40
+
41
+ def listener_factory(request: WSUpgradeRequest):
42
+ if request.path == b"/health":
43
+ return create_health_check_response()
44
+ if request.path == b"/ws":
45
+ return EchoListener()
46
+ return None
47
+
48
+
49
+ async def main():
50
+ server = await ws_create_server(listener_factory, "127.0.0.1", 9001)
51
+ print("Health check: http://127.0.0.1:9001/health")
52
+ print("WebSocket endpoint: ws://127.0.0.1:9001/ws")
53
+ async with server:
54
+ await server.serve_forever()
55
+
56
+
57
+ if __name__ == "__main__":
58
+ basicConfig(level=picows.PICOWS_DEBUG_LL)
59
+ asyncio.run(main())
@@ -1,28 +1,24 @@
1
1
  import asyncio
2
- import socket
2
+ import ssl as ssl_module
3
3
  import urllib.parse
4
- from dataclasses import dataclass
5
4
  from functools import partial
6
- from inspect import isawaitable
7
5
  from logging import Logger, LoggerAdapter, getLogger
8
6
  from ssl import SSLContext
9
- from typing import Callable, Optional, Union, Dict, Any, Awaitable, cast, TYPE_CHECKING
7
+ from typing import Any, Awaitable, Callable, Optional, Union, cast, TYPE_CHECKING
10
8
 
11
- from python_socks.async_.asyncio import Proxy
12
-
13
- from .common import (WSHeadersLike, WSUpgradeRequest, WSUpgradeResponse, WSHost, WSPort,
9
+ from .proxy import (ConnectedTransport, WSSocketFactory,
10
+ connect_through_optional_proxy)
11
+ from .common import (WSHeadersLike, WSUpgradeRequest, WSUpgradeResponse,
14
12
  WSUpgradeResponseWithListener, WSHandshakeError)
15
13
  from .picows import (WSListener, WSTransport, WSAutoPingStrategy, # type: ignore [attr-defined]
16
14
  WSProtocol)
17
- from .url import parse_url, WSInvalidURL, WSParsedURL
18
-
15
+ from .url import parse_url, WSParsedURL
19
16
 
20
17
  WSListenerFactory = Union[
21
18
  Callable[[], WSListener],
22
19
  Callable[[WSUpgradeRequest, WSUpgradeResponse], WSListener],
23
20
  ]
24
21
  WSServerListenerFactory = Callable[[WSUpgradeRequest], Union[WSListener, WSUpgradeResponseWithListener, None]]
25
- WSSocketFactory = Callable[[WSParsedURL], Union[Optional[socket.socket], Awaitable[Optional[socket.socket]]]]
26
22
 
27
23
  if TYPE_CHECKING:
28
24
  _WSLoggerAdapter = LoggerAdapter[Any]
@@ -64,14 +60,6 @@ def _maybe_handle_redirect(exc: WSHandshakeError, old_parsed_url: WSParsedURL, m
64
60
  return parsed_url
65
61
 
66
62
 
67
- def _is_connected(sock: socket.socket) -> bool:
68
- try:
69
- sock.getpeername()
70
- return True
71
- except OSError:
72
- return False
73
-
74
-
75
63
  def _resolve_logger(
76
64
  logger_name: WSLoggerLike,
77
65
  default_suffix: str,
@@ -85,109 +73,6 @@ def _resolve_logger(
85
73
 
86
74
  return logger_name
87
75
 
88
- @dataclass
89
- class _ConnectedSocket:
90
- sock: Optional[socket.socket]
91
- host: Optional[WSHost]
92
- port: Optional[WSPort]
93
-
94
-
95
- async def _create_connected_socket(
96
- loop: asyncio.AbstractEventLoop,
97
- socket_factory: Optional[WSSocketFactory],
98
- parsed_url: WSParsedURL
99
- ) -> Optional[socket.socket]:
100
- if socket_factory is None:
101
- return None
102
-
103
- sock_or_awaitable = socket_factory(parsed_url)
104
- sock: Optional[socket.socket]
105
- if sock_or_awaitable is None or isinstance(sock_or_awaitable, socket.socket):
106
- sock = sock_or_awaitable
107
- elif isawaitable(sock_or_awaitable):
108
- sock = await sock_or_awaitable
109
- else:
110
- raise TypeError(f"user socket_factory() returned invalid type: {type(sock_or_awaitable).__name__}")
111
-
112
- if sock is not None:
113
- sock.setblocking(False)
114
- if sock is not None:
115
- if not _is_connected(sock):
116
- await loop.sock_connect(sock, (parsed_url.host, parsed_url.port))
117
-
118
- return sock
119
-
120
-
121
- async def _connect_through_optional_proxy(
122
- loop: asyncio.AbstractEventLoop,
123
- parsed_url: WSParsedURL,
124
- proxy_parsed_url: Optional[WSParsedURL],
125
- socket_factory: Optional[WSSocketFactory],
126
- ssl_context: Optional[Union[SSLContext, bool]],
127
- conn_kwargs: Dict[str, Any]
128
- ) -> _ConnectedSocket:
129
- if proxy_parsed_url is not None and proxy_parsed_url.scheme == "https":
130
- raise WSInvalidURL(proxy_parsed_url.url,
131
- "HTTPS proxy URL scheme is not supported, use http://, socks4:// or socks5://")
132
-
133
- if proxy_parsed_url is not None:
134
- proxy_obj = Proxy.from_url(proxy_parsed_url.url, loop=loop)
135
- proxy_socket = await _create_connected_socket(loop, socket_factory, proxy_parsed_url)
136
- if proxy_socket is not None:
137
- # It is so ugly that I have to use python_socks internals
138
- # I could not figure out how to pass existing connected socket using public
139
- # interface. Maybe I should just copy that part of the code?
140
-
141
- # Import everthing as local as possible
142
- # If imports will stop working, picows ws_connect will break but only
143
- # if user has passed proxy together with socket_factory
144
-
145
- from python_socks import ProxyError
146
- from python_socks._connectors.factory_async import create_connector
147
- from python_socks._protocols.errors import ReplyError
148
- from python_socks.async_.asyncio._stream import AsyncioSocketStream
149
-
150
- stream = AsyncioSocketStream(sock=proxy_socket, loop=loop)
151
-
152
- try:
153
- connector = create_connector(
154
- proxy_type=proxy_obj._proxy_type,
155
- username=proxy_obj._username,
156
- password=proxy_obj._password,
157
- rdns=proxy_obj._rdns,
158
- resolver=proxy_obj._resolver,
159
- )
160
- await connector.connect(
161
- stream=stream,
162
- host=parsed_url.host,
163
- port=parsed_url.port,
164
- )
165
- except ReplyError as e:
166
- await stream.close()
167
- raise ProxyError(e, error_code=e.error_code)
168
- except (asyncio.CancelledError, Exception):
169
- await stream.close()
170
- raise
171
- else:
172
- proxy_socket = await proxy_obj.connect(
173
- dest_host=parsed_url.host,
174
- dest_port=parsed_url.port,
175
- )
176
-
177
- if ssl_context and "server_hostname" not in conn_kwargs:
178
- conn_kwargs["server_hostname"] = parsed_url.host
179
-
180
- return _ConnectedSocket(proxy_socket, None, None)
181
- else:
182
- sock = await _create_connected_socket(loop, socket_factory, parsed_url)
183
- if sock is not None:
184
- if ssl_context and "server_hostname" not in conn_kwargs:
185
- conn_kwargs["server_hostname"] = parsed_url.host
186
-
187
- return _ConnectedSocket(sock, None, None)
188
- else:
189
- return _ConnectedSocket(None, parsed_url.host, parsed_url.port)
190
-
191
76
 
192
77
  async def ws_connect(ws_listener_factory: WSListenerFactory, # type: ignore [no-untyped-def]
193
78
  url: str,
@@ -205,6 +90,7 @@ async def ws_connect(ws_listener_factory: WSListenerFactory, # type: ignore [no-
205
90
  extra_headers: Optional[WSHeadersLike] = None,
206
91
  max_redirects: int = 5,
207
92
  proxy: Optional[str] = None,
93
+ proxy_ssl_context: Optional[SSLContext] = None,
208
94
  read_buffer_init_size: int = 16 * 1024,
209
95
  socket_factory: Optional[WSSocketFactory] = None,
210
96
  use_aiofastnet: Optional[bool] = None,
@@ -265,9 +151,11 @@ async def ws_connect(ws_listener_factory: WSListenerFactory, # type: ignore [no-
265
151
  :param max_redirects:
266
152
  * How many times we can follow HTTP redirects. Set to 0 in order to disable redirects.
267
153
  :param proxy:
268
- Optional proxy URL. Supported schemes are ``http://``, ``socks4://``
269
- and ``socks5://`` (including authenticated variants).
270
- HTTPS proxy scheme (``https://``) is currently not supported.
154
+ Optional proxy URL. Supported schemes are ``http://``, ``https://``,
155
+ ``socks4://`` and ``socks5://`` (including authenticated variants).
156
+ :param proxy_ssl_context:
157
+ Optional SSLContext for the TLS connection to an ``https://`` proxy.
158
+ A default client context is used when this isn't provided.
271
159
  :param read_buffer_init_size:
272
160
  Initial size (in bytes) of the internal read buffer.
273
161
  The buffer grows exponentially when incoming data does not fit.
@@ -313,10 +201,13 @@ async def ws_connect(ws_listener_factory: WSListenerFactory, # type: ignore [no-
313
201
  parsed_proxy_url = parse_url(proxy, False) if proxy is not None else None
314
202
  loop = asyncio.get_running_loop()
315
203
 
204
+ start_tls: Callable[..., Awaitable[Optional[asyncio.Transport]]]
316
205
  if use_aiofastnet:
317
206
  create_connection = partial(aiofastnet.create_connection, loop)
207
+ start_tls = partial(aiofastnet.start_tls, loop)
318
208
  else:
319
209
  create_connection = loop.create_connection # type: ignore [assignment]
210
+ start_tls = loop.start_tls
320
211
 
321
212
  ssl: Optional[Union[SSLContext, bool]]
322
213
  while True:
@@ -349,17 +240,48 @@ async def ws_connect(ws_listener_factory: WSListenerFactory, # type: ignore [no-
349
240
 
350
241
  try:
351
242
  conn_kwargs = dict(kwargs)
352
- conn_socket = await _connect_through_optional_proxy(
353
- loop, parsed_url, parsed_proxy_url, socket_factory, ssl, conn_kwargs)
354
-
355
- (_, ws_protocol) = await create_connection(
356
- ws_protocol_factory,
357
- conn_socket.host,
358
- conn_socket.port,
359
- ssl=ssl,
360
- sock=conn_socket.sock,
361
- **conn_kwargs
362
- )
243
+ connection = await connect_through_optional_proxy(
244
+ loop, parsed_url, parsed_proxy_url, socket_factory, ssl,
245
+ proxy_ssl_context, conn_kwargs, create_connection)
246
+
247
+ if isinstance(connection, ConnectedTransport):
248
+ transport = connection.transport
249
+ try:
250
+ if ssl is not None:
251
+ target_ssl_context = ssl if isinstance(ssl, SSLContext) \
252
+ else ssl_module.create_default_context()
253
+ server_hostname = conn_kwargs.get("server_hostname", parsed_url.host)
254
+ start_tls_kwargs = {
255
+ key: value for key, value in conn_kwargs.items()
256
+ if key.startswith("ssl_")
257
+ }
258
+
259
+ new_transport = await start_tls(
260
+ transport, transport.get_protocol(), target_ssl_context,
261
+ server_hostname=server_hostname, **start_tls_kwargs)
262
+
263
+ # asyncio docs says that start_tls can potentially return None if transport
264
+ # is already closing
265
+ if new_transport is None:
266
+ raise ConnectionError("connection closed while starting TLS")
267
+
268
+ transport = new_transport
269
+
270
+ ws_protocol = ws_protocol_factory()
271
+ transport.set_protocol(ws_protocol)
272
+ ws_protocol.connection_made(transport)
273
+ except (asyncio.CancelledError, Exception):
274
+ transport.abort()
275
+ raise
276
+ else:
277
+ (_, ws_protocol) = await create_connection(
278
+ ws_protocol_factory,
279
+ connection.host,
280
+ connection.port,
281
+ ssl=ssl,
282
+ sock=connection.sock,
283
+ **conn_kwargs
284
+ )
363
285
 
364
286
  await ws_protocol.wait_until_handshake_complete()
365
287
  return ws_protocol.transport, ws_protocol.listener
@@ -396,12 +318,24 @@ async def ws_create_server(ws_listener_factory: WSServerListenerFactory,
396
318
  It has a few extra parameters to control WebSocket behavior.
397
319
 
398
320
  :param ws_listener_factory:
399
- A factory function that accepts WSUpgradeRequest object and returns one of:
321
+ A factory function that accepts a parsed WSUpgradeRequest object before
322
+ WebSocket upgrade headers are validated and returns one of:
400
323
 
401
324
  * User handler object. A standard 101 response will be sent to the client.
402
325
  * WSUpgradeResponseWithListener object. This allows to send a custom response with extra headers and an optional body.
403
326
  * None. In such case 404 Not Found response will be sent and the client will be disconnected.
404
327
 
328
+ Returning a response whose status isn't 101 Switching Protocols sends
329
+ that response with ``Connection: close``, without validating WebSocket
330
+ upgrade headers, and then disconnects the client. Any user-provided
331
+ ``Connection`` response header is overridden. This can be used for small
332
+ HTTP endpoints such as health checks. A 101 response is sent only after
333
+ validating the WebSocket upgrade request.
334
+
335
+ Incoming requests must be HTTP/1.1 GET requests without a body.
336
+ ``Content-Length`` may be omitted or set to zero; ``Transfer-Encoding``
337
+ and non-zero content lengths are rejected.
338
+
405
339
  The user handler must derive from WSListener and is responsible for
406
340
  processing incoming data.
407
341
 
@@ -45,6 +45,28 @@ class WSUpgradeResponse:
45
45
  headers : CIMultiDict[str]
46
46
  body : Optional[bytes]
47
47
 
48
+ @staticmethod
49
+ def create_ok_response(body: Optional[bytes] = None,
50
+ extra_headers: Optional[WSHeadersLike] = None) -> Any:
51
+ """
52
+ Create an HTTP 200 OK response.
53
+
54
+ :param body: optional response body
55
+ :param extra_headers: optional additional headers. Content-Type defaults
56
+ to ``text/plain; charset=utf-8`` when it isn't provided.
57
+ :return: a new WSUpgradeResponse object
58
+ """
59
+ self = WSUpgradeResponse()
60
+ self.version = b"HTTP/1.1"
61
+ self.status = HTTPStatus.OK
62
+ self.headers = CIMultiDict()
63
+ add_extra_headers(self.headers, extra_headers)
64
+ if "Content-Type" not in self.headers:
65
+ self.headers["Content-Type"] = "text/plain; charset=utf-8"
66
+ self.body = body
67
+
68
+ return self
69
+
48
70
  @staticmethod
49
71
  def create_error_response(status: Union[int, HTTPStatus],
50
72
  body: Optional[bytes] = None,
@@ -97,6 +97,9 @@ cdef class WSFrame:
97
97
 
98
98
 
99
99
  cpdef WSFrame _make_test_ws_frame(WSMsgType msg_type, bytes payload, bint fin, bint rsv1)
100
+ cpdef object _parse_http_request(bytes raw_headers)
101
+ cpdef bytes _validate_upgrade_request(object upgrade_request)
102
+ cpdef object _parse_upgrade_response(bytes data, bytes websocket_key_b64)
100
103
 
101
104
 
102
105
  cdef class WSTransport: