Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .gitattributes +1 -0
- .venv/lib/python3.11/site-packages/rpds/rpds.cpython-311-x86_64-linux-gnu.so +3 -0
- .venv/lib/python3.11/site-packages/starlette/__init__.py +1 -0
- .venv/lib/python3.11/site-packages/starlette/__pycache__/__init__.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/starlette/__pycache__/_exception_handler.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/starlette/__pycache__/_utils.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/starlette/__pycache__/applications.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/starlette/__pycache__/authentication.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/starlette/__pycache__/background.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/starlette/__pycache__/concurrency.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/starlette/__pycache__/config.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/starlette/__pycache__/convertors.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/starlette/__pycache__/datastructures.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/starlette/__pycache__/endpoints.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/starlette/__pycache__/exceptions.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/starlette/__pycache__/formparsers.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/starlette/__pycache__/requests.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/starlette/__pycache__/responses.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/starlette/__pycache__/routing.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/starlette/__pycache__/schemas.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/starlette/__pycache__/staticfiles.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/starlette/__pycache__/status.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/starlette/__pycache__/templating.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/starlette/__pycache__/testclient.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/starlette/__pycache__/types.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/starlette/__pycache__/websockets.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/starlette/_utils.py +100 -0
- .venv/lib/python3.11/site-packages/starlette/authentication.py +147 -0
- .venv/lib/python3.11/site-packages/starlette/background.py +41 -0
- .venv/lib/python3.11/site-packages/starlette/endpoints.py +122 -0
- .venv/lib/python3.11/site-packages/starlette/formparsers.py +272 -0
- .venv/lib/python3.11/site-packages/starlette/middleware/__init__.py +42 -0
- .venv/lib/python3.11/site-packages/starlette/middleware/__pycache__/__init__.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/starlette/middleware/__pycache__/authentication.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/starlette/middleware/__pycache__/base.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/starlette/middleware/__pycache__/cors.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/starlette/middleware/__pycache__/errors.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/starlette/middleware/__pycache__/exceptions.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/starlette/middleware/__pycache__/gzip.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/starlette/middleware/__pycache__/httpsredirect.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/starlette/middleware/__pycache__/sessions.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/starlette/middleware/__pycache__/trustedhost.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/starlette/middleware/__pycache__/wsgi.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/starlette/middleware/authentication.py +52 -0
- .venv/lib/python3.11/site-packages/starlette/middleware/base.py +221 -0
- .venv/lib/python3.11/site-packages/starlette/middleware/cors.py +172 -0
- .venv/lib/python3.11/site-packages/starlette/middleware/errors.py +260 -0
- .venv/lib/python3.11/site-packages/starlette/middleware/exceptions.py +72 -0
- .venv/lib/python3.11/site-packages/starlette/middleware/gzip.py +108 -0
- .venv/lib/python3.11/site-packages/starlette/middleware/httpsredirect.py +19 -0
.gitattributes
CHANGED
|
@@ -207,3 +207,4 @@ tuning-competition-baseline/.venv/lib/python3.11/site-packages/torch/_inductor/_
|
|
| 207 |
.venv/lib/python3.11/site-packages/wrapt/_wrappers.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
|
| 208 |
.venv/lib/python3.11/site-packages/__pycache__/typing_extensions.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
|
| 209 |
.venv/lib/python3.11/site-packages/__pycache__/pynvml.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 207 |
.venv/lib/python3.11/site-packages/wrapt/_wrappers.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
|
| 208 |
.venv/lib/python3.11/site-packages/__pycache__/typing_extensions.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
|
| 209 |
.venv/lib/python3.11/site-packages/__pycache__/pynvml.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
|
| 210 |
+
.venv/lib/python3.11/site-packages/rpds/rpds.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
|
.venv/lib/python3.11/site-packages/rpds/rpds.cpython-311-x86_64-linux-gnu.so
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:aded6ee5bd881096565cbd54a06c9cb432b1ec9e69b4fdc29f37f32c4573be16
|
| 3 |
+
size 1015312
|
.venv/lib/python3.11/site-packages/starlette/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
__version__ = "0.45.3"
|
.venv/lib/python3.11/site-packages/starlette/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (203 Bytes). View file
|
|
|
.venv/lib/python3.11/site-packages/starlette/__pycache__/_exception_handler.cpython-311.pyc
ADDED
|
Binary file (3.61 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/starlette/__pycache__/_utils.cpython-311.pyc
ADDED
|
Binary file (5.87 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/starlette/__pycache__/applications.cpython-311.pyc
ADDED
|
Binary file (13.8 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/starlette/__pycache__/authentication.cpython-311.pyc
ADDED
|
Binary file (9.21 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/starlette/__pycache__/background.cpython-311.pyc
ADDED
|
Binary file (2.95 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/starlette/__pycache__/concurrency.cpython-311.pyc
ADDED
|
Binary file (3.7 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/starlette/__pycache__/config.cpython-311.pyc
ADDED
|
Binary file (8.26 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/starlette/__pycache__/convertors.cpython-311.pyc
ADDED
|
Binary file (5.77 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/starlette/__pycache__/datastructures.cpython-311.pyc
ADDED
|
Binary file (45.7 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/starlette/__pycache__/endpoints.cpython-311.pyc
ADDED
|
Binary file (8.7 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/starlette/__pycache__/exceptions.cpython-311.pyc
ADDED
|
Binary file (2.61 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/starlette/__pycache__/formparsers.cpython-311.pyc
ADDED
|
Binary file (15 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/starlette/__pycache__/requests.cpython-311.pyc
ADDED
|
Binary file (17.8 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/starlette/__pycache__/responses.cpython-311.pyc
ADDED
|
Binary file (32.3 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/starlette/__pycache__/routing.cpython-311.pyc
ADDED
|
Binary file (48.5 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/starlette/__pycache__/schemas.cpython-311.pyc
ADDED
|
Binary file (8.04 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/starlette/__pycache__/staticfiles.cpython-311.pyc
ADDED
|
Binary file (12.6 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/starlette/__pycache__/status.cpython-311.pyc
ADDED
|
Binary file (3.65 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/starlette/__pycache__/templating.cpython-311.pyc
ADDED
|
Binary file (11.1 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/starlette/__pycache__/testclient.cpython-311.pyc
ADDED
|
Binary file (35.5 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/starlette/__pycache__/types.cpython-311.pyc
ADDED
|
Binary file (1.68 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/starlette/__pycache__/websockets.cpython-311.pyc
ADDED
|
Binary file (13 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/starlette/_utils.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import asyncio
|
| 4 |
+
import functools
|
| 5 |
+
import sys
|
| 6 |
+
import typing
|
| 7 |
+
from contextlib import contextmanager
|
| 8 |
+
|
| 9 |
+
from starlette.types import Scope
|
| 10 |
+
|
| 11 |
+
if sys.version_info >= (3, 10): # pragma: no cover
|
| 12 |
+
from typing import TypeGuard
|
| 13 |
+
else: # pragma: no cover
|
| 14 |
+
from typing_extensions import TypeGuard
|
| 15 |
+
|
| 16 |
+
has_exceptiongroups = True
|
| 17 |
+
if sys.version_info < (3, 11): # pragma: no cover
|
| 18 |
+
try:
|
| 19 |
+
from exceptiongroup import BaseExceptionGroup # type: ignore[unused-ignore,import-not-found]
|
| 20 |
+
except ImportError:
|
| 21 |
+
has_exceptiongroups = False
|
| 22 |
+
|
| 23 |
+
T = typing.TypeVar("T")
|
| 24 |
+
AwaitableCallable = typing.Callable[..., typing.Awaitable[T]]
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
@typing.overload
|
| 28 |
+
def is_async_callable(obj: AwaitableCallable[T]) -> TypeGuard[AwaitableCallable[T]]: ...
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
@typing.overload
|
| 32 |
+
def is_async_callable(obj: typing.Any) -> TypeGuard[AwaitableCallable[typing.Any]]: ...
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def is_async_callable(obj: typing.Any) -> typing.Any:
|
| 36 |
+
while isinstance(obj, functools.partial):
|
| 37 |
+
obj = obj.func
|
| 38 |
+
|
| 39 |
+
return asyncio.iscoroutinefunction(obj) or (callable(obj) and asyncio.iscoroutinefunction(obj.__call__))
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
T_co = typing.TypeVar("T_co", covariant=True)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
class AwaitableOrContextManager(typing.Awaitable[T_co], typing.AsyncContextManager[T_co], typing.Protocol[T_co]): ...
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class SupportsAsyncClose(typing.Protocol):
|
| 49 |
+
async def close(self) -> None: ... # pragma: no cover
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
SupportsAsyncCloseType = typing.TypeVar("SupportsAsyncCloseType", bound=SupportsAsyncClose, covariant=False)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
class AwaitableOrContextManagerWrapper(typing.Generic[SupportsAsyncCloseType]):
|
| 56 |
+
__slots__ = ("aw", "entered")
|
| 57 |
+
|
| 58 |
+
def __init__(self, aw: typing.Awaitable[SupportsAsyncCloseType]) -> None:
|
| 59 |
+
self.aw = aw
|
| 60 |
+
|
| 61 |
+
def __await__(self) -> typing.Generator[typing.Any, None, SupportsAsyncCloseType]:
|
| 62 |
+
return self.aw.__await__()
|
| 63 |
+
|
| 64 |
+
async def __aenter__(self) -> SupportsAsyncCloseType:
|
| 65 |
+
self.entered = await self.aw
|
| 66 |
+
return self.entered
|
| 67 |
+
|
| 68 |
+
async def __aexit__(self, *args: typing.Any) -> None | bool:
|
| 69 |
+
await self.entered.close()
|
| 70 |
+
return None
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
@contextmanager
|
| 74 |
+
def collapse_excgroups() -> typing.Generator[None, None, None]:
|
| 75 |
+
try:
|
| 76 |
+
yield
|
| 77 |
+
except BaseException as exc:
|
| 78 |
+
if has_exceptiongroups: # pragma: no cover
|
| 79 |
+
while isinstance(exc, BaseExceptionGroup) and len(exc.exceptions) == 1:
|
| 80 |
+
exc = exc.exceptions[0]
|
| 81 |
+
|
| 82 |
+
raise exc
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def get_route_path(scope: Scope) -> str:
|
| 86 |
+
path: str = scope["path"]
|
| 87 |
+
root_path = scope.get("root_path", "")
|
| 88 |
+
if not root_path:
|
| 89 |
+
return path
|
| 90 |
+
|
| 91 |
+
if not path.startswith(root_path):
|
| 92 |
+
return path
|
| 93 |
+
|
| 94 |
+
if path == root_path:
|
| 95 |
+
return ""
|
| 96 |
+
|
| 97 |
+
if path[len(root_path)] == "/":
|
| 98 |
+
return path[len(root_path) :]
|
| 99 |
+
|
| 100 |
+
return path
|
.venv/lib/python3.11/site-packages/starlette/authentication.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import functools
|
| 4 |
+
import inspect
|
| 5 |
+
import sys
|
| 6 |
+
import typing
|
| 7 |
+
from urllib.parse import urlencode
|
| 8 |
+
|
| 9 |
+
if sys.version_info >= (3, 10): # pragma: no cover
|
| 10 |
+
from typing import ParamSpec
|
| 11 |
+
else: # pragma: no cover
|
| 12 |
+
from typing_extensions import ParamSpec
|
| 13 |
+
|
| 14 |
+
from starlette._utils import is_async_callable
|
| 15 |
+
from starlette.exceptions import HTTPException
|
| 16 |
+
from starlette.requests import HTTPConnection, Request
|
| 17 |
+
from starlette.responses import RedirectResponse
|
| 18 |
+
from starlette.websockets import WebSocket
|
| 19 |
+
|
| 20 |
+
_P = ParamSpec("_P")
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def has_required_scope(conn: HTTPConnection, scopes: typing.Sequence[str]) -> bool:
|
| 24 |
+
for scope in scopes:
|
| 25 |
+
if scope not in conn.auth.scopes:
|
| 26 |
+
return False
|
| 27 |
+
return True
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def requires(
|
| 31 |
+
scopes: str | typing.Sequence[str],
|
| 32 |
+
status_code: int = 403,
|
| 33 |
+
redirect: str | None = None,
|
| 34 |
+
) -> typing.Callable[[typing.Callable[_P, typing.Any]], typing.Callable[_P, typing.Any]]:
|
| 35 |
+
scopes_list = [scopes] if isinstance(scopes, str) else list(scopes)
|
| 36 |
+
|
| 37 |
+
def decorator(
|
| 38 |
+
func: typing.Callable[_P, typing.Any],
|
| 39 |
+
) -> typing.Callable[_P, typing.Any]:
|
| 40 |
+
sig = inspect.signature(func)
|
| 41 |
+
for idx, parameter in enumerate(sig.parameters.values()):
|
| 42 |
+
if parameter.name == "request" or parameter.name == "websocket":
|
| 43 |
+
type_ = parameter.name
|
| 44 |
+
break
|
| 45 |
+
else:
|
| 46 |
+
raise Exception(f'No "request" or "websocket" argument on function "{func}"')
|
| 47 |
+
|
| 48 |
+
if type_ == "websocket":
|
| 49 |
+
# Handle websocket functions. (Always async)
|
| 50 |
+
@functools.wraps(func)
|
| 51 |
+
async def websocket_wrapper(*args: _P.args, **kwargs: _P.kwargs) -> None:
|
| 52 |
+
websocket = kwargs.get("websocket", args[idx] if idx < len(args) else None)
|
| 53 |
+
assert isinstance(websocket, WebSocket)
|
| 54 |
+
|
| 55 |
+
if not has_required_scope(websocket, scopes_list):
|
| 56 |
+
await websocket.close()
|
| 57 |
+
else:
|
| 58 |
+
await func(*args, **kwargs)
|
| 59 |
+
|
| 60 |
+
return websocket_wrapper
|
| 61 |
+
|
| 62 |
+
elif is_async_callable(func):
|
| 63 |
+
# Handle async request/response functions.
|
| 64 |
+
@functools.wraps(func)
|
| 65 |
+
async def async_wrapper(*args: _P.args, **kwargs: _P.kwargs) -> typing.Any:
|
| 66 |
+
request = kwargs.get("request", args[idx] if idx < len(args) else None)
|
| 67 |
+
assert isinstance(request, Request)
|
| 68 |
+
|
| 69 |
+
if not has_required_scope(request, scopes_list):
|
| 70 |
+
if redirect is not None:
|
| 71 |
+
orig_request_qparam = urlencode({"next": str(request.url)})
|
| 72 |
+
next_url = f"{request.url_for(redirect)}?{orig_request_qparam}"
|
| 73 |
+
return RedirectResponse(url=next_url, status_code=303)
|
| 74 |
+
raise HTTPException(status_code=status_code)
|
| 75 |
+
return await func(*args, **kwargs)
|
| 76 |
+
|
| 77 |
+
return async_wrapper
|
| 78 |
+
|
| 79 |
+
else:
|
| 80 |
+
# Handle sync request/response functions.
|
| 81 |
+
@functools.wraps(func)
|
| 82 |
+
def sync_wrapper(*args: _P.args, **kwargs: _P.kwargs) -> typing.Any:
|
| 83 |
+
request = kwargs.get("request", args[idx] if idx < len(args) else None)
|
| 84 |
+
assert isinstance(request, Request)
|
| 85 |
+
|
| 86 |
+
if not has_required_scope(request, scopes_list):
|
| 87 |
+
if redirect is not None:
|
| 88 |
+
orig_request_qparam = urlencode({"next": str(request.url)})
|
| 89 |
+
next_url = f"{request.url_for(redirect)}?{orig_request_qparam}"
|
| 90 |
+
return RedirectResponse(url=next_url, status_code=303)
|
| 91 |
+
raise HTTPException(status_code=status_code)
|
| 92 |
+
return func(*args, **kwargs)
|
| 93 |
+
|
| 94 |
+
return sync_wrapper
|
| 95 |
+
|
| 96 |
+
return decorator
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
class AuthenticationError(Exception):
|
| 100 |
+
pass
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
class AuthenticationBackend:
|
| 104 |
+
async def authenticate(self, conn: HTTPConnection) -> tuple[AuthCredentials, BaseUser] | None:
|
| 105 |
+
raise NotImplementedError() # pragma: no cover
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
class AuthCredentials:
|
| 109 |
+
def __init__(self, scopes: typing.Sequence[str] | None = None):
|
| 110 |
+
self.scopes = [] if scopes is None else list(scopes)
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
class BaseUser:
|
| 114 |
+
@property
|
| 115 |
+
def is_authenticated(self) -> bool:
|
| 116 |
+
raise NotImplementedError() # pragma: no cover
|
| 117 |
+
|
| 118 |
+
@property
|
| 119 |
+
def display_name(self) -> str:
|
| 120 |
+
raise NotImplementedError() # pragma: no cover
|
| 121 |
+
|
| 122 |
+
@property
|
| 123 |
+
def identity(self) -> str:
|
| 124 |
+
raise NotImplementedError() # pragma: no cover
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
class SimpleUser(BaseUser):
|
| 128 |
+
def __init__(self, username: str) -> None:
|
| 129 |
+
self.username = username
|
| 130 |
+
|
| 131 |
+
@property
|
| 132 |
+
def is_authenticated(self) -> bool:
|
| 133 |
+
return True
|
| 134 |
+
|
| 135 |
+
@property
|
| 136 |
+
def display_name(self) -> str:
|
| 137 |
+
return self.username
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
class UnauthenticatedUser(BaseUser):
|
| 141 |
+
@property
|
| 142 |
+
def is_authenticated(self) -> bool:
|
| 143 |
+
return False
|
| 144 |
+
|
| 145 |
+
@property
|
| 146 |
+
def display_name(self) -> str:
|
| 147 |
+
return ""
|
.venv/lib/python3.11/site-packages/starlette/background.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import sys
|
| 4 |
+
import typing
|
| 5 |
+
|
| 6 |
+
if sys.version_info >= (3, 10): # pragma: no cover
|
| 7 |
+
from typing import ParamSpec
|
| 8 |
+
else: # pragma: no cover
|
| 9 |
+
from typing_extensions import ParamSpec
|
| 10 |
+
|
| 11 |
+
from starlette._utils import is_async_callable
|
| 12 |
+
from starlette.concurrency import run_in_threadpool
|
| 13 |
+
|
| 14 |
+
P = ParamSpec("P")
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class BackgroundTask:
|
| 18 |
+
def __init__(self, func: typing.Callable[P, typing.Any], *args: P.args, **kwargs: P.kwargs) -> None:
|
| 19 |
+
self.func = func
|
| 20 |
+
self.args = args
|
| 21 |
+
self.kwargs = kwargs
|
| 22 |
+
self.is_async = is_async_callable(func)
|
| 23 |
+
|
| 24 |
+
async def __call__(self) -> None:
|
| 25 |
+
if self.is_async:
|
| 26 |
+
await self.func(*self.args, **self.kwargs)
|
| 27 |
+
else:
|
| 28 |
+
await run_in_threadpool(self.func, *self.args, **self.kwargs)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class BackgroundTasks(BackgroundTask):
|
| 32 |
+
def __init__(self, tasks: typing.Sequence[BackgroundTask] | None = None):
|
| 33 |
+
self.tasks = list(tasks) if tasks else []
|
| 34 |
+
|
| 35 |
+
def add_task(self, func: typing.Callable[P, typing.Any], *args: P.args, **kwargs: P.kwargs) -> None:
|
| 36 |
+
task = BackgroundTask(func, *args, **kwargs)
|
| 37 |
+
self.tasks.append(task)
|
| 38 |
+
|
| 39 |
+
async def __call__(self) -> None:
|
| 40 |
+
for task in self.tasks:
|
| 41 |
+
await task()
|
.venv/lib/python3.11/site-packages/starlette/endpoints.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import typing
|
| 5 |
+
|
| 6 |
+
from starlette import status
|
| 7 |
+
from starlette._utils import is_async_callable
|
| 8 |
+
from starlette.concurrency import run_in_threadpool
|
| 9 |
+
from starlette.exceptions import HTTPException
|
| 10 |
+
from starlette.requests import Request
|
| 11 |
+
from starlette.responses import PlainTextResponse, Response
|
| 12 |
+
from starlette.types import Message, Receive, Scope, Send
|
| 13 |
+
from starlette.websockets import WebSocket
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class HTTPEndpoint:
|
| 17 |
+
def __init__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
| 18 |
+
assert scope["type"] == "http"
|
| 19 |
+
self.scope = scope
|
| 20 |
+
self.receive = receive
|
| 21 |
+
self.send = send
|
| 22 |
+
self._allowed_methods = [
|
| 23 |
+
method
|
| 24 |
+
for method in ("GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
|
| 25 |
+
if getattr(self, method.lower(), None) is not None
|
| 26 |
+
]
|
| 27 |
+
|
| 28 |
+
def __await__(self) -> typing.Generator[typing.Any, None, None]:
|
| 29 |
+
return self.dispatch().__await__()
|
| 30 |
+
|
| 31 |
+
async def dispatch(self) -> None:
|
| 32 |
+
request = Request(self.scope, receive=self.receive)
|
| 33 |
+
handler_name = "get" if request.method == "HEAD" and not hasattr(self, "head") else request.method.lower()
|
| 34 |
+
|
| 35 |
+
handler: typing.Callable[[Request], typing.Any] = getattr(self, handler_name, self.method_not_allowed)
|
| 36 |
+
is_async = is_async_callable(handler)
|
| 37 |
+
if is_async:
|
| 38 |
+
response = await handler(request)
|
| 39 |
+
else:
|
| 40 |
+
response = await run_in_threadpool(handler, request)
|
| 41 |
+
await response(self.scope, self.receive, self.send)
|
| 42 |
+
|
| 43 |
+
async def method_not_allowed(self, request: Request) -> Response:
|
| 44 |
+
# If we're running inside a starlette application then raise an
|
| 45 |
+
# exception, so that the configurable exception handler can deal with
|
| 46 |
+
# returning the response. For plain ASGI apps, just return the response.
|
| 47 |
+
headers = {"Allow": ", ".join(self._allowed_methods)}
|
| 48 |
+
if "app" in self.scope:
|
| 49 |
+
raise HTTPException(status_code=405, headers=headers)
|
| 50 |
+
return PlainTextResponse("Method Not Allowed", status_code=405, headers=headers)
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
class WebSocketEndpoint:
|
| 54 |
+
encoding: str | None = None # May be "text", "bytes", or "json".
|
| 55 |
+
|
| 56 |
+
def __init__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
| 57 |
+
assert scope["type"] == "websocket"
|
| 58 |
+
self.scope = scope
|
| 59 |
+
self.receive = receive
|
| 60 |
+
self.send = send
|
| 61 |
+
|
| 62 |
+
def __await__(self) -> typing.Generator[typing.Any, None, None]:
|
| 63 |
+
return self.dispatch().__await__()
|
| 64 |
+
|
| 65 |
+
async def dispatch(self) -> None:
|
| 66 |
+
websocket = WebSocket(self.scope, receive=self.receive, send=self.send)
|
| 67 |
+
await self.on_connect(websocket)
|
| 68 |
+
|
| 69 |
+
close_code = status.WS_1000_NORMAL_CLOSURE
|
| 70 |
+
|
| 71 |
+
try:
|
| 72 |
+
while True:
|
| 73 |
+
message = await websocket.receive()
|
| 74 |
+
if message["type"] == "websocket.receive":
|
| 75 |
+
data = await self.decode(websocket, message)
|
| 76 |
+
await self.on_receive(websocket, data)
|
| 77 |
+
elif message["type"] == "websocket.disconnect": # pragma: no branch
|
| 78 |
+
close_code = int(message.get("code") or status.WS_1000_NORMAL_CLOSURE)
|
| 79 |
+
break
|
| 80 |
+
except Exception as exc:
|
| 81 |
+
close_code = status.WS_1011_INTERNAL_ERROR
|
| 82 |
+
raise exc
|
| 83 |
+
finally:
|
| 84 |
+
await self.on_disconnect(websocket, close_code)
|
| 85 |
+
|
| 86 |
+
async def decode(self, websocket: WebSocket, message: Message) -> typing.Any:
|
| 87 |
+
if self.encoding == "text":
|
| 88 |
+
if "text" not in message:
|
| 89 |
+
await websocket.close(code=status.WS_1003_UNSUPPORTED_DATA)
|
| 90 |
+
raise RuntimeError("Expected text websocket messages, but got bytes")
|
| 91 |
+
return message["text"]
|
| 92 |
+
|
| 93 |
+
elif self.encoding == "bytes":
|
| 94 |
+
if "bytes" not in message:
|
| 95 |
+
await websocket.close(code=status.WS_1003_UNSUPPORTED_DATA)
|
| 96 |
+
raise RuntimeError("Expected bytes websocket messages, but got text")
|
| 97 |
+
return message["bytes"]
|
| 98 |
+
|
| 99 |
+
elif self.encoding == "json":
|
| 100 |
+
if message.get("text") is not None:
|
| 101 |
+
text = message["text"]
|
| 102 |
+
else:
|
| 103 |
+
text = message["bytes"].decode("utf-8")
|
| 104 |
+
|
| 105 |
+
try:
|
| 106 |
+
return json.loads(text)
|
| 107 |
+
except json.decoder.JSONDecodeError:
|
| 108 |
+
await websocket.close(code=status.WS_1003_UNSUPPORTED_DATA)
|
| 109 |
+
raise RuntimeError("Malformed JSON data received.")
|
| 110 |
+
|
| 111 |
+
assert self.encoding is None, f"Unsupported 'encoding' attribute {self.encoding}"
|
| 112 |
+
return message["text"] if message.get("text") else message["bytes"]
|
| 113 |
+
|
| 114 |
+
async def on_connect(self, websocket: WebSocket) -> None:
|
| 115 |
+
"""Override to handle an incoming websocket connection"""
|
| 116 |
+
await websocket.accept()
|
| 117 |
+
|
| 118 |
+
async def on_receive(self, websocket: WebSocket, data: typing.Any) -> None:
|
| 119 |
+
"""Override to handle an incoming websocket message"""
|
| 120 |
+
|
| 121 |
+
async def on_disconnect(self, websocket: WebSocket, close_code: int) -> None:
|
| 122 |
+
"""Override to handle a disconnecting websocket"""
|
.venv/lib/python3.11/site-packages/starlette/formparsers.py
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import typing
|
| 4 |
+
from dataclasses import dataclass, field
|
| 5 |
+
from enum import Enum
|
| 6 |
+
from tempfile import SpooledTemporaryFile
|
| 7 |
+
from urllib.parse import unquote_plus
|
| 8 |
+
|
| 9 |
+
from starlette.datastructures import FormData, Headers, UploadFile
|
| 10 |
+
|
| 11 |
+
if typing.TYPE_CHECKING:
|
| 12 |
+
import python_multipart as multipart
|
| 13 |
+
from python_multipart.multipart import MultipartCallbacks, QuerystringCallbacks, parse_options_header
|
| 14 |
+
else:
|
| 15 |
+
try:
|
| 16 |
+
try:
|
| 17 |
+
import python_multipart as multipart
|
| 18 |
+
from python_multipart.multipart import parse_options_header
|
| 19 |
+
except ModuleNotFoundError: # pragma: no cover
|
| 20 |
+
import multipart
|
| 21 |
+
from multipart.multipart import parse_options_header
|
| 22 |
+
except ModuleNotFoundError: # pragma: no cover
|
| 23 |
+
multipart = None
|
| 24 |
+
parse_options_header = None
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class FormMessage(Enum):
|
| 28 |
+
FIELD_START = 1
|
| 29 |
+
FIELD_NAME = 2
|
| 30 |
+
FIELD_DATA = 3
|
| 31 |
+
FIELD_END = 4
|
| 32 |
+
END = 5
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
@dataclass
|
| 36 |
+
class MultipartPart:
|
| 37 |
+
content_disposition: bytes | None = None
|
| 38 |
+
field_name: str = ""
|
| 39 |
+
data: bytearray = field(default_factory=bytearray)
|
| 40 |
+
file: UploadFile | None = None
|
| 41 |
+
item_headers: list[tuple[bytes, bytes]] = field(default_factory=list)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def _user_safe_decode(src: bytes | bytearray, codec: str) -> str:
|
| 45 |
+
try:
|
| 46 |
+
return src.decode(codec)
|
| 47 |
+
except (UnicodeDecodeError, LookupError):
|
| 48 |
+
return src.decode("latin-1")
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
class MultiPartException(Exception):
|
| 52 |
+
def __init__(self, message: str) -> None:
|
| 53 |
+
self.message = message
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
class FormParser:
|
| 57 |
+
def __init__(self, headers: Headers, stream: typing.AsyncGenerator[bytes, None]) -> None:
|
| 58 |
+
assert multipart is not None, "The `python-multipart` library must be installed to use form parsing."
|
| 59 |
+
self.headers = headers
|
| 60 |
+
self.stream = stream
|
| 61 |
+
self.messages: list[tuple[FormMessage, bytes]] = []
|
| 62 |
+
|
| 63 |
+
def on_field_start(self) -> None:
|
| 64 |
+
message = (FormMessage.FIELD_START, b"")
|
| 65 |
+
self.messages.append(message)
|
| 66 |
+
|
| 67 |
+
def on_field_name(self, data: bytes, start: int, end: int) -> None:
|
| 68 |
+
message = (FormMessage.FIELD_NAME, data[start:end])
|
| 69 |
+
self.messages.append(message)
|
| 70 |
+
|
| 71 |
+
def on_field_data(self, data: bytes, start: int, end: int) -> None:
|
| 72 |
+
message = (FormMessage.FIELD_DATA, data[start:end])
|
| 73 |
+
self.messages.append(message)
|
| 74 |
+
|
| 75 |
+
def on_field_end(self) -> None:
|
| 76 |
+
message = (FormMessage.FIELD_END, b"")
|
| 77 |
+
self.messages.append(message)
|
| 78 |
+
|
| 79 |
+
def on_end(self) -> None:
|
| 80 |
+
message = (FormMessage.END, b"")
|
| 81 |
+
self.messages.append(message)
|
| 82 |
+
|
| 83 |
+
async def parse(self) -> FormData:
|
| 84 |
+
# Callbacks dictionary.
|
| 85 |
+
callbacks: QuerystringCallbacks = {
|
| 86 |
+
"on_field_start": self.on_field_start,
|
| 87 |
+
"on_field_name": self.on_field_name,
|
| 88 |
+
"on_field_data": self.on_field_data,
|
| 89 |
+
"on_field_end": self.on_field_end,
|
| 90 |
+
"on_end": self.on_end,
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
# Create the parser.
|
| 94 |
+
parser = multipart.QuerystringParser(callbacks)
|
| 95 |
+
field_name = b""
|
| 96 |
+
field_value = b""
|
| 97 |
+
|
| 98 |
+
items: list[tuple[str, str | UploadFile]] = []
|
| 99 |
+
|
| 100 |
+
# Feed the parser with data from the request.
|
| 101 |
+
async for chunk in self.stream:
|
| 102 |
+
if chunk:
|
| 103 |
+
parser.write(chunk)
|
| 104 |
+
else:
|
| 105 |
+
parser.finalize()
|
| 106 |
+
messages = list(self.messages)
|
| 107 |
+
self.messages.clear()
|
| 108 |
+
for message_type, message_bytes in messages:
|
| 109 |
+
if message_type == FormMessage.FIELD_START:
|
| 110 |
+
field_name = b""
|
| 111 |
+
field_value = b""
|
| 112 |
+
elif message_type == FormMessage.FIELD_NAME:
|
| 113 |
+
field_name += message_bytes
|
| 114 |
+
elif message_type == FormMessage.FIELD_DATA:
|
| 115 |
+
field_value += message_bytes
|
| 116 |
+
elif message_type == FormMessage.FIELD_END:
|
| 117 |
+
name = unquote_plus(field_name.decode("latin-1"))
|
| 118 |
+
value = unquote_plus(field_value.decode("latin-1"))
|
| 119 |
+
items.append((name, value))
|
| 120 |
+
|
| 121 |
+
return FormData(items)
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
class MultiPartParser:
|
| 125 |
+
max_file_size = 1024 * 1024 # 1MB
|
| 126 |
+
|
| 127 |
+
def __init__(
|
| 128 |
+
self,
|
| 129 |
+
headers: Headers,
|
| 130 |
+
stream: typing.AsyncGenerator[bytes, None],
|
| 131 |
+
*,
|
| 132 |
+
max_files: int | float = 1000,
|
| 133 |
+
max_fields: int | float = 1000,
|
| 134 |
+
max_part_size: int = 1024 * 1024, # 1MB
|
| 135 |
+
) -> None:
|
| 136 |
+
assert multipart is not None, "The `python-multipart` library must be installed to use form parsing."
|
| 137 |
+
self.headers = headers
|
| 138 |
+
self.stream = stream
|
| 139 |
+
self.max_files = max_files
|
| 140 |
+
self.max_fields = max_fields
|
| 141 |
+
self.items: list[tuple[str, str | UploadFile]] = []
|
| 142 |
+
self._current_files = 0
|
| 143 |
+
self._current_fields = 0
|
| 144 |
+
self._current_partial_header_name: bytes = b""
|
| 145 |
+
self._current_partial_header_value: bytes = b""
|
| 146 |
+
self._current_part = MultipartPart()
|
| 147 |
+
self._charset = ""
|
| 148 |
+
self._file_parts_to_write: list[tuple[MultipartPart, bytes]] = []
|
| 149 |
+
self._file_parts_to_finish: list[MultipartPart] = []
|
| 150 |
+
self._files_to_close_on_error: list[SpooledTemporaryFile[bytes]] = []
|
| 151 |
+
self.max_part_size = max_part_size
|
| 152 |
+
|
| 153 |
+
def on_part_begin(self) -> None:
|
| 154 |
+
self._current_part = MultipartPart()
|
| 155 |
+
|
| 156 |
+
def on_part_data(self, data: bytes, start: int, end: int) -> None:
|
| 157 |
+
message_bytes = data[start:end]
|
| 158 |
+
if self._current_part.file is None:
|
| 159 |
+
if len(self._current_part.data) + len(message_bytes) > self.max_part_size:
|
| 160 |
+
raise MultiPartException(f"Part exceeded maximum size of {int(self.max_part_size / 1024)}KB.")
|
| 161 |
+
self._current_part.data.extend(message_bytes)
|
| 162 |
+
else:
|
| 163 |
+
self._file_parts_to_write.append((self._current_part, message_bytes))
|
| 164 |
+
|
| 165 |
+
def on_part_end(self) -> None:
|
| 166 |
+
if self._current_part.file is None:
|
| 167 |
+
self.items.append(
|
| 168 |
+
(
|
| 169 |
+
self._current_part.field_name,
|
| 170 |
+
_user_safe_decode(self._current_part.data, self._charset),
|
| 171 |
+
)
|
| 172 |
+
)
|
| 173 |
+
else:
|
| 174 |
+
self._file_parts_to_finish.append(self._current_part)
|
| 175 |
+
# The file can be added to the items right now even though it's not
|
| 176 |
+
# finished yet, because it will be finished in the `parse()` method, before
|
| 177 |
+
# self.items is used in the return value.
|
| 178 |
+
self.items.append((self._current_part.field_name, self._current_part.file))
|
| 179 |
+
|
| 180 |
+
def on_header_field(self, data: bytes, start: int, end: int) -> None:
|
| 181 |
+
self._current_partial_header_name += data[start:end]
|
| 182 |
+
|
| 183 |
+
def on_header_value(self, data: bytes, start: int, end: int) -> None:
|
| 184 |
+
self._current_partial_header_value += data[start:end]
|
| 185 |
+
|
| 186 |
+
def on_header_end(self) -> None:
|
| 187 |
+
field = self._current_partial_header_name.lower()
|
| 188 |
+
if field == b"content-disposition":
|
| 189 |
+
self._current_part.content_disposition = self._current_partial_header_value
|
| 190 |
+
self._current_part.item_headers.append((field, self._current_partial_header_value))
|
| 191 |
+
self._current_partial_header_name = b""
|
| 192 |
+
self._current_partial_header_value = b""
|
| 193 |
+
|
| 194 |
+
def on_headers_finished(self) -> None:
|
| 195 |
+
disposition, options = parse_options_header(self._current_part.content_disposition)
|
| 196 |
+
try:
|
| 197 |
+
self._current_part.field_name = _user_safe_decode(options[b"name"], self._charset)
|
| 198 |
+
except KeyError:
|
| 199 |
+
raise MultiPartException('The Content-Disposition header field "name" must be provided.')
|
| 200 |
+
if b"filename" in options:
|
| 201 |
+
self._current_files += 1
|
| 202 |
+
if self._current_files > self.max_files:
|
| 203 |
+
raise MultiPartException(f"Too many files. Maximum number of files is {self.max_files}.")
|
| 204 |
+
filename = _user_safe_decode(options[b"filename"], self._charset)
|
| 205 |
+
tempfile = SpooledTemporaryFile(max_size=self.max_file_size)
|
| 206 |
+
self._files_to_close_on_error.append(tempfile)
|
| 207 |
+
self._current_part.file = UploadFile(
|
| 208 |
+
file=tempfile, # type: ignore[arg-type]
|
| 209 |
+
size=0,
|
| 210 |
+
filename=filename,
|
| 211 |
+
headers=Headers(raw=self._current_part.item_headers),
|
| 212 |
+
)
|
| 213 |
+
else:
|
| 214 |
+
self._current_fields += 1
|
| 215 |
+
if self._current_fields > self.max_fields:
|
| 216 |
+
raise MultiPartException(f"Too many fields. Maximum number of fields is {self.max_fields}.")
|
| 217 |
+
self._current_part.file = None
|
| 218 |
+
|
| 219 |
+
def on_end(self) -> None:
|
| 220 |
+
pass
|
| 221 |
+
|
| 222 |
+
async def parse(self) -> FormData:
|
| 223 |
+
# Parse the Content-Type header to get the multipart boundary.
|
| 224 |
+
_, params = parse_options_header(self.headers["Content-Type"])
|
| 225 |
+
charset = params.get(b"charset", "utf-8")
|
| 226 |
+
if isinstance(charset, bytes):
|
| 227 |
+
charset = charset.decode("latin-1")
|
| 228 |
+
self._charset = charset
|
| 229 |
+
try:
|
| 230 |
+
boundary = params[b"boundary"]
|
| 231 |
+
except KeyError:
|
| 232 |
+
raise MultiPartException("Missing boundary in multipart.")
|
| 233 |
+
|
| 234 |
+
# Callbacks dictionary.
|
| 235 |
+
callbacks: MultipartCallbacks = {
|
| 236 |
+
"on_part_begin": self.on_part_begin,
|
| 237 |
+
"on_part_data": self.on_part_data,
|
| 238 |
+
"on_part_end": self.on_part_end,
|
| 239 |
+
"on_header_field": self.on_header_field,
|
| 240 |
+
"on_header_value": self.on_header_value,
|
| 241 |
+
"on_header_end": self.on_header_end,
|
| 242 |
+
"on_headers_finished": self.on_headers_finished,
|
| 243 |
+
"on_end": self.on_end,
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
# Create the parser.
|
| 247 |
+
parser = multipart.MultipartParser(boundary, callbacks)
|
| 248 |
+
try:
|
| 249 |
+
# Feed the parser with data from the request.
|
| 250 |
+
async for chunk in self.stream:
|
| 251 |
+
parser.write(chunk)
|
| 252 |
+
# Write file data, it needs to use await with the UploadFile methods
|
| 253 |
+
# that call the corresponding file methods *in a threadpool*,
|
| 254 |
+
# otherwise, if they were called directly in the callback methods above
|
| 255 |
+
# (regular, non-async functions), that would block the event loop in
|
| 256 |
+
# the main thread.
|
| 257 |
+
for part, data in self._file_parts_to_write:
|
| 258 |
+
assert part.file # for type checkers
|
| 259 |
+
await part.file.write(data)
|
| 260 |
+
for part in self._file_parts_to_finish:
|
| 261 |
+
assert part.file # for type checkers
|
| 262 |
+
await part.file.seek(0)
|
| 263 |
+
self._file_parts_to_write.clear()
|
| 264 |
+
self._file_parts_to_finish.clear()
|
| 265 |
+
except MultiPartException as exc:
|
| 266 |
+
# Close all the files if there was an error.
|
| 267 |
+
for file in self._files_to_close_on_error:
|
| 268 |
+
file.close()
|
| 269 |
+
raise exc
|
| 270 |
+
|
| 271 |
+
parser.finalize()
|
| 272 |
+
return FormData(self.items)
|
.venv/lib/python3.11/site-packages/starlette/middleware/__init__.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import sys
|
| 4 |
+
from collections.abc import Iterator
|
| 5 |
+
from typing import Any, Protocol
|
| 6 |
+
|
| 7 |
+
if sys.version_info >= (3, 10): # pragma: no cover
|
| 8 |
+
from typing import ParamSpec
|
| 9 |
+
else: # pragma: no cover
|
| 10 |
+
from typing_extensions import ParamSpec
|
| 11 |
+
|
| 12 |
+
from starlette.types import ASGIApp
|
| 13 |
+
|
| 14 |
+
P = ParamSpec("P")
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class _MiddlewareFactory(Protocol[P]):
|
| 18 |
+
def __call__(self, app: ASGIApp, /, *args: P.args, **kwargs: P.kwargs) -> ASGIApp: ... # pragma: no cover
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class Middleware:
|
| 22 |
+
def __init__(
|
| 23 |
+
self,
|
| 24 |
+
cls: _MiddlewareFactory[P],
|
| 25 |
+
*args: P.args,
|
| 26 |
+
**kwargs: P.kwargs,
|
| 27 |
+
) -> None:
|
| 28 |
+
self.cls = cls
|
| 29 |
+
self.args = args
|
| 30 |
+
self.kwargs = kwargs
|
| 31 |
+
|
| 32 |
+
def __iter__(self) -> Iterator[Any]:
|
| 33 |
+
as_tuple = (self.cls, self.args, self.kwargs)
|
| 34 |
+
return iter(as_tuple)
|
| 35 |
+
|
| 36 |
+
def __repr__(self) -> str:
|
| 37 |
+
class_name = self.__class__.__name__
|
| 38 |
+
args_strings = [f"{value!r}" for value in self.args]
|
| 39 |
+
option_strings = [f"{key}={value!r}" for key, value in self.kwargs.items()]
|
| 40 |
+
name = getattr(self.cls, "__name__", "")
|
| 41 |
+
args_repr = ", ".join([name] + args_strings + option_strings)
|
| 42 |
+
return f"{class_name}({args_repr})"
|
.venv/lib/python3.11/site-packages/starlette/middleware/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (3.08 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/starlette/middleware/__pycache__/authentication.cpython-311.pyc
ADDED
|
Binary file (3.25 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/starlette/middleware/__pycache__/base.cpython-311.pyc
ADDED
|
Binary file (12.7 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/starlette/middleware/__pycache__/cors.cpython-311.pyc
ADDED
|
Binary file (8.45 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/starlette/middleware/__pycache__/errors.cpython-311.pyc
ADDED
|
Binary file (10.7 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/starlette/middleware/__pycache__/exceptions.cpython-311.pyc
ADDED
|
Binary file (4.47 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/starlette/middleware/__pycache__/gzip.cpython-311.pyc
ADDED
|
Binary file (7.38 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/starlette/middleware/__pycache__/httpsredirect.cpython-311.pyc
ADDED
|
Binary file (1.95 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/starlette/middleware/__pycache__/sessions.cpython-311.pyc
ADDED
|
Binary file (5.03 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/starlette/middleware/__pycache__/trustedhost.cpython-311.pyc
ADDED
|
Binary file (3.54 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/starlette/middleware/__pycache__/wsgi.cpython-311.pyc
ADDED
|
Binary file (9.69 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/starlette/middleware/authentication.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import typing
|
| 4 |
+
|
| 5 |
+
from starlette.authentication import (
|
| 6 |
+
AuthCredentials,
|
| 7 |
+
AuthenticationBackend,
|
| 8 |
+
AuthenticationError,
|
| 9 |
+
UnauthenticatedUser,
|
| 10 |
+
)
|
| 11 |
+
from starlette.requests import HTTPConnection
|
| 12 |
+
from starlette.responses import PlainTextResponse, Response
|
| 13 |
+
from starlette.types import ASGIApp, Receive, Scope, Send
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class AuthenticationMiddleware:
|
| 17 |
+
def __init__(
|
| 18 |
+
self,
|
| 19 |
+
app: ASGIApp,
|
| 20 |
+
backend: AuthenticationBackend,
|
| 21 |
+
on_error: typing.Callable[[HTTPConnection, AuthenticationError], Response] | None = None,
|
| 22 |
+
) -> None:
|
| 23 |
+
self.app = app
|
| 24 |
+
self.backend = backend
|
| 25 |
+
self.on_error: typing.Callable[[HTTPConnection, AuthenticationError], Response] = (
|
| 26 |
+
on_error if on_error is not None else self.default_on_error
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
| 30 |
+
if scope["type"] not in ["http", "websocket"]:
|
| 31 |
+
await self.app(scope, receive, send)
|
| 32 |
+
return
|
| 33 |
+
|
| 34 |
+
conn = HTTPConnection(scope)
|
| 35 |
+
try:
|
| 36 |
+
auth_result = await self.backend.authenticate(conn)
|
| 37 |
+
except AuthenticationError as exc:
|
| 38 |
+
response = self.on_error(conn, exc)
|
| 39 |
+
if scope["type"] == "websocket":
|
| 40 |
+
await send({"type": "websocket.close", "code": 1000})
|
| 41 |
+
else:
|
| 42 |
+
await response(scope, receive, send)
|
| 43 |
+
return
|
| 44 |
+
|
| 45 |
+
if auth_result is None:
|
| 46 |
+
auth_result = AuthCredentials(), UnauthenticatedUser()
|
| 47 |
+
scope["auth"], scope["user"] = auth_result
|
| 48 |
+
await self.app(scope, receive, send)
|
| 49 |
+
|
| 50 |
+
@staticmethod
|
| 51 |
+
def default_on_error(conn: HTTPConnection, exc: Exception) -> Response:
|
| 52 |
+
return PlainTextResponse(str(exc), status_code=400)
|
.venv/lib/python3.11/site-packages/starlette/middleware/base.py
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import typing
|
| 4 |
+
|
| 5 |
+
import anyio
|
| 6 |
+
|
| 7 |
+
from starlette._utils import collapse_excgroups
|
| 8 |
+
from starlette.requests import ClientDisconnect, Request
|
| 9 |
+
from starlette.responses import AsyncContentStream, Response
|
| 10 |
+
from starlette.types import ASGIApp, Message, Receive, Scope, Send
|
| 11 |
+
|
| 12 |
+
RequestResponseEndpoint = typing.Callable[[Request], typing.Awaitable[Response]]
|
| 13 |
+
DispatchFunction = typing.Callable[[Request, RequestResponseEndpoint], typing.Awaitable[Response]]
|
| 14 |
+
T = typing.TypeVar("T")
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class _CachedRequest(Request):
|
| 18 |
+
"""
|
| 19 |
+
If the user calls Request.body() from their dispatch function
|
| 20 |
+
we cache the entire request body in memory and pass that to downstream middlewares,
|
| 21 |
+
but if they call Request.stream() then all we do is send an
|
| 22 |
+
empty body so that downstream things don't hang forever.
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
def __init__(self, scope: Scope, receive: Receive):
|
| 26 |
+
super().__init__(scope, receive)
|
| 27 |
+
self._wrapped_rcv_disconnected = False
|
| 28 |
+
self._wrapped_rcv_consumed = False
|
| 29 |
+
self._wrapped_rc_stream = self.stream()
|
| 30 |
+
|
| 31 |
+
async def wrapped_receive(self) -> Message:
|
| 32 |
+
# wrapped_rcv state 1: disconnected
|
| 33 |
+
if self._wrapped_rcv_disconnected:
|
| 34 |
+
# we've already sent a disconnect to the downstream app
|
| 35 |
+
# we don't need to wait to get another one
|
| 36 |
+
# (although most ASGI servers will just keep sending it)
|
| 37 |
+
return {"type": "http.disconnect"}
|
| 38 |
+
# wrapped_rcv state 1: consumed but not yet disconnected
|
| 39 |
+
if self._wrapped_rcv_consumed:
|
| 40 |
+
# since the downstream app has consumed us all that is left
|
| 41 |
+
# is to send it a disconnect
|
| 42 |
+
if self._is_disconnected:
|
| 43 |
+
# the middleware has already seen the disconnect
|
| 44 |
+
# since we know the client is disconnected no need to wait
|
| 45 |
+
# for the message
|
| 46 |
+
self._wrapped_rcv_disconnected = True
|
| 47 |
+
return {"type": "http.disconnect"}
|
| 48 |
+
# we don't know yet if the client is disconnected or not
|
| 49 |
+
# so we'll wait until we get that message
|
| 50 |
+
msg = await self.receive()
|
| 51 |
+
if msg["type"] != "http.disconnect": # pragma: no cover
|
| 52 |
+
# at this point a disconnect is all that we should be receiving
|
| 53 |
+
# if we get something else, things went wrong somewhere
|
| 54 |
+
raise RuntimeError(f"Unexpected message received: {msg['type']}")
|
| 55 |
+
self._wrapped_rcv_disconnected = True
|
| 56 |
+
return msg
|
| 57 |
+
|
| 58 |
+
# wrapped_rcv state 3: not yet consumed
|
| 59 |
+
if getattr(self, "_body", None) is not None:
|
| 60 |
+
# body() was called, we return it even if the client disconnected
|
| 61 |
+
self._wrapped_rcv_consumed = True
|
| 62 |
+
return {
|
| 63 |
+
"type": "http.request",
|
| 64 |
+
"body": self._body,
|
| 65 |
+
"more_body": False,
|
| 66 |
+
}
|
| 67 |
+
elif self._stream_consumed:
|
| 68 |
+
# stream() was called to completion
|
| 69 |
+
# return an empty body so that downstream apps don't hang
|
| 70 |
+
# waiting for a disconnect
|
| 71 |
+
self._wrapped_rcv_consumed = True
|
| 72 |
+
return {
|
| 73 |
+
"type": "http.request",
|
| 74 |
+
"body": b"",
|
| 75 |
+
"more_body": False,
|
| 76 |
+
}
|
| 77 |
+
else:
|
| 78 |
+
# body() was never called and stream() wasn't consumed
|
| 79 |
+
try:
|
| 80 |
+
stream = self.stream()
|
| 81 |
+
chunk = await stream.__anext__()
|
| 82 |
+
self._wrapped_rcv_consumed = self._stream_consumed
|
| 83 |
+
return {
|
| 84 |
+
"type": "http.request",
|
| 85 |
+
"body": chunk,
|
| 86 |
+
"more_body": not self._stream_consumed,
|
| 87 |
+
}
|
| 88 |
+
except ClientDisconnect:
|
| 89 |
+
self._wrapped_rcv_disconnected = True
|
| 90 |
+
return {"type": "http.disconnect"}
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
class BaseHTTPMiddleware:
|
| 94 |
+
def __init__(self, app: ASGIApp, dispatch: DispatchFunction | None = None) -> None:
|
| 95 |
+
self.app = app
|
| 96 |
+
self.dispatch_func = self.dispatch if dispatch is None else dispatch
|
| 97 |
+
|
| 98 |
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
| 99 |
+
if scope["type"] != "http":
|
| 100 |
+
await self.app(scope, receive, send)
|
| 101 |
+
return
|
| 102 |
+
|
| 103 |
+
request = _CachedRequest(scope, receive)
|
| 104 |
+
wrapped_receive = request.wrapped_receive
|
| 105 |
+
response_sent = anyio.Event()
|
| 106 |
+
|
| 107 |
+
async def call_next(request: Request) -> Response:
|
| 108 |
+
app_exc: Exception | None = None
|
| 109 |
+
|
| 110 |
+
async def receive_or_disconnect() -> Message:
|
| 111 |
+
if response_sent.is_set():
|
| 112 |
+
return {"type": "http.disconnect"}
|
| 113 |
+
|
| 114 |
+
async with anyio.create_task_group() as task_group:
|
| 115 |
+
|
| 116 |
+
async def wrap(func: typing.Callable[[], typing.Awaitable[T]]) -> T:
|
| 117 |
+
result = await func()
|
| 118 |
+
task_group.cancel_scope.cancel()
|
| 119 |
+
return result
|
| 120 |
+
|
| 121 |
+
task_group.start_soon(wrap, response_sent.wait)
|
| 122 |
+
message = await wrap(wrapped_receive)
|
| 123 |
+
|
| 124 |
+
if response_sent.is_set():
|
| 125 |
+
return {"type": "http.disconnect"}
|
| 126 |
+
|
| 127 |
+
return message
|
| 128 |
+
|
| 129 |
+
async def send_no_error(message: Message) -> None:
|
| 130 |
+
try:
|
| 131 |
+
await send_stream.send(message)
|
| 132 |
+
except anyio.BrokenResourceError:
|
| 133 |
+
# recv_stream has been closed, i.e. response_sent has been set.
|
| 134 |
+
return
|
| 135 |
+
|
| 136 |
+
async def coro() -> None:
|
| 137 |
+
nonlocal app_exc
|
| 138 |
+
|
| 139 |
+
with send_stream:
|
| 140 |
+
try:
|
| 141 |
+
await self.app(scope, receive_or_disconnect, send_no_error)
|
| 142 |
+
except Exception as exc:
|
| 143 |
+
app_exc = exc
|
| 144 |
+
|
| 145 |
+
task_group.start_soon(coro)
|
| 146 |
+
|
| 147 |
+
try:
|
| 148 |
+
message = await recv_stream.receive()
|
| 149 |
+
info = message.get("info", None)
|
| 150 |
+
if message["type"] == "http.response.debug" and info is not None:
|
| 151 |
+
message = await recv_stream.receive()
|
| 152 |
+
except anyio.EndOfStream:
|
| 153 |
+
if app_exc is not None:
|
| 154 |
+
raise app_exc
|
| 155 |
+
raise RuntimeError("No response returned.")
|
| 156 |
+
|
| 157 |
+
assert message["type"] == "http.response.start"
|
| 158 |
+
|
| 159 |
+
async def body_stream() -> typing.AsyncGenerator[bytes, None]:
|
| 160 |
+
async for message in recv_stream:
|
| 161 |
+
assert message["type"] == "http.response.body"
|
| 162 |
+
body = message.get("body", b"")
|
| 163 |
+
if body:
|
| 164 |
+
yield body
|
| 165 |
+
if not message.get("more_body", False):
|
| 166 |
+
break
|
| 167 |
+
|
| 168 |
+
if app_exc is not None:
|
| 169 |
+
raise app_exc
|
| 170 |
+
|
| 171 |
+
response = _StreamingResponse(status_code=message["status"], content=body_stream(), info=info)
|
| 172 |
+
response.raw_headers = message["headers"]
|
| 173 |
+
return response
|
| 174 |
+
|
| 175 |
+
streams: anyio.create_memory_object_stream[Message] = anyio.create_memory_object_stream()
|
| 176 |
+
send_stream, recv_stream = streams
|
| 177 |
+
with recv_stream, send_stream, collapse_excgroups():
|
| 178 |
+
async with anyio.create_task_group() as task_group:
|
| 179 |
+
response = await self.dispatch_func(request, call_next)
|
| 180 |
+
await response(scope, wrapped_receive, send)
|
| 181 |
+
response_sent.set()
|
| 182 |
+
recv_stream.close()
|
| 183 |
+
|
| 184 |
+
async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
|
| 185 |
+
raise NotImplementedError() # pragma: no cover
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
class _StreamingResponse(Response):
|
| 189 |
+
def __init__(
|
| 190 |
+
self,
|
| 191 |
+
content: AsyncContentStream,
|
| 192 |
+
status_code: int = 200,
|
| 193 |
+
headers: typing.Mapping[str, str] | None = None,
|
| 194 |
+
media_type: str | None = None,
|
| 195 |
+
info: typing.Mapping[str, typing.Any] | None = None,
|
| 196 |
+
) -> None:
|
| 197 |
+
self.info = info
|
| 198 |
+
self.body_iterator = content
|
| 199 |
+
self.status_code = status_code
|
| 200 |
+
self.media_type = media_type
|
| 201 |
+
self.init_headers(headers)
|
| 202 |
+
self.background = None
|
| 203 |
+
|
| 204 |
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
| 205 |
+
if self.info is not None:
|
| 206 |
+
await send({"type": "http.response.debug", "info": self.info})
|
| 207 |
+
await send(
|
| 208 |
+
{
|
| 209 |
+
"type": "http.response.start",
|
| 210 |
+
"status": self.status_code,
|
| 211 |
+
"headers": self.raw_headers,
|
| 212 |
+
}
|
| 213 |
+
)
|
| 214 |
+
|
| 215 |
+
async for chunk in self.body_iterator:
|
| 216 |
+
await send({"type": "http.response.body", "body": chunk, "more_body": True})
|
| 217 |
+
|
| 218 |
+
await send({"type": "http.response.body", "body": b"", "more_body": False})
|
| 219 |
+
|
| 220 |
+
if self.background:
|
| 221 |
+
await self.background()
|
.venv/lib/python3.11/site-packages/starlette/middleware/cors.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import functools
|
| 4 |
+
import re
|
| 5 |
+
import typing
|
| 6 |
+
|
| 7 |
+
from starlette.datastructures import Headers, MutableHeaders
|
| 8 |
+
from starlette.responses import PlainTextResponse, Response
|
| 9 |
+
from starlette.types import ASGIApp, Message, Receive, Scope, Send
|
| 10 |
+
|
| 11 |
+
ALL_METHODS = ("DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT")
|
| 12 |
+
SAFELISTED_HEADERS = {"Accept", "Accept-Language", "Content-Language", "Content-Type"}
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class CORSMiddleware:
|
| 16 |
+
def __init__(
|
| 17 |
+
self,
|
| 18 |
+
app: ASGIApp,
|
| 19 |
+
allow_origins: typing.Sequence[str] = (),
|
| 20 |
+
allow_methods: typing.Sequence[str] = ("GET",),
|
| 21 |
+
allow_headers: typing.Sequence[str] = (),
|
| 22 |
+
allow_credentials: bool = False,
|
| 23 |
+
allow_origin_regex: str | None = None,
|
| 24 |
+
expose_headers: typing.Sequence[str] = (),
|
| 25 |
+
max_age: int = 600,
|
| 26 |
+
) -> None:
|
| 27 |
+
if "*" in allow_methods:
|
| 28 |
+
allow_methods = ALL_METHODS
|
| 29 |
+
|
| 30 |
+
compiled_allow_origin_regex = None
|
| 31 |
+
if allow_origin_regex is not None:
|
| 32 |
+
compiled_allow_origin_regex = re.compile(allow_origin_regex)
|
| 33 |
+
|
| 34 |
+
allow_all_origins = "*" in allow_origins
|
| 35 |
+
allow_all_headers = "*" in allow_headers
|
| 36 |
+
preflight_explicit_allow_origin = not allow_all_origins or allow_credentials
|
| 37 |
+
|
| 38 |
+
simple_headers = {}
|
| 39 |
+
if allow_all_origins:
|
| 40 |
+
simple_headers["Access-Control-Allow-Origin"] = "*"
|
| 41 |
+
if allow_credentials:
|
| 42 |
+
simple_headers["Access-Control-Allow-Credentials"] = "true"
|
| 43 |
+
if expose_headers:
|
| 44 |
+
simple_headers["Access-Control-Expose-Headers"] = ", ".join(expose_headers)
|
| 45 |
+
|
| 46 |
+
preflight_headers = {}
|
| 47 |
+
if preflight_explicit_allow_origin:
|
| 48 |
+
# The origin value will be set in preflight_response() if it is allowed.
|
| 49 |
+
preflight_headers["Vary"] = "Origin"
|
| 50 |
+
else:
|
| 51 |
+
preflight_headers["Access-Control-Allow-Origin"] = "*"
|
| 52 |
+
preflight_headers.update(
|
| 53 |
+
{
|
| 54 |
+
"Access-Control-Allow-Methods": ", ".join(allow_methods),
|
| 55 |
+
"Access-Control-Max-Age": str(max_age),
|
| 56 |
+
}
|
| 57 |
+
)
|
| 58 |
+
allow_headers = sorted(SAFELISTED_HEADERS | set(allow_headers))
|
| 59 |
+
if allow_headers and not allow_all_headers:
|
| 60 |
+
preflight_headers["Access-Control-Allow-Headers"] = ", ".join(allow_headers)
|
| 61 |
+
if allow_credentials:
|
| 62 |
+
preflight_headers["Access-Control-Allow-Credentials"] = "true"
|
| 63 |
+
|
| 64 |
+
self.app = app
|
| 65 |
+
self.allow_origins = allow_origins
|
| 66 |
+
self.allow_methods = allow_methods
|
| 67 |
+
self.allow_headers = [h.lower() for h in allow_headers]
|
| 68 |
+
self.allow_all_origins = allow_all_origins
|
| 69 |
+
self.allow_all_headers = allow_all_headers
|
| 70 |
+
self.preflight_explicit_allow_origin = preflight_explicit_allow_origin
|
| 71 |
+
self.allow_origin_regex = compiled_allow_origin_regex
|
| 72 |
+
self.simple_headers = simple_headers
|
| 73 |
+
self.preflight_headers = preflight_headers
|
| 74 |
+
|
| 75 |
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
| 76 |
+
if scope["type"] != "http": # pragma: no cover
|
| 77 |
+
await self.app(scope, receive, send)
|
| 78 |
+
return
|
| 79 |
+
|
| 80 |
+
method = scope["method"]
|
| 81 |
+
headers = Headers(scope=scope)
|
| 82 |
+
origin = headers.get("origin")
|
| 83 |
+
|
| 84 |
+
if origin is None:
|
| 85 |
+
await self.app(scope, receive, send)
|
| 86 |
+
return
|
| 87 |
+
|
| 88 |
+
if method == "OPTIONS" and "access-control-request-method" in headers:
|
| 89 |
+
response = self.preflight_response(request_headers=headers)
|
| 90 |
+
await response(scope, receive, send)
|
| 91 |
+
return
|
| 92 |
+
|
| 93 |
+
await self.simple_response(scope, receive, send, request_headers=headers)
|
| 94 |
+
|
| 95 |
+
def is_allowed_origin(self, origin: str) -> bool:
|
| 96 |
+
if self.allow_all_origins:
|
| 97 |
+
return True
|
| 98 |
+
|
| 99 |
+
if self.allow_origin_regex is not None and self.allow_origin_regex.fullmatch(origin):
|
| 100 |
+
return True
|
| 101 |
+
|
| 102 |
+
return origin in self.allow_origins
|
| 103 |
+
|
| 104 |
+
def preflight_response(self, request_headers: Headers) -> Response:
|
| 105 |
+
requested_origin = request_headers["origin"]
|
| 106 |
+
requested_method = request_headers["access-control-request-method"]
|
| 107 |
+
requested_headers = request_headers.get("access-control-request-headers")
|
| 108 |
+
|
| 109 |
+
headers = dict(self.preflight_headers)
|
| 110 |
+
failures = []
|
| 111 |
+
|
| 112 |
+
if self.is_allowed_origin(origin=requested_origin):
|
| 113 |
+
if self.preflight_explicit_allow_origin:
|
| 114 |
+
# The "else" case is already accounted for in self.preflight_headers
|
| 115 |
+
# and the value would be "*".
|
| 116 |
+
headers["Access-Control-Allow-Origin"] = requested_origin
|
| 117 |
+
else:
|
| 118 |
+
failures.append("origin")
|
| 119 |
+
|
| 120 |
+
if requested_method not in self.allow_methods:
|
| 121 |
+
failures.append("method")
|
| 122 |
+
|
| 123 |
+
# If we allow all headers, then we have to mirror back any requested
|
| 124 |
+
# headers in the response.
|
| 125 |
+
if self.allow_all_headers and requested_headers is not None:
|
| 126 |
+
headers["Access-Control-Allow-Headers"] = requested_headers
|
| 127 |
+
elif requested_headers is not None:
|
| 128 |
+
for header in [h.lower() for h in requested_headers.split(",")]:
|
| 129 |
+
if header.strip() not in self.allow_headers:
|
| 130 |
+
failures.append("headers")
|
| 131 |
+
break
|
| 132 |
+
|
| 133 |
+
# We don't strictly need to use 400 responses here, since its up to
|
| 134 |
+
# the browser to enforce the CORS policy, but its more informative
|
| 135 |
+
# if we do.
|
| 136 |
+
if failures:
|
| 137 |
+
failure_text = "Disallowed CORS " + ", ".join(failures)
|
| 138 |
+
return PlainTextResponse(failure_text, status_code=400, headers=headers)
|
| 139 |
+
|
| 140 |
+
return PlainTextResponse("OK", status_code=200, headers=headers)
|
| 141 |
+
|
| 142 |
+
async def simple_response(self, scope: Scope, receive: Receive, send: Send, request_headers: Headers) -> None:
|
| 143 |
+
send = functools.partial(self.send, send=send, request_headers=request_headers)
|
| 144 |
+
await self.app(scope, receive, send)
|
| 145 |
+
|
| 146 |
+
async def send(self, message: Message, send: Send, request_headers: Headers) -> None:
|
| 147 |
+
if message["type"] != "http.response.start":
|
| 148 |
+
await send(message)
|
| 149 |
+
return
|
| 150 |
+
|
| 151 |
+
message.setdefault("headers", [])
|
| 152 |
+
headers = MutableHeaders(scope=message)
|
| 153 |
+
headers.update(self.simple_headers)
|
| 154 |
+
origin = request_headers["Origin"]
|
| 155 |
+
has_cookie = "cookie" in request_headers
|
| 156 |
+
|
| 157 |
+
# If request includes any cookie headers, then we must respond
|
| 158 |
+
# with the specific origin instead of '*'.
|
| 159 |
+
if self.allow_all_origins and has_cookie:
|
| 160 |
+
self.allow_explicit_origin(headers, origin)
|
| 161 |
+
|
| 162 |
+
# If we only allow specific origins, then we have to mirror back
|
| 163 |
+
# the Origin header in the response.
|
| 164 |
+
elif not self.allow_all_origins and self.is_allowed_origin(origin=origin):
|
| 165 |
+
self.allow_explicit_origin(headers, origin)
|
| 166 |
+
|
| 167 |
+
await send(message)
|
| 168 |
+
|
| 169 |
+
@staticmethod
|
| 170 |
+
def allow_explicit_origin(headers: MutableHeaders, origin: str) -> None:
|
| 171 |
+
headers["Access-Control-Allow-Origin"] = origin
|
| 172 |
+
headers.add_vary_header("Origin")
|
.venv/lib/python3.11/site-packages/starlette/middleware/errors.py
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import html
|
| 4 |
+
import inspect
|
| 5 |
+
import sys
|
| 6 |
+
import traceback
|
| 7 |
+
import typing
|
| 8 |
+
|
| 9 |
+
from starlette._utils import is_async_callable
|
| 10 |
+
from starlette.concurrency import run_in_threadpool
|
| 11 |
+
from starlette.requests import Request
|
| 12 |
+
from starlette.responses import HTMLResponse, PlainTextResponse, Response
|
| 13 |
+
from starlette.types import ASGIApp, Message, Receive, Scope, Send
|
| 14 |
+
|
| 15 |
+
STYLES = """
|
| 16 |
+
p {
|
| 17 |
+
color: #211c1c;
|
| 18 |
+
}
|
| 19 |
+
.traceback-container {
|
| 20 |
+
border: 1px solid #038BB8;
|
| 21 |
+
}
|
| 22 |
+
.traceback-title {
|
| 23 |
+
background-color: #038BB8;
|
| 24 |
+
color: lemonchiffon;
|
| 25 |
+
padding: 12px;
|
| 26 |
+
font-size: 20px;
|
| 27 |
+
margin-top: 0px;
|
| 28 |
+
}
|
| 29 |
+
.frame-line {
|
| 30 |
+
padding-left: 10px;
|
| 31 |
+
font-family: monospace;
|
| 32 |
+
}
|
| 33 |
+
.frame-filename {
|
| 34 |
+
font-family: monospace;
|
| 35 |
+
}
|
| 36 |
+
.center-line {
|
| 37 |
+
background-color: #038BB8;
|
| 38 |
+
color: #f9f6e1;
|
| 39 |
+
padding: 5px 0px 5px 5px;
|
| 40 |
+
}
|
| 41 |
+
.lineno {
|
| 42 |
+
margin-right: 5px;
|
| 43 |
+
}
|
| 44 |
+
.frame-title {
|
| 45 |
+
font-weight: unset;
|
| 46 |
+
padding: 10px 10px 10px 10px;
|
| 47 |
+
background-color: #E4F4FD;
|
| 48 |
+
margin-right: 10px;
|
| 49 |
+
color: #191f21;
|
| 50 |
+
font-size: 17px;
|
| 51 |
+
border: 1px solid #c7dce8;
|
| 52 |
+
}
|
| 53 |
+
.collapse-btn {
|
| 54 |
+
float: right;
|
| 55 |
+
padding: 0px 5px 1px 5px;
|
| 56 |
+
border: solid 1px #96aebb;
|
| 57 |
+
cursor: pointer;
|
| 58 |
+
}
|
| 59 |
+
.collapsed {
|
| 60 |
+
display: none;
|
| 61 |
+
}
|
| 62 |
+
.source-code {
|
| 63 |
+
font-family: courier;
|
| 64 |
+
font-size: small;
|
| 65 |
+
padding-bottom: 10px;
|
| 66 |
+
}
|
| 67 |
+
"""
|
| 68 |
+
|
| 69 |
+
JS = """
|
| 70 |
+
<script type="text/javascript">
|
| 71 |
+
function collapse(element){
|
| 72 |
+
const frameId = element.getAttribute("data-frame-id");
|
| 73 |
+
const frame = document.getElementById(frameId);
|
| 74 |
+
|
| 75 |
+
if (frame.classList.contains("collapsed")){
|
| 76 |
+
element.innerHTML = "‒";
|
| 77 |
+
frame.classList.remove("collapsed");
|
| 78 |
+
} else {
|
| 79 |
+
element.innerHTML = "+";
|
| 80 |
+
frame.classList.add("collapsed");
|
| 81 |
+
}
|
| 82 |
+
}
|
| 83 |
+
</script>
|
| 84 |
+
"""
|
| 85 |
+
|
| 86 |
+
TEMPLATE = """
|
| 87 |
+
<html>
|
| 88 |
+
<head>
|
| 89 |
+
<style type='text/css'>
|
| 90 |
+
{styles}
|
| 91 |
+
</style>
|
| 92 |
+
<title>Starlette Debugger</title>
|
| 93 |
+
</head>
|
| 94 |
+
<body>
|
| 95 |
+
<h1>500 Server Error</h1>
|
| 96 |
+
<h2>{error}</h2>
|
| 97 |
+
<div class="traceback-container">
|
| 98 |
+
<p class="traceback-title">Traceback</p>
|
| 99 |
+
<div>{exc_html}</div>
|
| 100 |
+
</div>
|
| 101 |
+
{js}
|
| 102 |
+
</body>
|
| 103 |
+
</html>
|
| 104 |
+
"""
|
| 105 |
+
|
| 106 |
+
FRAME_TEMPLATE = """
|
| 107 |
+
<div>
|
| 108 |
+
<p class="frame-title">File <span class="frame-filename">{frame_filename}</span>,
|
| 109 |
+
line <i>{frame_lineno}</i>,
|
| 110 |
+
in <b>{frame_name}</b>
|
| 111 |
+
<span class="collapse-btn" data-frame-id="{frame_filename}-{frame_lineno}" onclick="collapse(this)">{collapse_button}</span>
|
| 112 |
+
</p>
|
| 113 |
+
<div id="{frame_filename}-{frame_lineno}" class="source-code {collapsed}">{code_context}</div>
|
| 114 |
+
</div>
|
| 115 |
+
""" # noqa: E501
|
| 116 |
+
|
| 117 |
+
LINE = """
|
| 118 |
+
<p><span class="frame-line">
|
| 119 |
+
<span class="lineno">{lineno}.</span> {line}</span></p>
|
| 120 |
+
"""
|
| 121 |
+
|
| 122 |
+
CENTER_LINE = """
|
| 123 |
+
<p class="center-line"><span class="frame-line center-line">
|
| 124 |
+
<span class="lineno">{lineno}.</span> {line}</span></p>
|
| 125 |
+
"""
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
class ServerErrorMiddleware:
|
| 129 |
+
"""
|
| 130 |
+
Handles returning 500 responses when a server error occurs.
|
| 131 |
+
|
| 132 |
+
If 'debug' is set, then traceback responses will be returned,
|
| 133 |
+
otherwise the designated 'handler' will be called.
|
| 134 |
+
|
| 135 |
+
This middleware class should generally be used to wrap *everything*
|
| 136 |
+
else up, so that unhandled exceptions anywhere in the stack
|
| 137 |
+
always result in an appropriate 500 response.
|
| 138 |
+
"""
|
| 139 |
+
|
| 140 |
+
def __init__(
|
| 141 |
+
self,
|
| 142 |
+
app: ASGIApp,
|
| 143 |
+
handler: typing.Callable[[Request, Exception], typing.Any] | None = None,
|
| 144 |
+
debug: bool = False,
|
| 145 |
+
) -> None:
|
| 146 |
+
self.app = app
|
| 147 |
+
self.handler = handler
|
| 148 |
+
self.debug = debug
|
| 149 |
+
|
| 150 |
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
| 151 |
+
if scope["type"] != "http":
|
| 152 |
+
await self.app(scope, receive, send)
|
| 153 |
+
return
|
| 154 |
+
|
| 155 |
+
response_started = False
|
| 156 |
+
|
| 157 |
+
async def _send(message: Message) -> None:
|
| 158 |
+
nonlocal response_started, send
|
| 159 |
+
|
| 160 |
+
if message["type"] == "http.response.start":
|
| 161 |
+
response_started = True
|
| 162 |
+
await send(message)
|
| 163 |
+
|
| 164 |
+
try:
|
| 165 |
+
await self.app(scope, receive, _send)
|
| 166 |
+
except Exception as exc:
|
| 167 |
+
request = Request(scope)
|
| 168 |
+
if self.debug:
|
| 169 |
+
# In debug mode, return traceback responses.
|
| 170 |
+
response = self.debug_response(request, exc)
|
| 171 |
+
elif self.handler is None:
|
| 172 |
+
# Use our default 500 error handler.
|
| 173 |
+
response = self.error_response(request, exc)
|
| 174 |
+
else:
|
| 175 |
+
# Use an installed 500 error handler.
|
| 176 |
+
if is_async_callable(self.handler):
|
| 177 |
+
response = await self.handler(request, exc)
|
| 178 |
+
else:
|
| 179 |
+
response = await run_in_threadpool(self.handler, request, exc)
|
| 180 |
+
|
| 181 |
+
if not response_started:
|
| 182 |
+
await response(scope, receive, send)
|
| 183 |
+
|
| 184 |
+
# We always continue to raise the exception.
|
| 185 |
+
# This allows servers to log the error, or allows test clients
|
| 186 |
+
# to optionally raise the error within the test case.
|
| 187 |
+
raise exc
|
| 188 |
+
|
| 189 |
+
def format_line(self, index: int, line: str, frame_lineno: int, frame_index: int) -> str:
|
| 190 |
+
values = {
|
| 191 |
+
# HTML escape - line could contain < or >
|
| 192 |
+
"line": html.escape(line).replace(" ", " "),
|
| 193 |
+
"lineno": (frame_lineno - frame_index) + index,
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
if index != frame_index:
|
| 197 |
+
return LINE.format(**values)
|
| 198 |
+
return CENTER_LINE.format(**values)
|
| 199 |
+
|
| 200 |
+
def generate_frame_html(self, frame: inspect.FrameInfo, is_collapsed: bool) -> str:
|
| 201 |
+
code_context = "".join(
|
| 202 |
+
self.format_line(
|
| 203 |
+
index,
|
| 204 |
+
line,
|
| 205 |
+
frame.lineno,
|
| 206 |
+
frame.index, # type: ignore[arg-type]
|
| 207 |
+
)
|
| 208 |
+
for index, line in enumerate(frame.code_context or [])
|
| 209 |
+
)
|
| 210 |
+
|
| 211 |
+
values = {
|
| 212 |
+
# HTML escape - filename could contain < or >, especially if it's a virtual
|
| 213 |
+
# file e.g. <stdin> in the REPL
|
| 214 |
+
"frame_filename": html.escape(frame.filename),
|
| 215 |
+
"frame_lineno": frame.lineno,
|
| 216 |
+
# HTML escape - if you try very hard it's possible to name a function with <
|
| 217 |
+
# or >
|
| 218 |
+
"frame_name": html.escape(frame.function),
|
| 219 |
+
"code_context": code_context,
|
| 220 |
+
"collapsed": "collapsed" if is_collapsed else "",
|
| 221 |
+
"collapse_button": "+" if is_collapsed else "‒",
|
| 222 |
+
}
|
| 223 |
+
return FRAME_TEMPLATE.format(**values)
|
| 224 |
+
|
| 225 |
+
def generate_html(self, exc: Exception, limit: int = 7) -> str:
|
| 226 |
+
traceback_obj = traceback.TracebackException.from_exception(exc, capture_locals=True)
|
| 227 |
+
|
| 228 |
+
exc_html = ""
|
| 229 |
+
is_collapsed = False
|
| 230 |
+
exc_traceback = exc.__traceback__
|
| 231 |
+
if exc_traceback is not None:
|
| 232 |
+
frames = inspect.getinnerframes(exc_traceback, limit)
|
| 233 |
+
for frame in reversed(frames):
|
| 234 |
+
exc_html += self.generate_frame_html(frame, is_collapsed)
|
| 235 |
+
is_collapsed = True
|
| 236 |
+
|
| 237 |
+
if sys.version_info >= (3, 13): # pragma: no cover
|
| 238 |
+
exc_type_str = traceback_obj.exc_type_str
|
| 239 |
+
else: # pragma: no cover
|
| 240 |
+
exc_type_str = traceback_obj.exc_type.__name__
|
| 241 |
+
|
| 242 |
+
# escape error class and text
|
| 243 |
+
error = f"{html.escape(exc_type_str)}: {html.escape(str(traceback_obj))}"
|
| 244 |
+
|
| 245 |
+
return TEMPLATE.format(styles=STYLES, js=JS, error=error, exc_html=exc_html)
|
| 246 |
+
|
| 247 |
+
def generate_plain_text(self, exc: Exception) -> str:
|
| 248 |
+
return "".join(traceback.format_exception(type(exc), exc, exc.__traceback__))
|
| 249 |
+
|
| 250 |
+
def debug_response(self, request: Request, exc: Exception) -> Response:
|
| 251 |
+
accept = request.headers.get("accept", "")
|
| 252 |
+
|
| 253 |
+
if "text/html" in accept:
|
| 254 |
+
content = self.generate_html(exc)
|
| 255 |
+
return HTMLResponse(content, status_code=500)
|
| 256 |
+
content = self.generate_plain_text(exc)
|
| 257 |
+
return PlainTextResponse(content, status_code=500)
|
| 258 |
+
|
| 259 |
+
def error_response(self, request: Request, exc: Exception) -> Response:
|
| 260 |
+
return PlainTextResponse("Internal Server Error", status_code=500)
|
.venv/lib/python3.11/site-packages/starlette/middleware/exceptions.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import typing
|
| 4 |
+
|
| 5 |
+
from starlette._exception_handler import (
|
| 6 |
+
ExceptionHandlers,
|
| 7 |
+
StatusHandlers,
|
| 8 |
+
wrap_app_handling_exceptions,
|
| 9 |
+
)
|
| 10 |
+
from starlette.exceptions import HTTPException, WebSocketException
|
| 11 |
+
from starlette.requests import Request
|
| 12 |
+
from starlette.responses import PlainTextResponse, Response
|
| 13 |
+
from starlette.types import ASGIApp, Receive, Scope, Send
|
| 14 |
+
from starlette.websockets import WebSocket
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class ExceptionMiddleware:
|
| 18 |
+
def __init__(
|
| 19 |
+
self,
|
| 20 |
+
app: ASGIApp,
|
| 21 |
+
handlers: typing.Mapping[typing.Any, typing.Callable[[Request, Exception], Response]] | None = None,
|
| 22 |
+
debug: bool = False,
|
| 23 |
+
) -> None:
|
| 24 |
+
self.app = app
|
| 25 |
+
self.debug = debug # TODO: We ought to handle 404 cases if debug is set.
|
| 26 |
+
self._status_handlers: StatusHandlers = {}
|
| 27 |
+
self._exception_handlers: ExceptionHandlers = {
|
| 28 |
+
HTTPException: self.http_exception,
|
| 29 |
+
WebSocketException: self.websocket_exception,
|
| 30 |
+
}
|
| 31 |
+
if handlers is not None: # pragma: no branch
|
| 32 |
+
for key, value in handlers.items():
|
| 33 |
+
self.add_exception_handler(key, value)
|
| 34 |
+
|
| 35 |
+
def add_exception_handler(
|
| 36 |
+
self,
|
| 37 |
+
exc_class_or_status_code: int | type[Exception],
|
| 38 |
+
handler: typing.Callable[[Request, Exception], Response],
|
| 39 |
+
) -> None:
|
| 40 |
+
if isinstance(exc_class_or_status_code, int):
|
| 41 |
+
self._status_handlers[exc_class_or_status_code] = handler
|
| 42 |
+
else:
|
| 43 |
+
assert issubclass(exc_class_or_status_code, Exception)
|
| 44 |
+
self._exception_handlers[exc_class_or_status_code] = handler
|
| 45 |
+
|
| 46 |
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
| 47 |
+
if scope["type"] not in ("http", "websocket"):
|
| 48 |
+
await self.app(scope, receive, send)
|
| 49 |
+
return
|
| 50 |
+
|
| 51 |
+
scope["starlette.exception_handlers"] = (
|
| 52 |
+
self._exception_handlers,
|
| 53 |
+
self._status_handlers,
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
conn: Request | WebSocket
|
| 57 |
+
if scope["type"] == "http":
|
| 58 |
+
conn = Request(scope, receive, send)
|
| 59 |
+
else:
|
| 60 |
+
conn = WebSocket(scope, receive, send)
|
| 61 |
+
|
| 62 |
+
await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
|
| 63 |
+
|
| 64 |
+
def http_exception(self, request: Request, exc: Exception) -> Response:
|
| 65 |
+
assert isinstance(exc, HTTPException)
|
| 66 |
+
if exc.status_code in {204, 304}:
|
| 67 |
+
return Response(status_code=exc.status_code, headers=exc.headers)
|
| 68 |
+
return PlainTextResponse(exc.detail, status_code=exc.status_code, headers=exc.headers)
|
| 69 |
+
|
| 70 |
+
async def websocket_exception(self, websocket: WebSocket, exc: Exception) -> None:
|
| 71 |
+
assert isinstance(exc, WebSocketException)
|
| 72 |
+
await websocket.close(code=exc.code, reason=exc.reason) # pragma: no cover
|
.venv/lib/python3.11/site-packages/starlette/middleware/gzip.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gzip
|
| 2 |
+
import io
|
| 3 |
+
import typing
|
| 4 |
+
|
| 5 |
+
from starlette.datastructures import Headers, MutableHeaders
|
| 6 |
+
from starlette.types import ASGIApp, Message, Receive, Scope, Send
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class GZipMiddleware:
|
| 10 |
+
def __init__(self, app: ASGIApp, minimum_size: int = 500, compresslevel: int = 9) -> None:
|
| 11 |
+
self.app = app
|
| 12 |
+
self.minimum_size = minimum_size
|
| 13 |
+
self.compresslevel = compresslevel
|
| 14 |
+
|
| 15 |
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
| 16 |
+
if scope["type"] == "http": # pragma: no branch
|
| 17 |
+
headers = Headers(scope=scope)
|
| 18 |
+
if "gzip" in headers.get("Accept-Encoding", ""):
|
| 19 |
+
responder = GZipResponder(self.app, self.minimum_size, compresslevel=self.compresslevel)
|
| 20 |
+
await responder(scope, receive, send)
|
| 21 |
+
return
|
| 22 |
+
await self.app(scope, receive, send)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class GZipResponder:
|
| 26 |
+
def __init__(self, app: ASGIApp, minimum_size: int, compresslevel: int = 9) -> None:
|
| 27 |
+
self.app = app
|
| 28 |
+
self.minimum_size = minimum_size
|
| 29 |
+
self.send: Send = unattached_send
|
| 30 |
+
self.initial_message: Message = {}
|
| 31 |
+
self.started = False
|
| 32 |
+
self.content_encoding_set = False
|
| 33 |
+
self.gzip_buffer = io.BytesIO()
|
| 34 |
+
self.gzip_file = gzip.GzipFile(mode="wb", fileobj=self.gzip_buffer, compresslevel=compresslevel)
|
| 35 |
+
|
| 36 |
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
| 37 |
+
self.send = send
|
| 38 |
+
with self.gzip_buffer, self.gzip_file:
|
| 39 |
+
await self.app(scope, receive, self.send_with_gzip)
|
| 40 |
+
|
| 41 |
+
async def send_with_gzip(self, message: Message) -> None:
|
| 42 |
+
message_type = message["type"]
|
| 43 |
+
if message_type == "http.response.start":
|
| 44 |
+
# Don't send the initial message until we've determined how to
|
| 45 |
+
# modify the outgoing headers correctly.
|
| 46 |
+
self.initial_message = message
|
| 47 |
+
headers = Headers(raw=self.initial_message["headers"])
|
| 48 |
+
self.content_encoding_set = "content-encoding" in headers
|
| 49 |
+
elif message_type == "http.response.body" and self.content_encoding_set:
|
| 50 |
+
if not self.started:
|
| 51 |
+
self.started = True
|
| 52 |
+
await self.send(self.initial_message)
|
| 53 |
+
await self.send(message)
|
| 54 |
+
elif message_type == "http.response.body" and not self.started:
|
| 55 |
+
self.started = True
|
| 56 |
+
body = message.get("body", b"")
|
| 57 |
+
more_body = message.get("more_body", False)
|
| 58 |
+
if len(body) < self.minimum_size and not more_body:
|
| 59 |
+
# Don't apply GZip to small outgoing responses.
|
| 60 |
+
await self.send(self.initial_message)
|
| 61 |
+
await self.send(message)
|
| 62 |
+
elif not more_body:
|
| 63 |
+
# Standard GZip response.
|
| 64 |
+
self.gzip_file.write(body)
|
| 65 |
+
self.gzip_file.close()
|
| 66 |
+
body = self.gzip_buffer.getvalue()
|
| 67 |
+
|
| 68 |
+
headers = MutableHeaders(raw=self.initial_message["headers"])
|
| 69 |
+
headers["Content-Encoding"] = "gzip"
|
| 70 |
+
headers["Content-Length"] = str(len(body))
|
| 71 |
+
headers.add_vary_header("Accept-Encoding")
|
| 72 |
+
message["body"] = body
|
| 73 |
+
|
| 74 |
+
await self.send(self.initial_message)
|
| 75 |
+
await self.send(message)
|
| 76 |
+
else:
|
| 77 |
+
# Initial body in streaming GZip response.
|
| 78 |
+
headers = MutableHeaders(raw=self.initial_message["headers"])
|
| 79 |
+
headers["Content-Encoding"] = "gzip"
|
| 80 |
+
headers.add_vary_header("Accept-Encoding")
|
| 81 |
+
del headers["Content-Length"]
|
| 82 |
+
|
| 83 |
+
self.gzip_file.write(body)
|
| 84 |
+
message["body"] = self.gzip_buffer.getvalue()
|
| 85 |
+
self.gzip_buffer.seek(0)
|
| 86 |
+
self.gzip_buffer.truncate()
|
| 87 |
+
|
| 88 |
+
await self.send(self.initial_message)
|
| 89 |
+
await self.send(message)
|
| 90 |
+
|
| 91 |
+
elif message_type == "http.response.body": # pragma: no branch
|
| 92 |
+
# Remaining body in streaming GZip response.
|
| 93 |
+
body = message.get("body", b"")
|
| 94 |
+
more_body = message.get("more_body", False)
|
| 95 |
+
|
| 96 |
+
self.gzip_file.write(body)
|
| 97 |
+
if not more_body:
|
| 98 |
+
self.gzip_file.close()
|
| 99 |
+
|
| 100 |
+
message["body"] = self.gzip_buffer.getvalue()
|
| 101 |
+
self.gzip_buffer.seek(0)
|
| 102 |
+
self.gzip_buffer.truncate()
|
| 103 |
+
|
| 104 |
+
await self.send(message)
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
async def unattached_send(message: Message) -> typing.NoReturn:
|
| 108 |
+
raise RuntimeError("send awaitable not set") # pragma: no cover
|
.venv/lib/python3.11/site-packages/starlette/middleware/httpsredirect.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from starlette.datastructures import URL
|
| 2 |
+
from starlette.responses import RedirectResponse
|
| 3 |
+
from starlette.types import ASGIApp, Receive, Scope, Send
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class HTTPSRedirectMiddleware:
|
| 7 |
+
def __init__(self, app: ASGIApp) -> None:
|
| 8 |
+
self.app = app
|
| 9 |
+
|
| 10 |
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
| 11 |
+
if scope["type"] in ("http", "websocket") and scope["scheme"] in ("http", "ws"):
|
| 12 |
+
url = URL(scope=scope)
|
| 13 |
+
redirect_scheme = {"http": "https", "ws": "wss"}[url.scheme]
|
| 14 |
+
netloc = url.hostname if url.port in (80, 443) else url.netloc
|
| 15 |
+
url = url.replace(scheme=redirect_scheme, netloc=netloc)
|
| 16 |
+
response = RedirectResponse(url, status_code=307)
|
| 17 |
+
await response(scope, receive, send)
|
| 18 |
+
else:
|
| 19 |
+
await self.app(scope, receive, send)
|