uvicorn 0.24.0.post1__tar.gz → 0.25.0__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 (45) hide show
  1. {uvicorn-0.24.0.post1 → uvicorn-0.25.0}/PKG-INFO +3 -3
  2. {uvicorn-0.24.0.post1 → uvicorn-0.25.0}/pyproject.toml +1 -1
  3. {uvicorn-0.24.0.post1 → uvicorn-0.25.0}/uvicorn/__init__.py +1 -1
  4. {uvicorn-0.24.0.post1 → uvicorn-0.25.0}/uvicorn/_types.py +31 -13
  5. {uvicorn-0.24.0.post1 → uvicorn-0.25.0}/uvicorn/config.py +3 -3
  6. {uvicorn-0.24.0.post1 → uvicorn-0.25.0}/uvicorn/main.py +5 -5
  7. {uvicorn-0.24.0.post1 → uvicorn-0.25.0}/uvicorn/protocols/websockets/websockets_impl.py +43 -13
  8. {uvicorn-0.24.0.post1 → uvicorn-0.25.0}/uvicorn/protocols/websockets/wsproto_impl.py +59 -15
  9. {uvicorn-0.24.0.post1 → uvicorn-0.25.0}/uvicorn/supervisors/watchfilesreload.py +3 -0
  10. {uvicorn-0.24.0.post1 → uvicorn-0.25.0}/uvicorn/supervisors/watchgodreload.py +3 -0
  11. {uvicorn-0.24.0.post1 → uvicorn-0.25.0}/.gitignore +0 -0
  12. {uvicorn-0.24.0.post1 → uvicorn-0.25.0}/LICENSE.md +0 -0
  13. {uvicorn-0.24.0.post1 → uvicorn-0.25.0}/README.md +0 -0
  14. {uvicorn-0.24.0.post1 → uvicorn-0.25.0}/uvicorn/__main__.py +0 -0
  15. {uvicorn-0.24.0.post1 → uvicorn-0.25.0}/uvicorn/_subprocess.py +0 -0
  16. {uvicorn-0.24.0.post1 → uvicorn-0.25.0}/uvicorn/importer.py +0 -0
  17. {uvicorn-0.24.0.post1 → uvicorn-0.25.0}/uvicorn/lifespan/__init__.py +0 -0
  18. {uvicorn-0.24.0.post1 → uvicorn-0.25.0}/uvicorn/lifespan/off.py +0 -0
  19. {uvicorn-0.24.0.post1 → uvicorn-0.25.0}/uvicorn/lifespan/on.py +0 -0
  20. {uvicorn-0.24.0.post1 → uvicorn-0.25.0}/uvicorn/logging.py +0 -0
  21. {uvicorn-0.24.0.post1 → uvicorn-0.25.0}/uvicorn/loops/__init__.py +0 -0
  22. {uvicorn-0.24.0.post1 → uvicorn-0.25.0}/uvicorn/loops/asyncio.py +0 -0
  23. {uvicorn-0.24.0.post1 → uvicorn-0.25.0}/uvicorn/loops/auto.py +0 -0
  24. {uvicorn-0.24.0.post1 → uvicorn-0.25.0}/uvicorn/loops/uvloop.py +0 -0
  25. {uvicorn-0.24.0.post1 → uvicorn-0.25.0}/uvicorn/middleware/__init__.py +0 -0
  26. {uvicorn-0.24.0.post1 → uvicorn-0.25.0}/uvicorn/middleware/asgi2.py +0 -0
  27. {uvicorn-0.24.0.post1 → uvicorn-0.25.0}/uvicorn/middleware/message_logger.py +0 -0
  28. {uvicorn-0.24.0.post1 → uvicorn-0.25.0}/uvicorn/middleware/proxy_headers.py +0 -0
  29. {uvicorn-0.24.0.post1 → uvicorn-0.25.0}/uvicorn/middleware/wsgi.py +0 -0
  30. {uvicorn-0.24.0.post1 → uvicorn-0.25.0}/uvicorn/protocols/__init__.py +0 -0
  31. {uvicorn-0.24.0.post1 → uvicorn-0.25.0}/uvicorn/protocols/http/__init__.py +0 -0
  32. {uvicorn-0.24.0.post1 → uvicorn-0.25.0}/uvicorn/protocols/http/auto.py +0 -0
  33. {uvicorn-0.24.0.post1 → uvicorn-0.25.0}/uvicorn/protocols/http/flow_control.py +0 -0
  34. {uvicorn-0.24.0.post1 → uvicorn-0.25.0}/uvicorn/protocols/http/h11_impl.py +0 -0
  35. {uvicorn-0.24.0.post1 → uvicorn-0.25.0}/uvicorn/protocols/http/httptools_impl.py +0 -0
  36. {uvicorn-0.24.0.post1 → uvicorn-0.25.0}/uvicorn/protocols/utils.py +0 -0
  37. {uvicorn-0.24.0.post1 → uvicorn-0.25.0}/uvicorn/protocols/websockets/__init__.py +0 -0
  38. {uvicorn-0.24.0.post1 → uvicorn-0.25.0}/uvicorn/protocols/websockets/auto.py +0 -0
  39. {uvicorn-0.24.0.post1 → uvicorn-0.25.0}/uvicorn/py.typed +0 -0
  40. {uvicorn-0.24.0.post1 → uvicorn-0.25.0}/uvicorn/server.py +0 -0
  41. {uvicorn-0.24.0.post1 → uvicorn-0.25.0}/uvicorn/supervisors/__init__.py +0 -0
  42. {uvicorn-0.24.0.post1 → uvicorn-0.25.0}/uvicorn/supervisors/basereload.py +0 -0
  43. {uvicorn-0.24.0.post1 → uvicorn-0.25.0}/uvicorn/supervisors/multiprocess.py +0 -0
  44. {uvicorn-0.24.0.post1 → uvicorn-0.25.0}/uvicorn/supervisors/statreload.py +0 -0
  45. {uvicorn-0.24.0.post1 → uvicorn-0.25.0}/uvicorn/workers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: uvicorn
3
- Version: 0.24.0.post1
3
+ Version: 0.25.0
4
4
  Summary: The lightning-fast ASGI server.
5
5
  Project-URL: Changelog, https://github.com/encode/uvicorn/blob/master/CHANGELOG.md
6
6
  Project-URL: Funding, https://github.com/sponsors/encode
@@ -28,11 +28,11 @@ Requires-Dist: click>=7.0
28
28
  Requires-Dist: h11>=0.8
29
29
  Requires-Dist: typing-extensions>=4.0; python_version < '3.11'
30
30
  Provides-Extra: standard
31
- Requires-Dist: colorama>=0.4; sys_platform == 'win32' and extra == 'standard'
31
+ Requires-Dist: colorama>=0.4; (sys_platform == 'win32') and extra == 'standard'
32
32
  Requires-Dist: httptools>=0.5.0; extra == 'standard'
33
33
  Requires-Dist: python-dotenv>=0.13; extra == 'standard'
34
34
  Requires-Dist: pyyaml>=5.1; extra == 'standard'
35
- Requires-Dist: uvloop!=0.15.0,!=0.15.1,>=0.14.0; sys_platform != 'win32' and (sys_platform != 'cygwin' and platform_python_implementation != 'PyPy') and extra == 'standard'
35
+ Requires-Dist: uvloop!=0.15.0,!=0.15.1,>=0.14.0; (sys_platform != 'win32' and (sys_platform != 'cygwin' and platform_python_implementation != 'PyPy')) and extra == 'standard'
36
36
  Requires-Dist: watchfiles>=0.13; extra == 'standard'
37
37
  Requires-Dist: websockets>=10.4; extra == 'standard'
38
38
  Description-Content-Type: text/markdown
@@ -64,7 +64,7 @@ include = ["/uvicorn"]
64
64
  select = ["E", "F", "I"]
65
65
  ignore = ["B904", "B028"]
66
66
 
67
- [tool.ruff.isort]
67
+ [tool.ruff.lint.isort]
68
68
  combine-as-imports = true
69
69
 
70
70
  [tool.mypy]
@@ -1,5 +1,5 @@
1
1
  from uvicorn.config import Config
2
2
  from uvicorn.main import Server, main, run
3
3
 
4
- __version__ = "0.24.0.post1"
4
+ __version__ = "0.25.0"
5
5
  __all__ = ["main", "run", "Config", "Server"]
@@ -124,14 +124,14 @@ class HTTPResponseDebugEvent(TypedDict):
124
124
  class HTTPResponseStartEvent(TypedDict):
125
125
  type: Literal["http.response.start"]
126
126
  status: int
127
- headers: Iterable[Tuple[bytes, bytes]]
127
+ headers: NotRequired[Iterable[Tuple[bytes, bytes]]]
128
128
  trailers: NotRequired[bool]
129
129
 
130
130
 
131
131
  class HTTPResponseBodyEvent(TypedDict):
132
132
  type: Literal["http.response.body"]
133
133
  body: bytes
134
- more_body: bool
134
+ more_body: NotRequired[bool]
135
135
 
136
136
 
137
137
  class HTTPResponseTrailersEvent(TypedDict):
@@ -156,20 +156,38 @@ class WebSocketConnectEvent(TypedDict):
156
156
 
157
157
  class WebSocketAcceptEvent(TypedDict):
158
158
  type: Literal["websocket.accept"]
159
- subprotocol: Optional[str]
160
- headers: Iterable[Tuple[bytes, bytes]]
159
+ subprotocol: NotRequired[Optional[str]]
160
+ headers: NotRequired[Iterable[Tuple[bytes, bytes]]]
161
+
162
+
163
+ class _WebSocketReceiveEventBytes(TypedDict):
164
+ type: Literal["websocket.receive"]
165
+ bytes: bytes
166
+ text: NotRequired[None]
161
167
 
162
168
 
163
- class WebSocketReceiveEvent(TypedDict):
169
+ class _WebSocketReceiveEventText(TypedDict):
164
170
  type: Literal["websocket.receive"]
165
- bytes: Optional[bytes]
166
- text: Optional[str]
171
+ bytes: NotRequired[None]
172
+ text: str
173
+
174
+
175
+ WebSocketReceiveEvent = Union[_WebSocketReceiveEventBytes, _WebSocketReceiveEventText]
167
176
 
168
177
 
169
- class WebSocketSendEvent(TypedDict):
178
+ class _WebSocketSendEventBytes(TypedDict):
170
179
  type: Literal["websocket.send"]
171
- bytes: Optional[bytes]
172
- text: Optional[str]
180
+ bytes: bytes
181
+ text: NotRequired[None]
182
+
183
+
184
+ class _WebSocketSendEventText(TypedDict):
185
+ type: Literal["websocket.send"]
186
+ bytes: NotRequired[None]
187
+ text: str
188
+
189
+
190
+ WebSocketSendEvent = Union[_WebSocketSendEventBytes, _WebSocketSendEventText]
173
191
 
174
192
 
175
193
  class WebSocketResponseStartEvent(TypedDict):
@@ -181,7 +199,7 @@ class WebSocketResponseStartEvent(TypedDict):
181
199
  class WebSocketResponseBodyEvent(TypedDict):
182
200
  type: Literal["websocket.http.response.body"]
183
201
  body: bytes
184
- more_body: bool
202
+ more_body: NotRequired[bool]
185
203
 
186
204
 
187
205
  class WebSocketDisconnectEvent(TypedDict):
@@ -191,8 +209,8 @@ class WebSocketDisconnectEvent(TypedDict):
191
209
 
192
210
  class WebSocketCloseEvent(TypedDict):
193
211
  type: Literal["websocket.close"]
194
- code: int
195
- reason: Optional[str]
212
+ code: NotRequired[int]
213
+ reason: NotRequired[Optional[str]]
196
214
 
197
215
 
198
216
  class LifespanStartupEvent(TypedDict):
@@ -187,7 +187,7 @@ def _normalize_dirs(dirs: Union[List[str], str, None]) -> List[str]:
187
187
  class Config:
188
188
  def __init__(
189
189
  self,
190
- app: Union["ASGIApplication", Callable, str],
190
+ app: Union["ASGIApplication", Callable[..., Any], str],
191
191
  host: str = "127.0.0.1",
192
192
  port: int = 8000,
193
193
  uds: Optional[str] = None,
@@ -201,7 +201,7 @@ class Config:
201
201
  ws_ping_timeout: Optional[float] = 20.0,
202
202
  ws_per_message_deflate: bool = True,
203
203
  lifespan: LifespanType = "auto",
204
- env_file: Optional[Union[str, os.PathLike]] = None,
204
+ env_file: "str | os.PathLike[str] | None" = None,
205
205
  log_config: Optional[Union[Dict[str, Any], str]] = LOGGING_CONFIG,
206
206
  log_level: Optional[Union[str, int]] = None,
207
207
  access_log: bool = True,
@@ -226,7 +226,7 @@ class Config:
226
226
  timeout_graceful_shutdown: Optional[int] = None,
227
227
  callback_notify: Optional[Callable[..., Awaitable[None]]] = None,
228
228
  ssl_keyfile: Optional[str] = None,
229
- ssl_certfile: Optional[Union[str, os.PathLike]] = None,
229
+ ssl_certfile: "str | os.PathLike[str] | None" = None,
230
230
  ssl_keyfile_password: Optional[str] = None,
231
231
  ssl_version: int = SSL_PROTOCOL_VERSION,
232
232
  ssl_cert_reqs: int = ssl.CERT_NONE,
@@ -156,14 +156,14 @@ def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> No
156
156
  "--ws-ping-interval",
157
157
  type=float,
158
158
  default=20.0,
159
- help="WebSocket ping interval",
159
+ help="WebSocket ping interval in seconds.",
160
160
  show_default=True,
161
161
  )
162
162
  @click.option(
163
163
  "--ws-ping-timeout",
164
164
  type=float,
165
165
  default=20.0,
166
- help="WebSocket ping timeout",
166
+ help="WebSocket ping timeout in seconds.",
167
167
  show_default=True,
168
168
  )
169
169
  @click.option(
@@ -465,7 +465,7 @@ def main(
465
465
 
466
466
 
467
467
  def run(
468
- app: typing.Union["ASGIApplication", typing.Callable, str],
468
+ app: typing.Union["ASGIApplication", typing.Callable[..., typing.Any], str],
469
469
  *,
470
470
  host: str = "127.0.0.1",
471
471
  port: int = 8000,
@@ -487,7 +487,7 @@ def run(
487
487
  reload_excludes: typing.Optional[typing.Union[typing.List[str], str]] = None,
488
488
  reload_delay: float = 0.25,
489
489
  workers: typing.Optional[int] = None,
490
- env_file: typing.Optional[typing.Union[str, os.PathLike]] = None,
490
+ env_file: "str | os.PathLike[str] | None" = None,
491
491
  log_config: typing.Optional[
492
492
  typing.Union[typing.Dict[str, typing.Any], str]
493
493
  ] = LOGGING_CONFIG,
@@ -504,7 +504,7 @@ def run(
504
504
  timeout_keep_alive: int = 5,
505
505
  timeout_graceful_shutdown: typing.Optional[int] = None,
506
506
  ssl_keyfile: typing.Optional[str] = None,
507
- ssl_certfile: typing.Optional[typing.Union[str, os.PathLike]] = None,
507
+ ssl_certfile: "str | os.PathLike[str] | None" = None,
508
508
  ssl_keyfile_password: typing.Optional[str] = None,
509
509
  ssl_version: int = SSL_PROTOCOL_VERSION,
510
510
  ssl_cert_reqs: int = ssl.CERT_NONE,
@@ -29,6 +29,8 @@ from uvicorn._types import (
29
29
  WebSocketConnectEvent,
30
30
  WebSocketDisconnectEvent,
31
31
  WebSocketReceiveEvent,
32
+ WebSocketResponseBodyEvent,
33
+ WebSocketResponseStartEvent,
32
34
  WebSocketScope,
33
35
  WebSocketSendEvent,
34
36
  )
@@ -196,6 +198,7 @@ class WebSocketProtocol(WebSocketServerProtocol):
196
198
  "headers": asgi_headers,
197
199
  "subprotocols": subprotocols,
198
200
  "state": self.app_state.copy(),
201
+ "extensions": {"websocket.http.response": {}},
199
202
  }
200
203
  task = self.loop.create_task(self.run_asgi())
201
204
  task.add_done_callback(self.on_task_complete)
@@ -302,14 +305,31 @@ class WebSocketProtocol(WebSocketServerProtocol):
302
305
  self.handshake_started_event.set()
303
306
  self.closed_event.set()
304
307
 
308
+ elif message_type == "websocket.http.response.start":
309
+ message = cast("WebSocketResponseStartEvent", message)
310
+ self.logger.info(
311
+ '%s - "WebSocket %s" %d',
312
+ self.scope["client"],
313
+ get_path_with_query_string(self.scope),
314
+ message["status"],
315
+ )
316
+ # websockets requires the status to be an enum. look it up.
317
+ status = http.HTTPStatus(message["status"])
318
+ headers = [
319
+ (name.decode("latin-1"), value.decode("latin-1"))
320
+ for name, value in message.get("headers", [])
321
+ ]
322
+ self.initial_response = (status, headers, b"")
323
+ self.handshake_started_event.set()
324
+
305
325
  else:
306
326
  msg = (
307
- "Expected ASGI message 'websocket.accept' or 'websocket.close', "
308
- "but got '%s'."
327
+ "Expected ASGI message 'websocket.accept', 'websocket.close', "
328
+ "or 'websocket.http.response.start' but got '%s'."
309
329
  )
310
330
  raise RuntimeError(msg % message_type)
311
331
 
312
- elif not self.closed_event.is_set():
332
+ elif not self.closed_event.is_set() and self.initial_response is None:
313
333
  await self.handshake_completed_event.wait()
314
334
 
315
335
  if message_type == "websocket.send":
@@ -333,8 +353,25 @@ class WebSocketProtocol(WebSocketServerProtocol):
333
353
  )
334
354
  raise RuntimeError(msg % message_type)
335
355
 
356
+ elif self.initial_response is not None:
357
+ if message_type == "websocket.http.response.body":
358
+ message = cast("WebSocketResponseBodyEvent", message)
359
+ body = self.initial_response[2] + message["body"]
360
+ self.initial_response = self.initial_response[:2] + (body,)
361
+ if not message.get("more_body", False):
362
+ self.closed_event.set()
363
+ else:
364
+ msg = (
365
+ "Expected ASGI message 'websocket.http.response.body' "
366
+ "but got '%s'."
367
+ )
368
+ raise RuntimeError(msg % message_type)
369
+
336
370
  else:
337
- msg = "Unexpected ASGI message '%s', after sending 'websocket.close'."
371
+ msg = (
372
+ "Unexpected ASGI message '%s', after sending 'websocket.close' "
373
+ "or response already completed."
374
+ )
338
375
  raise RuntimeError(msg % message_type)
339
376
 
340
377
  async def asgi_receive(
@@ -364,13 +401,6 @@ class WebSocketProtocol(WebSocketServerProtocol):
364
401
  return {"type": "websocket.disconnect", "code": 1012}
365
402
  return {"type": "websocket.disconnect", "code": exc.code}
366
403
 
367
- msg: WebSocketReceiveEvent = { # type: ignore[typeddict-item]
368
- "type": "websocket.receive"
369
- }
370
-
371
404
  if isinstance(data, str):
372
- msg["text"] = data
373
- else:
374
- msg["bytes"] = data
375
-
376
- return msg
405
+ return {"type": "websocket.receive", "text": data}
406
+ return {"type": "websocket.receive", "bytes": data}
@@ -15,7 +15,8 @@ from uvicorn._types import (
15
15
  WebSocketAcceptEvent,
16
16
  WebSocketCloseEvent,
17
17
  WebSocketEvent,
18
- WebSocketReceiveEvent,
18
+ WebSocketResponseBodyEvent,
19
+ WebSocketResponseStartEvent,
19
20
  WebSocketScope,
20
21
  WebSocketSendEvent,
21
22
  )
@@ -64,6 +65,9 @@ class WSProtocol(asyncio.Protocol):
64
65
  self.handshake_complete = False
65
66
  self.close_sent = False
66
67
 
68
+ # Rejection state
69
+ self.response_started = False
70
+
67
71
  self.conn = wsproto.WSConnection(connection_type=ConnectionType.SERVER)
68
72
 
69
73
  self.read_paused = False
@@ -172,6 +176,7 @@ class WSProtocol(asyncio.Protocol):
172
176
  "headers": headers,
173
177
  "subprotocols": event.subprotocols,
174
178
  "state": self.app_state.copy(),
179
+ "extensions": {"websocket.http.response": {}},
175
180
  }
176
181
  self.queue.put_nowait({"type": "websocket.connect"})
177
182
  task = self.loop.create_task(self.run_asgi())
@@ -181,11 +186,7 @@ class WSProtocol(asyncio.Protocol):
181
186
  def handle_text(self, event: events.TextMessage) -> None:
182
187
  self.text += event.data
183
188
  if event.message_finished:
184
- msg: "WebSocketReceiveEvent" = { # type: ignore[typeddict-item]
185
- "type": "websocket.receive",
186
- "text": self.text,
187
- }
188
- self.queue.put_nowait(msg)
189
+ self.queue.put_nowait({"type": "websocket.receive", "text": self.text})
189
190
  self.text = ""
190
191
  if not self.read_paused:
191
192
  self.read_paused = True
@@ -195,11 +196,7 @@ class WSProtocol(asyncio.Protocol):
195
196
  self.bytes += event.data
196
197
  # todo: we may want to guard the size of self.bytes and self.text
197
198
  if event.message_finished:
198
- msg: "WebSocketReceiveEvent" = { # type: ignore[typeddict-item]
199
- "type": "websocket.receive",
200
- "bytes": self.bytes,
201
- }
202
- self.queue.put_nowait(msg)
199
+ self.queue.put_nowait({"type": "websocket.receive", "bytes": self.bytes})
203
200
  self.bytes = b""
204
201
  if not self.read_paused:
205
202
  self.read_paused = True
@@ -215,6 +212,8 @@ class WSProtocol(asyncio.Protocol):
215
212
  self.transport.write(self.conn.send(event.response()))
216
213
 
217
214
  def send_500_response(self) -> None:
215
+ if self.response_started or self.handshake_complete:
216
+ return # we cannot send responses anymore
218
217
  headers = [
219
218
  (b"content-type", b"text/plain; charset=utf-8"),
220
219
  (b"connection", b"close"),
@@ -234,8 +233,7 @@ class WSProtocol(asyncio.Protocol):
234
233
  result = await self.app(self.scope, self.receive, self.send)
235
234
  except BaseException:
236
235
  self.logger.exception("Exception in ASGI application\n")
237
- if not self.handshake_complete:
238
- self.send_500_response()
236
+ self.send_500_response()
239
237
  self.transport.close()
240
238
  else:
241
239
  if not self.handshake_complete:
@@ -291,14 +289,37 @@ class WSProtocol(asyncio.Protocol):
291
289
  self.transport.write(output)
292
290
  self.transport.close()
293
291
 
292
+ elif message_type == "websocket.http.response.start":
293
+ message = typing.cast("WebSocketResponseStartEvent", message)
294
+ # ensure status code is in the valid range
295
+ if not (100 <= message["status"] < 600):
296
+ msg = "Invalid HTTP status code '%d' in response."
297
+ raise RuntimeError(msg % message["status"])
298
+ self.logger.info(
299
+ '%s - "WebSocket %s" %d',
300
+ self.scope["client"],
301
+ get_path_with_query_string(self.scope),
302
+ message["status"],
303
+ )
304
+ self.handshake_complete = True
305
+ event = events.RejectConnection(
306
+ status_code=message["status"],
307
+ headers=list(message["headers"]),
308
+ has_body=True,
309
+ )
310
+ output = self.conn.send(event)
311
+ self.transport.write(output)
312
+ self.response_started = True
313
+
294
314
  else:
295
315
  msg = (
296
- "Expected ASGI message 'websocket.accept' or 'websocket.close', "
316
+ "Expected ASGI message 'websocket.accept', 'websocket.close' "
317
+ "or 'websocket.http.response.start' "
297
318
  "but got '%s'."
298
319
  )
299
320
  raise RuntimeError(msg % message_type)
300
321
 
301
- elif not self.close_sent:
322
+ elif not self.close_sent and not self.response_started:
302
323
  if message_type == "websocket.send":
303
324
  message = typing.cast("WebSocketSendEvent", message)
304
325
  bytes_data = message.get("bytes")
@@ -329,6 +350,29 @@ class WSProtocol(asyncio.Protocol):
329
350
  " but got '%s'."
330
351
  )
331
352
  raise RuntimeError(msg % message_type)
353
+ elif self.response_started:
354
+ if message_type == "websocket.http.response.body":
355
+ message = typing.cast("WebSocketResponseBodyEvent", message)
356
+ body_finished = not message.get("more_body", False)
357
+ reject_data = events.RejectData(
358
+ data=message["body"], body_finished=body_finished
359
+ )
360
+ output = self.conn.send(reject_data)
361
+ self.transport.write(output)
362
+
363
+ if body_finished:
364
+ self.queue.put_nowait(
365
+ {"type": "websocket.disconnect", "code": 1006}
366
+ )
367
+ self.close_sent = True
368
+ self.transport.close()
369
+
370
+ else:
371
+ msg = (
372
+ "Expected ASGI message 'websocket.http.response.body' "
373
+ "but got '%s'."
374
+ )
375
+ raise RuntimeError(msg % message_type)
332
376
 
333
377
  else:
334
378
  msg = "Unexpected ASGI message '%s', after sending 'websocket.close'."
@@ -43,6 +43,9 @@ class FileFilter:
43
43
  def __call__(self, path: Path) -> bool:
44
44
  for include_pattern in self.includes:
45
45
  if path.match(include_pattern):
46
+ if str(path).endswith(include_pattern):
47
+ return True
48
+
46
49
  for exclude_dir in self.exclude_dirs:
47
50
  if exclude_dir in path.parents:
48
51
  return False
@@ -56,6 +56,9 @@ class CustomWatcher(DefaultWatcher):
56
56
  self.watched_files[entry.path] = False
57
57
  return False
58
58
  for include_pattern in self.includes:
59
+ if str(entry_path).endswith(include_pattern):
60
+ self.watched_files[entry.path] = True
61
+ return True
59
62
  if entry_path.match(include_pattern):
60
63
  for exclude_pattern in self.excludes:
61
64
  if entry_path.match(exclude_pattern):
File without changes
File without changes
File without changes