|
5 | 5 | import urllib.parse
|
6 | 6 | from typing import Any, Awaitable, Callable, Literal
|
7 | 7 |
|
8 |
| -from werkzeug.exceptions import NotFound |
9 |
| -from werkzeug.routing import Map, RequestRedirect |
10 |
| - |
11 | 8 | from ..http11 import Request, Response
|
12 | 9 | from .server import Server, ServerConnection, serve
|
13 | 10 |
|
14 | 11 |
|
15 | 12 | __all__ = ["route", "unix_route", "Router"]
|
16 | 13 |
|
17 | 14 |
|
18 |
| -class Router: |
19 |
| - """WebSocket router supporting :func:`route`.""" |
| 15 | +try: |
| 16 | + from werkzeug.exceptions import NotFound |
| 17 | + from werkzeug.routing import Map, RequestRedirect |
| 18 | + |
| 19 | +except ImportError: |
20 | 20 |
|
21 |
| - def __init__( |
22 |
| - self, |
| 21 | + def route( |
23 | 22 | url_map: Map,
|
| 23 | + *args: Any, |
24 | 24 | server_name: str | None = None,
|
25 |
| - url_scheme: str = "ws", |
26 |
| - ) -> None: |
27 |
| - self.url_map = url_map |
28 |
| - self.server_name = server_name |
29 |
| - self.url_scheme = url_scheme |
30 |
| - for rule in self.url_map.iter_rules(): |
31 |
| - rule.websocket = True |
32 |
| - |
33 |
| - def get_server_name(self, connection: ServerConnection, request: Request) -> str: |
34 |
| - if self.server_name is None: |
35 |
| - return request.headers["Host"] |
36 |
| - else: |
37 |
| - return self.server_name |
38 |
| - |
39 |
| - def redirect(self, connection: ServerConnection, url: str) -> Response: |
40 |
| - response = connection.respond(http.HTTPStatus.FOUND, f"Found at {url}") |
41 |
| - response.headers["Location"] = url |
42 |
| - return response |
43 |
| - |
44 |
| - def not_found(self, connection: ServerConnection) -> Response: |
45 |
| - return connection.respond(http.HTTPStatus.NOT_FOUND, "Not Found") |
46 |
| - |
47 |
| - def route_request( |
48 |
| - self, connection: ServerConnection, request: Request |
49 |
| - ) -> Response | None: |
50 |
| - """Route incoming request.""" |
51 |
| - url_map_adapter = self.url_map.bind( |
52 |
| - server_name=self.get_server_name(connection, request), |
53 |
| - url_scheme=self.url_scheme, |
54 |
| - ) |
55 |
| - try: |
56 |
| - parsed = urllib.parse.urlparse(request.path) |
57 |
| - handler, kwargs = url_map_adapter.match( |
58 |
| - path_info=parsed.path, |
59 |
| - query_args=parsed.query, |
60 |
| - ) |
61 |
| - except RequestRedirect as redirect: |
62 |
| - return self.redirect(connection, redirect.new_url) |
63 |
| - except NotFound: |
64 |
| - return self.not_found(connection) |
65 |
| - connection.handler, connection.handler_kwargs = handler, kwargs |
66 |
| - return None |
| 25 | + ssl: ssl_module.SSLContext | Literal[True] | None = None, |
| 26 | + create_router: type[Router] | None = None, |
| 27 | + **kwargs: Any, |
| 28 | + ) -> Awaitable[Server]: |
| 29 | + raise ImportError("route() requires werkzeug") |
67 | 30 |
|
68 |
| - async def handler(self, connection: ServerConnection) -> None: |
69 |
| - """Handle a connection.""" |
70 |
| - return await connection.handler(connection, **connection.handler_kwargs) |
71 |
| - |
72 |
| - |
73 |
| -def route( |
74 |
| - url_map: Map, |
75 |
| - *args: Any, |
76 |
| - server_name: str | None = None, |
77 |
| - ssl: ssl_module.SSLContext | Literal[True] | None = None, |
78 |
| - create_router: type[Router] | None = None, |
79 |
| - **kwargs: Any, |
80 |
| -) -> Awaitable[Server]: |
81 |
| - """ |
82 |
| - Create a WebSocket server dispatching connections to different handlers. |
83 |
| -
|
84 |
| - This feature requires the third-party library `werkzeug`_: |
| 31 | + def unix_route( |
| 32 | + url_map: Map, |
| 33 | + path: str | None = None, |
| 34 | + **kwargs: Any, |
| 35 | + ) -> Awaitable[Server]: |
| 36 | + raise ImportError("unix_route() requires werkzeug") |
| 37 | + |
| 38 | +else: |
| 39 | + |
| 40 | + class Router: |
| 41 | + """WebSocket router supporting :func:`route`.""" |
| 42 | + |
| 43 | + def __init__( |
| 44 | + self, |
| 45 | + url_map: Map, |
| 46 | + server_name: str | None = None, |
| 47 | + url_scheme: str = "ws", |
| 48 | + ) -> None: |
| 49 | + self.url_map = url_map |
| 50 | + self.server_name = server_name |
| 51 | + self.url_scheme = url_scheme |
| 52 | + for rule in self.url_map.iter_rules(): |
| 53 | + rule.websocket = True |
| 54 | + |
| 55 | + def get_server_name( |
| 56 | + self, connection: ServerConnection, request: Request |
| 57 | + ) -> str: |
| 58 | + if self.server_name is None: |
| 59 | + return request.headers["Host"] |
| 60 | + else: |
| 61 | + return self.server_name |
| 62 | + |
| 63 | + def redirect(self, connection: ServerConnection, url: str) -> Response: |
| 64 | + response = connection.respond(http.HTTPStatus.FOUND, f"Found at {url}") |
| 65 | + response.headers["Location"] = url |
| 66 | + return response |
| 67 | + |
| 68 | + def not_found(self, connection: ServerConnection) -> Response: |
| 69 | + return connection.respond(http.HTTPStatus.NOT_FOUND, "Not Found") |
| 70 | + |
| 71 | + def route_request( |
| 72 | + self, connection: ServerConnection, request: Request |
| 73 | + ) -> Response | None: |
| 74 | + """Route incoming request.""" |
| 75 | + url_map_adapter = self.url_map.bind( |
| 76 | + server_name=self.get_server_name(connection, request), |
| 77 | + url_scheme=self.url_scheme, |
| 78 | + ) |
| 79 | + try: |
| 80 | + parsed = urllib.parse.urlparse(request.path) |
| 81 | + handler, kwargs = url_map_adapter.match( |
| 82 | + path_info=parsed.path, |
| 83 | + query_args=parsed.query, |
| 84 | + ) |
| 85 | + except RequestRedirect as redirect: |
| 86 | + return self.redirect(connection, redirect.new_url) |
| 87 | + except NotFound: |
| 88 | + return self.not_found(connection) |
| 89 | + connection.handler, connection.handler_kwargs = handler, kwargs |
| 90 | + return None |
| 91 | + |
| 92 | + async def handler(self, connection: ServerConnection) -> None: |
| 93 | + """Handle a connection.""" |
| 94 | + return await connection.handler(connection, **connection.handler_kwargs) |
| 95 | + |
| 96 | + def route( |
| 97 | + url_map: Map, |
| 98 | + *args: Any, |
| 99 | + server_name: str | None = None, |
| 100 | + ssl: ssl_module.SSLContext | Literal[True] | None = None, |
| 101 | + create_router: type[Router] | None = None, |
| 102 | + **kwargs: Any, |
| 103 | + ) -> Awaitable[Server]: |
| 104 | + """ |
| 105 | + Create a WebSocket server dispatching connections to different handlers. |
85 | 106 |
|
86 |
| - .. code-block:: console |
| 107 | + This feature requires the third-party library `werkzeug`_: |
87 | 108 |
|
88 |
| - $ pip install werkzeug |
| 109 | + .. code-block:: console |
89 | 110 |
|
90 |
| - .. _werkzeug: https://werkzeug.palletsprojects.com/ |
| 111 | + $ pip install werkzeug |
91 | 112 |
|
92 |
| - :func:`route` accepts the same arguments as |
93 |
| - :func:`~websockets.sync.server.serve`, except as described below. |
| 113 | + .. _werkzeug: https://werkzeug.palletsprojects.com/ |
94 | 114 |
|
95 |
| - The first argument is a :class:`werkzeug.routing.Map` that maps URL patterns |
96 |
| - to connection handlers. In addition to the connection, handlers receive |
97 |
| - parameters captured in the URL as keyword arguments. |
| 115 | + :func:`route` accepts the same arguments as |
| 116 | + :func:`~websockets.sync.server.serve`, except as described below. |
98 | 117 |
|
99 |
| - Here's an example:: |
| 118 | + The first argument is a :class:`werkzeug.routing.Map` that maps URL patterns |
| 119 | + to connection handlers. In addition to the connection, handlers receive |
| 120 | + parameters captured in the URL as keyword arguments. |
100 | 121 |
|
| 122 | + Here's an example:: |
101 | 123 |
|
102 |
| - from websockets.asyncio.router import route |
103 |
| - from werkzeug.routing import Map, Rule |
104 | 124 |
|
105 |
| - async def channel_handler(websocket, channel_id): |
106 |
| - ... |
| 125 | + from websockets.asyncio.router import route |
| 126 | + from werkzeug.routing import Map, Rule |
107 | 127 |
|
108 |
| - url_map = Map([ |
109 |
| - Rule("/channel/<uuid:channel_id>", endpoint=channel_handler), |
110 |
| - ... |
111 |
| - ]) |
| 128 | + async def channel_handler(websocket, channel_id): |
| 129 | + ... |
112 | 130 |
|
113 |
| - # set this future to exit the server |
114 |
| - stop = asyncio.get_running_loop().create_future() |
| 131 | + url_map = Map([ |
| 132 | + Rule("/channel/<uuid:channel_id>", endpoint=channel_handler), |
| 133 | + ... |
| 134 | + ]) |
115 | 135 |
|
116 |
| - async with route(url_map, ...) as server: |
117 |
| - await stop |
| 136 | + # set this future to exit the server |
| 137 | + stop = asyncio.get_running_loop().create_future() |
118 | 138 |
|
| 139 | + async with route(url_map, ...) as server: |
| 140 | + await stop |
119 | 141 |
|
120 |
| - Refer to the documentation of :mod:`werkzeug.routing` for details. |
121 | 142 |
|
122 |
| - If you define redirects with ``Rule(..., redirect_to=...)`` in the URL map, |
123 |
| - when the server runs behind a reverse proxy that modifies the ``Host`` |
124 |
| - header or terminates TLS, you need additional configuration: |
| 143 | + Refer to the documentation of :mod:`werkzeug.routing` for details. |
125 | 144 |
|
126 |
| - * Set ``server_name`` to the name of the server as seen by clients. When not |
127 |
| - provided, websockets uses the value of the ``Host`` header. |
| 145 | + If you define redirects with ``Rule(..., redirect_to=...)`` in the URL map, |
| 146 | + when the server runs behind a reverse proxy that modifies the ``Host`` |
| 147 | + header or terminates TLS, you need additional configuration: |
128 | 148 |
|
129 |
| - * Set ``ssl=True`` to generate ``wss://`` URIs without actually enabling |
130 |
| - TLS. Under the hood, this bind the URL map with a ``url_scheme`` of |
131 |
| - ``wss://`` instead of ``ws://``. |
| 149 | + * Set ``server_name`` to the name of the server as seen by clients. When not |
| 150 | + provided, websockets uses the value of the ``Host`` header. |
132 | 151 |
|
133 |
| - There is no need to specify ``websocket=True`` in each rule. It is added |
134 |
| - automatically. |
| 152 | + * Set ``ssl=True`` to generate ``wss://`` URIs without actually enabling |
| 153 | + TLS. Under the hood, this bind the URL map with a ``url_scheme`` of |
| 154 | + ``wss://`` instead of ``ws://``. |
135 | 155 |
|
136 |
| - Args: |
137 |
| - url_map: Mapping of URL patterns to connection handlers. |
138 |
| - server_name: Name of the server as seen by clients. If :obj:`None`, |
139 |
| - websockets uses the value of the ``Host`` header. |
140 |
| - ssl: Configuration for enabling TLS on the connection. Set it to |
141 |
| - :obj:`True` if a reverse proxy terminates TLS connections. |
142 |
| - create_router: Factory for the :class:`Router` dispatching requests to |
143 |
| - handlers. Set it to a wrapper or a subclass to customize routing. |
| 156 | + There is no need to specify ``websocket=True`` in each rule. It is added |
| 157 | + automatically. |
144 | 158 |
|
145 |
| - """ |
146 |
| - url_scheme = "ws" if ssl is None else "wss" |
147 |
| - if ssl is not True and ssl is not None: |
148 |
| - kwargs["ssl"] = ssl |
| 159 | + Args: |
| 160 | + url_map: Mapping of URL patterns to connection handlers. |
| 161 | + server_name: Name of the server as seen by clients. If :obj:`None`, |
| 162 | + websockets uses the value of the ``Host`` header. |
| 163 | + ssl: Configuration for enabling TLS on the connection. Set it to |
| 164 | + :obj:`True` if a reverse proxy terminates TLS connections. |
| 165 | + create_router: Factory for the :class:`Router` dispatching requests to |
| 166 | + handlers. Set it to a wrapper or a subclass to customize routing. |
149 | 167 |
|
150 |
| - if create_router is None: |
151 |
| - create_router = Router |
| 168 | + """ |
| 169 | + url_scheme = "ws" if ssl is None else "wss" |
| 170 | + if ssl is not True and ssl is not None: |
| 171 | + kwargs["ssl"] = ssl |
152 | 172 |
|
153 |
| - router = create_router(url_map, server_name, url_scheme) |
| 173 | + if create_router is None: |
| 174 | + create_router = Router |
154 | 175 |
|
155 |
| - _process_request: ( |
156 |
| - Callable[ |
157 |
| - [ServerConnection, Request], |
158 |
| - Awaitable[Response | None] | Response | None, |
159 |
| - ] |
160 |
| - | None |
161 |
| - ) = kwargs.pop("process_request", None) |
162 |
| - if _process_request is None: |
163 |
| - process_request: Callable[ |
164 |
| - [ServerConnection, Request], |
165 |
| - Awaitable[Response | None] | Response | None, |
166 |
| - ] = router.route_request |
167 |
| - else: |
| 176 | + router = create_router(url_map, server_name, url_scheme) |
168 | 177 |
|
169 |
| - async def process_request( |
170 |
| - connection: ServerConnection, request: Request |
171 |
| - ) -> Response | None: |
172 |
| - response = _process_request(connection, request) |
173 |
| - if isinstance(response, Awaitable): |
174 |
| - response = await response |
175 |
| - if response is not None: |
176 |
| - return response |
177 |
| - return router.route_request(connection, request) |
| 178 | + _process_request: ( |
| 179 | + Callable[ |
| 180 | + [ServerConnection, Request], |
| 181 | + Awaitable[Response | None] | Response | None, |
| 182 | + ] |
| 183 | + | None |
| 184 | + ) = kwargs.pop("process_request", None) |
| 185 | + if _process_request is None: |
| 186 | + process_request: Callable[ |
| 187 | + [ServerConnection, Request], |
| 188 | + Awaitable[Response | None] | Response | None, |
| 189 | + ] = router.route_request |
| 190 | + else: |
178 | 191 |
|
179 |
| - return serve(router.handler, *args, process_request=process_request, **kwargs) |
| 192 | + async def process_request( |
| 193 | + connection: ServerConnection, request: Request |
| 194 | + ) -> Response | None: |
| 195 | + response = _process_request(connection, request) |
| 196 | + if isinstance(response, Awaitable): |
| 197 | + response = await response |
| 198 | + if response is not None: |
| 199 | + return response |
| 200 | + return router.route_request(connection, request) |
180 | 201 |
|
| 202 | + return serve(router.handler, *args, process_request=process_request, **kwargs) |
181 | 203 |
|
182 |
| -def unix_route( |
183 |
| - url_map: Map, |
184 |
| - path: str | None = None, |
185 |
| - **kwargs: Any, |
186 |
| -) -> Awaitable[Server]: |
187 |
| - """ |
188 |
| - Create a WebSocket Unix server dispatching connections to different handlers. |
| 204 | + def unix_route( |
| 205 | + url_map: Map, |
| 206 | + path: str | None = None, |
| 207 | + **kwargs: Any, |
| 208 | + ) -> Awaitable[Server]: |
| 209 | + """ |
| 210 | + Create a WebSocket Unix server dispatching connections to different handlers. |
189 | 211 |
|
190 |
| - :func:`unix_route` combines the behaviors of :func:`route` and |
191 |
| - :func:`~websockets.asyncio.server.unix_serve`. |
| 212 | + :func:`unix_route` combines the behaviors of :func:`route` and |
| 213 | + :func:`~websockets.asyncio.server.unix_serve`. |
192 | 214 |
|
193 |
| - Args: |
194 |
| - url_map: Mapping of URL patterns to connection handlers. |
195 |
| - path: File system path to the Unix socket. |
| 215 | + Args: |
| 216 | + url_map: Mapping of URL patterns to connection handlers. |
| 217 | + path: File system path to the Unix socket. |
196 | 218 |
|
197 |
| - """ |
198 |
| - return route(url_map, unix=True, path=path, **kwargs) |
| 219 | + """ |
| 220 | + return route(url_map, unix=True, path=path, **kwargs) |
0 commit comments