Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AsgiWhiteNoise and async WhiteNoiseMiddleware #359

Open
wants to merge 131 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
131 commits
Select commit Hold shift + click to select a range
9a56981
Add ASGI adapter
kmichel Sep 14, 2020
8590e82
add venv to gitignore
Archmonger Feb 7, 2022
24c5319
Merge remote-tracking branch 'upstream/master' into asgi-compat
Archmonger Feb 7, 2022
391bd8e
add aiofile
Archmonger Feb 7, 2022
3e735ec
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 7, 2022
42fbe15
Merge branch 'asgi-compat' of https://github.com/Archmonger/whitenois…
Archmonger Feb 7, 2022
aec9ac1
Revert "add venv to gitignore"
Archmonger Feb 8, 2022
cf4ee09
add ASGI extra
Archmonger Feb 10, 2022
9616e78
absolute imports
Archmonger Feb 10, 2022
35b9003
use SimpleNamespace
Archmonger Feb 10, 2022
8eef8d3
add zero-copy send todo
Archmonger Feb 10, 2022
2f0f685
replace empty equality with boolean operation
Archmonger Feb 10, 2022
f4931e3
minor syntax or performance improvements
Archmonger Feb 10, 2022
3fb5a38
BaseWhiteNoise class
Archmonger Feb 10, 2022
3bb4088
Merge remote-tracking branch 'upstream/master' into asgi-compat
Archmonger Feb 10, 2022
8a0930e
use f-strings
Archmonger Feb 10, 2022
e6fd03b
fix imports
Archmonger Feb 10, 2022
8fc1682
Merge remote-tracking branch 'upstream/master' into asgi-compat
Archmonger Feb 10, 2022
64fc5c7
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 10, 2022
83fea65
Revert "minor syntax or performance improvements"
Archmonger Feb 10, 2022
58f3743
Revert "use f-strings"
Archmonger Feb 10, 2022
f79aec7
re-add missing import
Archmonger Feb 10, 2022
e18df38
Remove upper bound
Archmonger Feb 10, 2022
45301b2
fix recceive call
Archmonger Feb 10, 2022
88ccf21
fix some of the tests
Archmonger Feb 11, 2022
998cf5e
temporarily remove broken tests
Archmonger Feb 11, 2022
d611e90
fix py3.10 deprecation warning
Archmonger Feb 11, 2022
54c7148
convert wsgi to asgi
Archmonger Feb 12, 2022
1057a77
remove contrived tests
Archmonger Feb 12, 2022
ea2e84c
AsgiWhiteNoise -> AsyncWhiteNoise
Archmonger Feb 12, 2022
6dcecc0
Revert "convert wsgi to asgi"
Archmonger Feb 12, 2022
c506e96
ASGI v3 static file server
Archmonger Jun 21, 2023
9892bea
Merge remote-tracking branch 'upstream/main' into asgi-compat
Archmonger Jun 21, 2023
e636411
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 21, 2023
41468a1
guarantee_single_callable
Archmonger Jun 22, 2023
08c70fb
customizable block size
Archmonger Jun 22, 2023
240ce22
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 22, 2023
be3796c
clean up comment
Archmonger Jun 22, 2023
b81ab6b
Merge branch 'asgi-compat' of https://github.com/Archmonger/whitenois…
Archmonger Jun 22, 2023
7aeb29a
Add functional middleware
Archmonger Jun 22, 2023
cd0ab1b
add aiofiles to all tests
Archmonger Jun 22, 2023
1265f03
temporarily add aiofiles to tox.ini
Archmonger Jun 22, 2023
f5e9d37
temporarily disable asgi tests
Archmonger Jun 22, 2023
0e42cb8
async -> asgi
Archmonger Jun 22, 2023
279b342
aiofile != aiofiles
Archmonger Jul 20, 2023
890fdd8
fix comment
Archmonger Jul 20, 2023
ba3bed4
properly close Django file responses
Archmonger Jul 21, 2023
7af5b32
async find_file within AsgiWhiteNoise
Archmonger Jul 21, 2023
ee068a7
AsyncSlicedFile
Archmonger Jul 21, 2023
f09b384
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 21, 2023
ca26354
simplify __call__ method
Archmonger Jul 21, 2023
c2c320c
Add WSGI compat docstring
Archmonger Jul 21, 2023
d0e9bfa
Merge branch 'asgi-compat' of https://github.com/Archmonger/whitenois…
Archmonger Jul 21, 2023
33ad918
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 21, 2023
067ab33
aget_response update comment
Archmonger Jul 21, 2023
a15422f
add prototypes and warnings for call/serve
Archmonger Jul 21, 2023
8d01092
update middleware comment
Archmonger Jul 21, 2023
d6bb4d8
fix compatibilty with SecurityMiddleware
Archmonger Jul 21, 2023
2a386cc
fix name
Archmonger Jul 21, 2023
06a874f
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 21, 2023
1f9f8b3
refactor WhiteNoiseFileResponse
Archmonger Jul 21, 2023
64c1c27
reduce code duplication for AsyncSlicedFile
Archmonger Jul 21, 2023
f3b00ab
Merge branch 'asgi-compat' of https://github.com/Archmonger/whitenois…
Archmonger Jul 21, 2023
68f19c2
update WhiteNoiseFileResponse docstring
Archmonger Jul 21, 2023
00f064d
customizable block_size
Archmonger Jul 21, 2023
e5f7526
docstring update
Archmonger Jul 21, 2023
9ee0d7d
rename aiofile param
Archmonger Jul 21, 2023
d97a05b
fix file seeking
Archmonger Jul 23, 2023
2dbc8b2
add async pytest for django
Archmonger Jul 23, 2023
de3456e
merge imports
Archmonger Jul 23, 2023
f0f0a66
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 23, 2023
511da12
comment out everything from test_asgi
Archmonger Jul 23, 2023
7fd30ef
handle file closure within iterator
Archmonger Jul 24, 2023
62de430
remove pytest asyncio
Archmonger Jul 24, 2023
bb62496
async file closure
Archmonger Jul 24, 2023
d839cf8
Py 3.8 compatibility
Archmonger Jul 24, 2023
b40ff65
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 24, 2023
522a50b
remove unused import
Archmonger Jul 24, 2023
518a46b
start from scratch for ASGI tests
Archmonger Jul 24, 2023
ed07496
Add async iterator support to old django versions
Archmonger Jul 24, 2023
3bff168
Merge branch 'asgi-compat' of https://github.com/Archmonger/whitenois…
Archmonger Jul 24, 2023
4c98b5e
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 24, 2023
4656a53
new base for AsyncSlicedFile
Archmonger Jul 24, 2023
4a77e8b
Py 3.12+ compatibility
Archmonger Jul 24, 2023
dd2d700
Merge branch 'asgi-compat' of https://github.com/Archmonger/whitenois…
Archmonger Jul 24, 2023
ea74977
refactor file closure and docstrings
Archmonger Jul 25, 2023
2bc2a7b
way more performant way of retaining WSGI compatibility
Archmonger Jul 26, 2023
d648ffc
move file closure to AsgiFileServer loop
Archmonger Jul 26, 2023
f6bf0c2
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 26, 2023
367a17d
Support for ASGI on Django<4.2
Archmonger Jul 26, 2023
e3bea39
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 26, 2023
b895b8f
resolve flake8 lint warning
Archmonger Jul 26, 2023
7132626
Remove `USE_ASYNC`. Always use the best available method.
Archmonger Jul 27, 2023
56fe9d1
remove type hints that break visual similarity between sync/async code
Archmonger Jul 27, 2023
17b9dc7
reduce tab depth
Archmonger Jul 27, 2023
2dc620e
add first batch of ASGI tests
Archmonger Jul 27, 2023
6d4baf3
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 27, 2023
f8fb573
complete tests for asgi.py
Archmonger Jul 28, 2023
b91dd7e
get_response is always mandatory
Archmonger Jul 28, 2023
5eaac51
Merge branch 'asgi-compat' of https://github.com/Archmonger/whitenois…
Archmonger Jul 28, 2023
292de02
More tests
Archmonger Jul 28, 2023
3117769
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 28, 2023
5de2d63
minor refactoring
Archmonger Jul 28, 2023
6197911
Merge branch 'asgi-compat' of https://github.com/Archmonger/whitenois…
Archmonger Jul 28, 2023
37dfac6
Make aiofiles mandatory because of Django
Archmonger Jul 28, 2023
f20e90d
format setup.cfg
Archmonger Jul 28, 2023
865a68d
WHITENOISE_BLOCK_SIZE docs
Archmonger Jul 28, 2023
d8f564e
rename base to wsgi
Archmonger Jul 28, 2023
4c2ca1d
QuickStart for other ASGI apps
Archmonger Jul 28, 2023
b238278
AsgiWhiteNoise docs
Archmonger Jul 28, 2023
fb118ad
Merge remote-tracking branch 'upstream/main' into asgi-compat
Archmonger Jul 28, 2023
8e8c63b
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 28, 2023
bd580a2
minor comment cleanup
Archmonger Jul 28, 2023
371ca90
minor docs wordsmithing
Archmonger Jul 28, 2023
6695140
Remove block size configuration attributes
Archmonger Jul 28, 2023
018f864
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 28, 2023
3a0180a
use dict comphrension for header conversion
Archmonger Jul 28, 2023
29c678c
headers -> wsgi_headers
Archmonger Jul 28, 2023
6da4dfd
serve doesn't need stubs
Archmonger Jul 28, 2023
b1c8ae3
Add ASGI to readme and pkg info
Archmonger Jul 29, 2023
88e8a86
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 29, 2023
1f0fa9f
More readible way of converting ASGI app
Archmonger Aug 2, 2023
5b74486
asgi_app -> user_app
Archmonger Aug 2, 2023
9f77bf7
no need for make_bytes
Archmonger Aug 4, 2023
484ddb4
make middleware sync capable
Archmonger Aug 6, 2023
07b8b3d
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 6, 2023
4db8458
reduce LOC changes for serve
Archmonger Aug 6, 2023
503e957
rename to `AsyncSlicedFile`
Archmonger Aug 7, 2023
67f8dee
Update tests/test_asgi.py
Archmonger Mar 23, 2024
1f8c4b2
Update docs/asgi.rst
Archmonger Mar 23, 2024
aa36dce
Update docs/asgi.rst
Archmonger Mar 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions setup.cfg
Expand Up @@ -43,6 +43,8 @@ zip_safe = False
[options.extras_require]
brotli =
Brotli
asgi =
aiofile >=3.0, <4.0
Archmonger marked this conversation as resolved.
Show resolved Hide resolved

[options.packages.find]
where = src
Expand Down
5 changes: 3 additions & 2 deletions src/whitenoise/__init__.py
@@ -1,5 +1,6 @@
from __future__ import annotations

from .base import WhiteNoise
from .asgi import AsgiWhiteNoise
from .wsgi import WhiteNoise

__all__ = ["WhiteNoise"]
__all__ = ["AsgiWhiteNoise", "WhiteNoise"]
86 changes: 86 additions & 0 deletions src/whitenoise/asgi.py
@@ -0,0 +1,86 @@
from __future__ import annotations

from whitenoise.base import BaseWhiteNoise


class AsgiWhiteNoise(BaseWhiteNoise):
# This is the same block size as wsgiref.FileWrapper
BLOCK_SIZE = 8192

async def __call__(self, scope, receive, send):
static_file = None
if scope["type"] == "http":
if self.autorefresh:
static_file = self.find_file(scope["path"])
else:
static_file = self.files.get(scope["path"])
if static_file is None:
await self.application(scope, receive, send)
else:
await receive(receive)
request_headers = convert_asgi_headers(scope["headers"])
await self.serve(
send, static_file, scope["method"], request_headers, self.BLOCK_SIZE
)

@staticmethod
async def serve(send, static_file, method, request_headers, block_size):
response = static_file.get_response(method, request_headers)
try:
await send(
{
"type": "http.response.start",
"status": response.status.value,
"headers": convert_wsgi_headers(response.headers),
}
)
if response.file:
# We need to only read content-length bytes instead of the whole file,
# the difference is important when serving range requests.
content_length = int(dict(response.headers)["Content-Length"])
for block in read_file(response.file, content_length, block_size):
# TODO: Recode this when ASGI webservers to support zero-copy send
# See https://asgi.readthedocs.io/en/latest/extensions.html#zero-copy-send
await send(
{"type": "http.response.body", "body": block, "more_body": True}
)
await send({"type": "http.response.body"})
finally:
if response.file:
response.file.close()

@staticmethod
async def receive(receive):
more_body = True
while more_body:
event = await receive()
if event["type"] != "http.request":
raise RuntimeError(
"Unexpected ASGI event {!r}, expected {!r}".format(
event["type"], "http.request"
)
)
more_body = event.get("more_body", False)


def read_file(file_handle, content_length, block_size):
bytes_left = content_length
while bytes_left > 0:
data = file_handle.read(min(block_size, bytes_left))
if data == b"":
raise RuntimeError(
f"Premature end of file, expected {bytes_left} more bytes"
)
bytes_left -= len(data)
yield data


def convert_asgi_headers(headers):
return {
"HTTP_" + name.decode().upper().replace("-", "_"): value.decode()
for name, value in headers
}


def convert_wsgi_headers(headers):
return [(key.lower().encode(), value.encode()) for key, value in headers]
33 changes: 3 additions & 30 deletions src/whitenoise/base.py
Expand Up @@ -5,18 +5,13 @@
import warnings
from posixpath import normpath
from wsgiref.headers import Headers
from wsgiref.util import FileWrapper

from .media_types import MediaTypes
from .responders import IsDirectoryError, MissingFileError, Redirect, StaticFile
from .string_utils import (
decode_if_byte_string,
decode_path_info,
ensure_leading_trailing_slash,
)
from .responders import MissingFileError, Redirect, StaticFile
from .string_utils import decode_if_byte_string, ensure_leading_trailing_slash


class WhiteNoise:
class BaseWhiteNoise:

# Ten years is what nginx sets a max age if you use 'expires max;'
# so we'll follow its lead
Expand Down Expand Up @@ -75,28 +70,6 @@ def __init__(self, application, root=None, prefix=None, **kwargs):
if root is not None:
self.add_files(root, prefix)

def __call__(self, environ, start_response):
path = decode_path_info(environ.get("PATH_INFO", ""))
if self.autorefresh:
static_file = self.find_file(path)
else:
static_file = self.files.get(path)
if static_file is None:
return self.application(environ, start_response)
else:
return self.serve(static_file, environ, start_response)

@staticmethod
def serve(static_file, environ, start_response):
response = static_file.get_response(environ["REQUEST_METHOD"], environ)
status_line = f"{response.status} {response.status.phrase}"
start_response(status_line, list(response.headers))
if response.file is not None:
file_wrapper = environ.get("wsgi.file_wrapper", FileWrapper)
return file_wrapper(response.file)
else:
return []

def add_files(self, root, prefix=None):
root = decode_if_byte_string(root, force_text=True)
root = os.path.abspath(root)
Expand Down
2 changes: 1 addition & 1 deletion src/whitenoise/middleware.py
Expand Up @@ -10,8 +10,8 @@
from django.http import FileResponse
from django.urls import get_script_prefix

from .base import WhiteNoise
from .string_utils import decode_if_byte_string, ensure_leading_trailing_slash
from .wsgi import WhiteNoise

__all__ = ["WhiteNoiseMiddleware"]

Expand Down
30 changes: 30 additions & 0 deletions src/whitenoise/wsgi.py
@@ -0,0 +1,30 @@
from __future__ import annotations

from wsgiref.util import FileWrapper

from .base import BaseWhiteNoise
from .string_utils import decode_path_info


class WhiteNoise(BaseWhiteNoise):
def __call__(self, environ, start_response):
path = decode_path_info(environ.get("PATH_INFO", ""))
if self.autorefresh:
static_file = self.find_file(path)
else:
static_file = self.files.get(path)
if static_file is None:
return self.application(environ, start_response)
else:
return self.serve(static_file, environ, start_response)

@staticmethod
def serve(static_file, environ, start_response):
response = static_file.get_response(environ["REQUEST_METHOD"], environ)
status_line = f"{response.status} {response.status.phrase}"
start_response(status_line, list(response.headers))
if response.file is not None:
file_wrapper = environ.get("wsgi.file_wrapper", FileWrapper)
return file_wrapper(response.file)
else:
return []