uvicorn 0.27.1__py3-none-any.whl → 0.28.1__py3-none-any.whl
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.
- uvicorn/__init__.py +1 -1
- uvicorn/_subprocess.py +3 -3
- uvicorn/_types.py +5 -11
- uvicorn/config.py +17 -57
- uvicorn/importer.py +2 -6
- uvicorn/lifespan/off.py +4 -2
- uvicorn/lifespan/on.py +8 -8
- uvicorn/logging.py +3 -5
- uvicorn/main.py +12 -24
- uvicorn/middleware/asgi2.py +1 -3
- uvicorn/middleware/proxy_headers.py +12 -27
- uvicorn/middleware/wsgi.py +17 -24
- uvicorn/protocols/http/flow_control.py +1 -3
- uvicorn/protocols/http/h11_impl.py +12 -19
- uvicorn/protocols/http/httptools_impl.py +17 -36
- uvicorn/protocols/utils.py +1 -3
- uvicorn/protocols/websockets/auto.py +3 -1
- uvicorn/protocols/websockets/websockets_impl.py +16 -45
- uvicorn/protocols/websockets/wsproto_impl.py +10 -30
- uvicorn/server.py +5 -17
- uvicorn/supervisors/__init__.py +4 -2
- uvicorn/supervisors/basereload.py +4 -10
- uvicorn/supervisors/multiprocess.py +5 -11
- uvicorn/supervisors/statreload.py +1 -4
- uvicorn/supervisors/watchfilesreload.py +2 -10
- uvicorn/supervisors/watchgodreload.py +7 -18
- uvicorn/workers.py +6 -4
- {uvicorn-0.27.1.dist-info → uvicorn-0.28.1.dist-info}/METADATA +2 -2
- uvicorn-0.28.1.dist-info/RECORD +45 -0
- {uvicorn-0.27.1.dist-info → uvicorn-0.28.1.dist-info}/WHEEL +1 -1
- uvicorn-0.27.1.dist-info/RECORD +0 -45
- {uvicorn-0.27.1.dist-info → uvicorn-0.28.1.dist-info}/entry_points.txt +0 -0
- {uvicorn-0.27.1.dist-info → uvicorn-0.28.1.dist-info}/licenses/LICENSE.md +0 -0
uvicorn/__init__.py
CHANGED
uvicorn/_subprocess.py
CHANGED
@@ -9,7 +9,7 @@ import os
|
|
9
9
|
import sys
|
10
10
|
from multiprocessing.context import SpawnProcess
|
11
11
|
from socket import socket
|
12
|
-
from typing import Callable
|
12
|
+
from typing import Callable
|
13
13
|
|
14
14
|
from uvicorn.config import Config
|
15
15
|
|
@@ -34,10 +34,10 @@ def get_subprocess(
|
|
34
34
|
"""
|
35
35
|
# We pass across the stdin fileno, and reopen it in the child process.
|
36
36
|
# This is required for some debugging environments.
|
37
|
-
stdin_fileno: Optional[int]
|
38
37
|
try:
|
39
38
|
stdin_fileno = sys.stdin.fileno()
|
40
|
-
|
39
|
+
# The `sys.stdin` can be `None`, see https://docs.python.org/3/library/sys.html#sys.__stdin__.
|
40
|
+
except (AttributeError, OSError):
|
41
41
|
stdin_fileno = None
|
42
42
|
|
43
43
|
kwargs = {
|
uvicorn/_types.py
CHANGED
@@ -36,18 +36,16 @@ from typing import (
|
|
36
36
|
Awaitable,
|
37
37
|
Callable,
|
38
38
|
Iterable,
|
39
|
+
Literal,
|
39
40
|
MutableMapping,
|
40
41
|
Optional,
|
42
|
+
Protocol,
|
41
43
|
Tuple,
|
42
44
|
Type,
|
45
|
+
TypedDict,
|
43
46
|
Union,
|
44
47
|
)
|
45
48
|
|
46
|
-
if sys.version_info >= (3, 8): # pragma: py-lt-38
|
47
|
-
from typing import Literal, Protocol, TypedDict
|
48
|
-
else: # pragma: py-gte-38
|
49
|
-
from typing_extensions import Literal, Protocol, TypedDict
|
50
|
-
|
51
49
|
if sys.version_info >= (3, 11): # pragma: py-lt-311
|
52
50
|
from typing import NotRequired
|
53
51
|
else: # pragma: py-gte-311
|
@@ -239,9 +237,7 @@ class LifespanShutdownFailedEvent(TypedDict):
|
|
239
237
|
message: str
|
240
238
|
|
241
239
|
|
242
|
-
WebSocketEvent = Union[
|
243
|
-
WebSocketReceiveEvent, WebSocketDisconnectEvent, WebSocketConnectEvent
|
244
|
-
]
|
240
|
+
WebSocketEvent = Union[WebSocketReceiveEvent, WebSocketDisconnectEvent, WebSocketConnectEvent]
|
245
241
|
|
246
242
|
|
247
243
|
ASGIReceiveEvent = Union[
|
@@ -281,9 +277,7 @@ class ASGI2Protocol(Protocol):
|
|
281
277
|
def __init__(self, scope: Scope) -> None:
|
282
278
|
... # pragma: no cover
|
283
279
|
|
284
|
-
async def __call__(
|
285
|
-
self, receive: ASGIReceiveCallable, send: ASGISendCallable
|
286
|
-
) -> None:
|
280
|
+
async def __call__(self, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None:
|
287
281
|
... # pragma: no cover
|
288
282
|
|
289
283
|
|
uvicorn/config.py
CHANGED
@@ -127,9 +127,7 @@ def is_dir(path: Path) -> bool:
|
|
127
127
|
return False
|
128
128
|
|
129
129
|
|
130
|
-
def resolve_reload_patterns(
|
131
|
-
patterns_list: list[str], directories_list: list[str]
|
132
|
-
) -> tuple[list[str], list[Path]]:
|
130
|
+
def resolve_reload_patterns(patterns_list: list[str], directories_list: list[str]) -> tuple[list[str], list[Path]]:
|
133
131
|
directories: list[Path] = list(set(map(Path, directories_list.copy())))
|
134
132
|
patterns: list[str] = patterns_list.copy()
|
135
133
|
|
@@ -150,9 +148,7 @@ def resolve_reload_patterns(
|
|
150
148
|
directories = list(set(directories))
|
151
149
|
directories = list(map(Path, directories))
|
152
150
|
directories = list(map(lambda x: x.resolve(), directories))
|
153
|
-
directories = list(
|
154
|
-
{reload_path for reload_path in directories if is_dir(reload_path)}
|
155
|
-
)
|
151
|
+
directories = list({reload_path for reload_path in directories if is_dir(reload_path)})
|
156
152
|
|
157
153
|
children = []
|
158
154
|
for j in range(len(directories)):
|
@@ -280,12 +276,9 @@ class Config:
|
|
280
276
|
self.reload_includes: list[str] = []
|
281
277
|
self.reload_excludes: list[str] = []
|
282
278
|
|
283
|
-
if (
|
284
|
-
reload_dirs or reload_includes or reload_excludes
|
285
|
-
) and not self.should_reload:
|
279
|
+
if (reload_dirs or reload_includes or reload_excludes) and not self.should_reload:
|
286
280
|
logger.warning(
|
287
|
-
"Current configuration will not reload as not all conditions are met, "
|
288
|
-
"please refer to documentation."
|
281
|
+
"Current configuration will not reload as not all conditions are met, " "please refer to documentation."
|
289
282
|
)
|
290
283
|
|
291
284
|
if self.should_reload:
|
@@ -293,22 +286,15 @@ class Config:
|
|
293
286
|
reload_includes = _normalize_dirs(reload_includes)
|
294
287
|
reload_excludes = _normalize_dirs(reload_excludes)
|
295
288
|
|
296
|
-
self.reload_includes, self.reload_dirs = resolve_reload_patterns(
|
297
|
-
reload_includes, reload_dirs
|
298
|
-
)
|
289
|
+
self.reload_includes, self.reload_dirs = resolve_reload_patterns(reload_includes, reload_dirs)
|
299
290
|
|
300
|
-
self.reload_excludes, self.reload_dirs_excludes = resolve_reload_patterns(
|
301
|
-
reload_excludes, []
|
302
|
-
)
|
291
|
+
self.reload_excludes, self.reload_dirs_excludes = resolve_reload_patterns(reload_excludes, [])
|
303
292
|
|
304
293
|
reload_dirs_tmp = self.reload_dirs.copy()
|
305
294
|
|
306
295
|
for directory in self.reload_dirs_excludes:
|
307
296
|
for reload_directory in reload_dirs_tmp:
|
308
|
-
if
|
309
|
-
directory == reload_directory
|
310
|
-
or directory in reload_directory.parents
|
311
|
-
):
|
297
|
+
if directory == reload_directory or directory in reload_directory.parents:
|
312
298
|
try:
|
313
299
|
self.reload_dirs.remove(reload_directory)
|
314
300
|
except ValueError:
|
@@ -343,9 +329,7 @@ class Config:
|
|
343
329
|
|
344
330
|
self.forwarded_allow_ips: list[str] | str
|
345
331
|
if forwarded_allow_ips is None:
|
346
|
-
self.forwarded_allow_ips = os.environ.get(
|
347
|
-
"FORWARDED_ALLOW_IPS", "127.0.0.1"
|
348
|
-
)
|
332
|
+
self.forwarded_allow_ips = os.environ.get("FORWARDED_ALLOW_IPS", "127.0.0.1")
|
349
333
|
else:
|
350
334
|
self.forwarded_allow_ips = forwarded_allow_ips
|
351
335
|
|
@@ -375,12 +359,8 @@ class Config:
|
|
375
359
|
if self.log_config is not None:
|
376
360
|
if isinstance(self.log_config, dict):
|
377
361
|
if self.use_colors in (True, False):
|
378
|
-
self.log_config["formatters"]["default"][
|
379
|
-
|
380
|
-
] = self.use_colors
|
381
|
-
self.log_config["formatters"]["access"][
|
382
|
-
"use_colors"
|
383
|
-
] = self.use_colors
|
362
|
+
self.log_config["formatters"]["default"]["use_colors"] = self.use_colors
|
363
|
+
self.log_config["formatters"]["access"]["use_colors"] = self.use_colors
|
384
364
|
logging.config.dictConfig(self.log_config)
|
385
365
|
elif self.log_config.endswith(".json"):
|
386
366
|
with open(self.log_config) as file:
|
@@ -397,9 +377,7 @@ class Config:
|
|
397
377
|
else:
|
398
378
|
# See the note about fileConfig() here:
|
399
379
|
# https://docs.python.org/3/library/logging.config.html#configuration-file-format
|
400
|
-
logging.config.fileConfig(
|
401
|
-
self.log_config, disable_existing_loggers=False
|
402
|
-
)
|
380
|
+
logging.config.fileConfig(self.log_config, disable_existing_loggers=False)
|
403
381
|
|
404
382
|
if self.log_level is not None:
|
405
383
|
if isinstance(self.log_level, str):
|
@@ -430,10 +408,7 @@ class Config:
|
|
430
408
|
else:
|
431
409
|
self.ssl = None
|
432
410
|
|
433
|
-
encoded_headers = [
|
434
|
-
(key.lower().encode("latin1"), value.encode("latin1"))
|
435
|
-
for key, value in self.headers
|
436
|
-
]
|
411
|
+
encoded_headers = [(key.lower().encode("latin1"), value.encode("latin1")) for key, value in self.headers]
|
437
412
|
self.encoded_headers = (
|
438
413
|
[(b"server", b"uvicorn")] + encoded_headers
|
439
414
|
if b"server" not in dict(encoded_headers) and self.server_header
|
@@ -469,8 +444,7 @@ class Config:
|
|
469
444
|
else:
|
470
445
|
if not self.factory:
|
471
446
|
logger.warning(
|
472
|
-
"ASGI app factory detected. Using it, "
|
473
|
-
"but please consider setting the --factory flag explicitly."
|
447
|
+
"ASGI app factory detected. Using it, " "but please consider setting the --factory flag explicitly."
|
474
448
|
)
|
475
449
|
|
476
450
|
if self.interface == "auto":
|
@@ -492,9 +466,7 @@ class Config:
|
|
492
466
|
if logger.getEffectiveLevel() <= TRACE_LOG_LEVEL:
|
493
467
|
self.loaded_app = MessageLoggerMiddleware(self.loaded_app)
|
494
468
|
if self.proxy_headers:
|
495
|
-
self.loaded_app = ProxyHeadersMiddleware(
|
496
|
-
self.loaded_app, trusted_hosts=self.forwarded_allow_ips
|
497
|
-
)
|
469
|
+
self.loaded_app = ProxyHeadersMiddleware(self.loaded_app, trusted_hosts=self.forwarded_allow_ips)
|
498
470
|
|
499
471
|
self.loaded = True
|
500
472
|
|
@@ -518,21 +490,13 @@ class Config:
|
|
518
490
|
|
519
491
|
message = "Uvicorn running on unix socket %s (Press CTRL+C to quit)"
|
520
492
|
sock_name_format = "%s"
|
521
|
-
color_message = (
|
522
|
-
"Uvicorn running on "
|
523
|
-
+ click.style(sock_name_format, bold=True)
|
524
|
-
+ " (Press CTRL+C to quit)"
|
525
|
-
)
|
493
|
+
color_message = "Uvicorn running on " + click.style(sock_name_format, bold=True) + " (Press CTRL+C to quit)"
|
526
494
|
logger_args = [self.uds]
|
527
495
|
elif self.fd: # pragma: py-win32
|
528
496
|
sock = socket.fromfd(self.fd, socket.AF_UNIX, socket.SOCK_STREAM)
|
529
497
|
message = "Uvicorn running on socket %s (Press CTRL+C to quit)"
|
530
498
|
fd_name_format = "%s"
|
531
|
-
color_message = (
|
532
|
-
"Uvicorn running on "
|
533
|
-
+ click.style(fd_name_format, bold=True)
|
534
|
-
+ " (Press CTRL+C to quit)"
|
535
|
-
)
|
499
|
+
color_message = "Uvicorn running on " + click.style(fd_name_format, bold=True) + " (Press CTRL+C to quit)"
|
536
500
|
logger_args = [sock.getsockname()]
|
537
501
|
else:
|
538
502
|
family = socket.AF_INET
|
@@ -552,11 +516,7 @@ class Config:
|
|
552
516
|
sys.exit(1)
|
553
517
|
|
554
518
|
message = f"Uvicorn running on {addr_format} (Press CTRL+C to quit)"
|
555
|
-
color_message = (
|
556
|
-
"Uvicorn running on "
|
557
|
-
+ click.style(addr_format, bold=True)
|
558
|
-
+ " (Press CTRL+C to quit)"
|
559
|
-
)
|
519
|
+
color_message = "Uvicorn running on " + click.style(addr_format, bold=True) + " (Press CTRL+C to quit)"
|
560
520
|
protocol_name = "https" if self.is_ssl else "http"
|
561
521
|
logger_args = [protocol_name, self.host, sock.getsockname()[1]]
|
562
522
|
logger.info(message, *logger_args, extra={"color_message": color_message})
|
uvicorn/importer.py
CHANGED
@@ -12,9 +12,7 @@ def import_from_string(import_str: Any) -> Any:
|
|
12
12
|
|
13
13
|
module_str, _, attrs_str = import_str.partition(":")
|
14
14
|
if not module_str or not attrs_str:
|
15
|
-
message =
|
16
|
-
'Import string "{import_str}" must be in format "<module>:<attribute>".'
|
17
|
-
)
|
15
|
+
message = 'Import string "{import_str}" must be in format "<module>:<attribute>".'
|
18
16
|
raise ImportFromStringError(message.format(import_str=import_str))
|
19
17
|
|
20
18
|
try:
|
@@ -31,8 +29,6 @@ def import_from_string(import_str: Any) -> Any:
|
|
31
29
|
instance = getattr(instance, attr_str)
|
32
30
|
except AttributeError:
|
33
31
|
message = 'Attribute "{attrs_str}" not found in module "{module_str}".'
|
34
|
-
raise ImportFromStringError(
|
35
|
-
message.format(attrs_str=attrs_str, module_str=module_str)
|
36
|
-
)
|
32
|
+
raise ImportFromStringError(message.format(attrs_str=attrs_str, module_str=module_str))
|
37
33
|
|
38
34
|
return instance
|
uvicorn/lifespan/off.py
CHANGED
@@ -1,4 +1,6 @@
|
|
1
|
-
from
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from typing import Any
|
2
4
|
|
3
5
|
from uvicorn import Config
|
4
6
|
|
@@ -6,7 +8,7 @@ from uvicorn import Config
|
|
6
8
|
class LifespanOff:
|
7
9
|
def __init__(self, config: Config) -> None:
|
8
10
|
self.should_exit = False
|
9
|
-
self.state:
|
11
|
+
self.state: dict[str, Any] = {}
|
10
12
|
|
11
13
|
async def startup(self) -> None:
|
12
14
|
pass
|
uvicorn/lifespan/on.py
CHANGED
@@ -1,7 +1,9 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import asyncio
|
2
4
|
import logging
|
3
5
|
from asyncio import Queue
|
4
|
-
from typing import Any,
|
6
|
+
from typing import Any, Union
|
5
7
|
|
6
8
|
from uvicorn import Config
|
7
9
|
from uvicorn._types import (
|
@@ -35,12 +37,12 @@ class LifespanOn:
|
|
35
37
|
self.logger = logging.getLogger("uvicorn.error")
|
36
38
|
self.startup_event = asyncio.Event()
|
37
39
|
self.shutdown_event = asyncio.Event()
|
38
|
-
self.receive_queue:
|
40
|
+
self.receive_queue: Queue[LifespanReceiveMessage] = asyncio.Queue()
|
39
41
|
self.error_occured = False
|
40
42
|
self.startup_failed = False
|
41
43
|
self.shutdown_failed = False
|
42
44
|
self.should_exit = False
|
43
|
-
self.state:
|
45
|
+
self.state: dict[str, Any] = {}
|
44
46
|
|
45
47
|
async def startup(self) -> None:
|
46
48
|
self.logger.info("Waiting for application startup.")
|
@@ -67,9 +69,7 @@ class LifespanOn:
|
|
67
69
|
await self.receive_queue.put(shutdown_event)
|
68
70
|
await self.shutdown_event.wait()
|
69
71
|
|
70
|
-
if self.shutdown_failed or (
|
71
|
-
self.error_occured and self.config.lifespan == "on"
|
72
|
-
):
|
72
|
+
if self.shutdown_failed or (self.error_occured and self.config.lifespan == "on"):
|
73
73
|
self.logger.error("Application shutdown failed. Exiting.")
|
74
74
|
self.should_exit = True
|
75
75
|
else:
|
@@ -99,7 +99,7 @@ class LifespanOn:
|
|
99
99
|
self.startup_event.set()
|
100
100
|
self.shutdown_event.set()
|
101
101
|
|
102
|
-
async def send(self, message:
|
102
|
+
async def send(self, message: LifespanSendMessage) -> None:
|
103
103
|
assert message["type"] in (
|
104
104
|
"lifespan.startup.complete",
|
105
105
|
"lifespan.startup.failed",
|
@@ -133,5 +133,5 @@ class LifespanOn:
|
|
133
133
|
if message.get("message"):
|
134
134
|
self.logger.error(message["message"])
|
135
135
|
|
136
|
-
async def receive(self) ->
|
136
|
+
async def receive(self) -> LifespanReceiveMessage:
|
137
137
|
return await self.receive_queue.get()
|
uvicorn/logging.py
CHANGED
@@ -26,9 +26,7 @@ class ColourizedFormatter(logging.Formatter):
|
|
26
26
|
logging.INFO: lambda level_name: click.style(str(level_name), fg="green"),
|
27
27
|
logging.WARNING: lambda level_name: click.style(str(level_name), fg="yellow"),
|
28
28
|
logging.ERROR: lambda level_name: click.style(str(level_name), fg="red"),
|
29
|
-
logging.CRITICAL: lambda level_name: click.style(
|
30
|
-
str(level_name), fg="bright_red"
|
31
|
-
),
|
29
|
+
logging.CRITICAL: lambda level_name: click.style(str(level_name), fg="bright_red"),
|
32
30
|
}
|
33
31
|
|
34
32
|
def __init__(
|
@@ -86,7 +84,7 @@ class AccessFormatter(ColourizedFormatter):
|
|
86
84
|
status_phrase = http.HTTPStatus(status_code).phrase
|
87
85
|
except ValueError:
|
88
86
|
status_phrase = ""
|
89
|
-
status_and_phrase = "
|
87
|
+
status_and_phrase = f"{status_code} {status_phrase}"
|
90
88
|
if self.use_colors:
|
91
89
|
|
92
90
|
def default(code: int) -> str:
|
@@ -106,7 +104,7 @@ class AccessFormatter(ColourizedFormatter):
|
|
106
104
|
status_code,
|
107
105
|
) = recordcopy.args # type: ignore[misc]
|
108
106
|
status_code = self.get_status_code(int(status_code)) # type: ignore[arg-type]
|
109
|
-
request_line = "
|
107
|
+
request_line = f"{method} {full_path} HTTP/{http_version}"
|
110
108
|
if self.use_colors:
|
111
109
|
request_line = click.style(request_line, bold=True)
|
112
110
|
recordcopy.__dict__.update(
|
uvicorn/main.py
CHANGED
@@ -47,12 +47,11 @@ def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> No
|
|
47
47
|
if not value or ctx.resilient_parsing:
|
48
48
|
return
|
49
49
|
click.echo(
|
50
|
-
"Running uvicorn
|
51
|
-
|
52
|
-
|
53
|
-
platform.
|
54
|
-
platform.
|
55
|
-
platform.system(),
|
50
|
+
"Running uvicorn {version} with {py_implementation} {py_version} on {system}".format(
|
51
|
+
version=uvicorn.__version__,
|
52
|
+
py_implementation=platform.python_implementation(),
|
53
|
+
py_version=platform.python_version(),
|
54
|
+
system=platform.system(),
|
56
55
|
)
|
57
56
|
)
|
58
57
|
ctx.exit()
|
@@ -75,16 +74,13 @@ def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> No
|
|
75
74
|
show_default=True,
|
76
75
|
)
|
77
76
|
@click.option("--uds", type=str, default=None, help="Bind to a UNIX domain socket.")
|
78
|
-
@click.option(
|
79
|
-
"--fd", type=int, default=None, help="Bind to socket from this file descriptor."
|
80
|
-
)
|
77
|
+
@click.option("--fd", type=int, default=None, help="Bind to socket from this file descriptor.")
|
81
78
|
@click.option("--reload", is_flag=True, default=False, help="Enable auto-reload.")
|
82
79
|
@click.option(
|
83
80
|
"--reload-dir",
|
84
81
|
"reload_dirs",
|
85
82
|
multiple=True,
|
86
|
-
help="Set reload directories explicitly, instead of using the current working"
|
87
|
-
" directory.",
|
83
|
+
help="Set reload directories explicitly, instead of using the current working" " directory.",
|
88
84
|
type=click.Path(exists=True),
|
89
85
|
)
|
90
86
|
@click.option(
|
@@ -109,8 +105,7 @@ def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> No
|
|
109
105
|
type=float,
|
110
106
|
default=0.25,
|
111
107
|
show_default=True,
|
112
|
-
help="Delay between previous and next check if application needs to be."
|
113
|
-
" Defaults to 0.25s.",
|
108
|
+
help="Delay between previous and next check if application needs to be." " Defaults to 0.25s.",
|
114
109
|
)
|
115
110
|
@click.option(
|
116
111
|
"--workers",
|
@@ -226,8 +221,7 @@ def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> No
|
|
226
221
|
"--proxy-headers/--no-proxy-headers",
|
227
222
|
is_flag=True,
|
228
223
|
default=True,
|
229
|
-
help="Enable/Disable X-Forwarded-Proto, X-Forwarded-For, X-Forwarded-Port to "
|
230
|
-
"populate remote address info.",
|
224
|
+
help="Enable/Disable X-Forwarded-Proto, X-Forwarded-For, X-Forwarded-Port to " "populate remote address info.",
|
231
225
|
)
|
232
226
|
@click.option(
|
233
227
|
"--server-header/--no-server-header",
|
@@ -258,8 +252,7 @@ def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> No
|
|
258
252
|
"--limit-concurrency",
|
259
253
|
type=int,
|
260
254
|
default=None,
|
261
|
-
help="Maximum number of concurrent connections or tasks to allow, before issuing"
|
262
|
-
" HTTP 503 responses.",
|
255
|
+
help="Maximum number of concurrent connections or tasks to allow, before issuing" " HTTP 503 responses.",
|
263
256
|
)
|
264
257
|
@click.option(
|
265
258
|
"--backlog",
|
@@ -286,9 +279,7 @@ def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> No
|
|
286
279
|
default=None,
|
287
280
|
help="Maximum number of seconds to wait for graceful shutdown.",
|
288
281
|
)
|
289
|
-
@click.option(
|
290
|
-
"--ssl-keyfile", type=str, default=None, help="SSL key file", show_default=True
|
291
|
-
)
|
282
|
+
@click.option("--ssl-keyfile", type=str, default=None, help="SSL key file", show_default=True)
|
292
283
|
@click.option(
|
293
284
|
"--ssl-certfile",
|
294
285
|
type=str,
|
@@ -571,10 +562,7 @@ def run(
|
|
571
562
|
|
572
563
|
if (config.reload or config.workers > 1) and not isinstance(app, str):
|
573
564
|
logger = logging.getLogger("uvicorn.error")
|
574
|
-
logger.warning(
|
575
|
-
"You must pass the application as an import string to enable 'reload' or "
|
576
|
-
"'workers'."
|
577
|
-
)
|
565
|
+
logger.warning("You must pass the application as an import string to enable 'reload' or " "'workers'.")
|
578
566
|
sys.exit(1)
|
579
567
|
|
580
568
|
if config.should_reload:
|
uvicorn/middleware/asgi2.py
CHANGED
@@ -10,8 +10,6 @@ class ASGI2Middleware:
|
|
10
10
|
def __init__(self, app: "ASGI2Application"):
|
11
11
|
self.app = app
|
12
12
|
|
13
|
-
async def __call__(
|
14
|
-
self, scope: "Scope", receive: "ASGIReceiveCallable", send: "ASGISendCallable"
|
15
|
-
) -> None:
|
13
|
+
async def __call__(self, scope: "Scope", receive: "ASGIReceiveCallable", send: "ASGISendCallable") -> None:
|
16
14
|
instance = self.app(scope)
|
17
15
|
await instance(receive, send)
|
@@ -8,23 +8,18 @@ the connecting client, rather that the connecting proxy.
|
|
8
8
|
|
9
9
|
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers#Proxies
|
10
10
|
"""
|
11
|
-
from
|
11
|
+
from __future__ import annotations
|
12
12
|
|
13
|
-
from
|
14
|
-
|
15
|
-
|
16
|
-
ASGISendCallable,
|
17
|
-
HTTPScope,
|
18
|
-
Scope,
|
19
|
-
WebSocketScope,
|
20
|
-
)
|
13
|
+
from typing import Union, cast
|
14
|
+
|
15
|
+
from uvicorn._types import ASGI3Application, ASGIReceiveCallable, ASGISendCallable, HTTPScope, Scope, WebSocketScope
|
21
16
|
|
22
17
|
|
23
18
|
class ProxyHeadersMiddleware:
|
24
19
|
def __init__(
|
25
20
|
self,
|
26
|
-
app:
|
27
|
-
trusted_hosts:
|
21
|
+
app: ASGI3Application,
|
22
|
+
trusted_hosts: list[str] | str = "127.0.0.1",
|
28
23
|
) -> None:
|
29
24
|
self.app = app
|
30
25
|
if isinstance(trusted_hosts, str):
|
@@ -33,9 +28,7 @@ class ProxyHeadersMiddleware:
|
|
33
28
|
self.trusted_hosts = set(trusted_hosts)
|
34
29
|
self.always_trust = "*" in self.trusted_hosts
|
35
30
|
|
36
|
-
def get_trusted_client_host(
|
37
|
-
self, x_forwarded_for_hosts: List[str]
|
38
|
-
) -> Optional[str]:
|
31
|
+
def get_trusted_client_host(self, x_forwarded_for_hosts: list[str]) -> str | None:
|
39
32
|
if self.always_trust:
|
40
33
|
return x_forwarded_for_hosts[0]
|
41
34
|
|
@@ -45,12 +38,10 @@ class ProxyHeadersMiddleware:
|
|
45
38
|
|
46
39
|
return None
|
47
40
|
|
48
|
-
async def __call__(
|
49
|
-
self, scope: "Scope", receive: "ASGIReceiveCallable", send: "ASGISendCallable"
|
50
|
-
) -> None:
|
41
|
+
async def __call__(self, scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None:
|
51
42
|
if scope["type"] in ("http", "websocket"):
|
52
43
|
scope = cast(Union["HTTPScope", "WebSocketScope"], scope)
|
53
|
-
client_addr:
|
44
|
+
client_addr: tuple[str, int] | None = scope.get("client")
|
54
45
|
client_host = client_addr[0] if client_addr else None
|
55
46
|
|
56
47
|
if self.always_trust or client_host in self.trusted_hosts:
|
@@ -59,13 +50,9 @@ class ProxyHeadersMiddleware:
|
|
59
50
|
if b"x-forwarded-proto" in headers:
|
60
51
|
# Determine if the incoming request was http or https based on
|
61
52
|
# the X-Forwarded-Proto header.
|
62
|
-
x_forwarded_proto = (
|
63
|
-
headers[b"x-forwarded-proto"].decode("latin1").strip()
|
64
|
-
)
|
53
|
+
x_forwarded_proto = headers[b"x-forwarded-proto"].decode("latin1").strip()
|
65
54
|
if scope["type"] == "websocket":
|
66
|
-
scope["scheme"] = (
|
67
|
-
"wss" if x_forwarded_proto == "https" else "ws"
|
68
|
-
)
|
55
|
+
scope["scheme"] = x_forwarded_proto.replace("http", "ws")
|
69
56
|
else:
|
70
57
|
scope["scheme"] = x_forwarded_proto
|
71
58
|
|
@@ -74,9 +61,7 @@ class ProxyHeadersMiddleware:
|
|
74
61
|
# X-Forwarded-For header. We've lost the connecting client's port
|
75
62
|
# information by now, so only include the host.
|
76
63
|
x_forwarded_for = headers[b"x-forwarded-for"].decode("latin1")
|
77
|
-
x_forwarded_for_hosts = [
|
78
|
-
item.strip() for item in x_forwarded_for.split(",")
|
79
|
-
]
|
64
|
+
x_forwarded_for_hosts = [item.strip() for item in x_forwarded_for.split(",")]
|
80
65
|
host = self.get_trusted_client_host(x_forwarded_for_hosts)
|
81
66
|
port = 0
|
82
67
|
scope["client"] = (host, port) # type: ignore[arg-type]
|
uvicorn/middleware/wsgi.py
CHANGED
@@ -1,10 +1,12 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import asyncio
|
2
4
|
import concurrent.futures
|
3
5
|
import io
|
4
6
|
import sys
|
5
7
|
import warnings
|
6
8
|
from collections import deque
|
7
|
-
from typing import
|
9
|
+
from typing import Iterable
|
8
10
|
|
9
11
|
from uvicorn._types import (
|
10
12
|
ASGIReceiveCallable,
|
@@ -22,9 +24,7 @@ from uvicorn._types import (
|
|
22
24
|
)
|
23
25
|
|
24
26
|
|
25
|
-
def build_environ(
|
26
|
-
scope: "HTTPScope", message: "ASGIReceiveEvent", body: io.BytesIO
|
27
|
-
) -> Environ:
|
27
|
+
def build_environ(scope: HTTPScope, message: ASGIReceiveEvent, body: io.BytesIO) -> Environ:
|
28
28
|
"""
|
29
29
|
Builds a scope and request message into a WSGI environ object.
|
30
30
|
"""
|
@@ -91,9 +91,9 @@ class _WSGIMiddleware:
|
|
91
91
|
|
92
92
|
async def __call__(
|
93
93
|
self,
|
94
|
-
scope:
|
95
|
-
receive:
|
96
|
-
send:
|
94
|
+
scope: HTTPScope,
|
95
|
+
receive: ASGIReceiveCallable,
|
96
|
+
send: ASGISendCallable,
|
97
97
|
) -> None:
|
98
98
|
assert scope["type"] == "http"
|
99
99
|
instance = WSGIResponder(self.app, self.executor, scope)
|
@@ -105,7 +105,7 @@ class WSGIResponder:
|
|
105
105
|
self,
|
106
106
|
app: WSGIApp,
|
107
107
|
executor: concurrent.futures.ThreadPoolExecutor,
|
108
|
-
scope:
|
108
|
+
scope: HTTPScope,
|
109
109
|
):
|
110
110
|
self.app = app
|
111
111
|
self.executor = executor
|
@@ -113,21 +113,19 @@ class WSGIResponder:
|
|
113
113
|
self.status = None
|
114
114
|
self.response_headers = None
|
115
115
|
self.send_event = asyncio.Event()
|
116
|
-
self.send_queue:
|
116
|
+
self.send_queue: deque[ASGISendEvent | None] = deque()
|
117
117
|
self.loop: asyncio.AbstractEventLoop = asyncio.get_event_loop()
|
118
118
|
self.response_started = False
|
119
|
-
self.exc_info:
|
119
|
+
self.exc_info: ExcInfo | None = None
|
120
120
|
|
121
|
-
async def __call__(
|
122
|
-
self, receive: "ASGIReceiveCallable", send: "ASGISendCallable"
|
123
|
-
) -> None:
|
121
|
+
async def __call__(self, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None:
|
124
122
|
message: HTTPRequestEvent = await receive() # type: ignore[assignment]
|
125
123
|
body = io.BytesIO(message.get("body", b""))
|
126
124
|
more_body = message.get("more_body", False)
|
127
125
|
if more_body:
|
128
126
|
body.seek(0, io.SEEK_END)
|
129
127
|
while more_body:
|
130
|
-
body_message:
|
128
|
+
body_message: HTTPRequestEvent = (
|
131
129
|
await receive() # type: ignore[assignment]
|
132
130
|
)
|
133
131
|
body.write(body_message.get("body", b""))
|
@@ -135,9 +133,7 @@ class WSGIResponder:
|
|
135
133
|
body.seek(0)
|
136
134
|
environ = build_environ(self.scope, message, body)
|
137
135
|
self.loop = asyncio.get_event_loop()
|
138
|
-
wsgi = self.loop.run_in_executor(
|
139
|
-
self.executor, self.wsgi, environ, self.start_response
|
140
|
-
)
|
136
|
+
wsgi = self.loop.run_in_executor(self.executor, self.wsgi, environ, self.start_response)
|
141
137
|
sender = self.loop.create_task(self.sender(send))
|
142
138
|
try:
|
143
139
|
await asyncio.wait_for(wsgi, None)
|
@@ -148,7 +144,7 @@ class WSGIResponder:
|
|
148
144
|
if self.exc_info is not None:
|
149
145
|
raise self.exc_info[0].with_traceback(self.exc_info[1], self.exc_info[2])
|
150
146
|
|
151
|
-
async def sender(self, send:
|
147
|
+
async def sender(self, send: ASGISendCallable) -> None:
|
152
148
|
while True:
|
153
149
|
if self.send_queue:
|
154
150
|
message = self.send_queue.popleft()
|
@@ -162,18 +158,15 @@ class WSGIResponder:
|
|
162
158
|
def start_response(
|
163
159
|
self,
|
164
160
|
status: str,
|
165
|
-
response_headers: Iterable[
|
166
|
-
exc_info:
|
161
|
+
response_headers: Iterable[tuple[str, str]],
|
162
|
+
exc_info: ExcInfo | None = None,
|
167
163
|
) -> None:
|
168
164
|
self.exc_info = exc_info
|
169
165
|
if not self.response_started:
|
170
166
|
self.response_started = True
|
171
167
|
status_code_str, _ = status.split(" ", 1)
|
172
168
|
status_code = int(status_code_str)
|
173
|
-
headers = [
|
174
|
-
(name.encode("ascii"), value.encode("ascii"))
|
175
|
-
for name, value in response_headers
|
176
|
-
]
|
169
|
+
headers = [(name.encode("ascii"), value.encode("ascii")) for name, value in response_headers]
|
177
170
|
http_response_start_event: HTTPResponseStartEvent = {
|
178
171
|
"type": "http.response.start",
|
179
172
|
"status": status_code,
|
@@ -45,9 +45,7 @@ class FlowControl:
|
|
45
45
|
self._is_writable_event.set()
|
46
46
|
|
47
47
|
|
48
|
-
async def service_unavailable(
|
49
|
-
scope: "Scope", receive: "ASGIReceiveCallable", send: "ASGISendCallable"
|
50
|
-
) -> None:
|
48
|
+
async def service_unavailable(scope: "Scope", receive: "ASGIReceiveCallable", send: "ASGISendCallable") -> None:
|
51
49
|
response_start: "HTTPResponseStartEvent" = {
|
52
50
|
"type": "http.response.start",
|
53
51
|
"status": 503,
|