aboutsummaryrefslogtreecommitdiff
path: root/.venv/lib/python3.12/site-packages/starlette
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/starlette')
-rw-r--r--.venv/lib/python3.12/site-packages/starlette/__init__.py1
-rw-r--r--.venv/lib/python3.12/site-packages/starlette/_exception_handler.py65
-rw-r--r--.venv/lib/python3.12/site-packages/starlette/_utils.py100
-rw-r--r--.venv/lib/python3.12/site-packages/starlette/applications.py249
-rw-r--r--.venv/lib/python3.12/site-packages/starlette/authentication.py147
-rw-r--r--.venv/lib/python3.12/site-packages/starlette/background.py41
-rw-r--r--.venv/lib/python3.12/site-packages/starlette/concurrency.py62
-rw-r--r--.venv/lib/python3.12/site-packages/starlette/config.py138
-rw-r--r--.venv/lib/python3.12/site-packages/starlette/convertors.py89
-rw-r--r--.venv/lib/python3.12/site-packages/starlette/datastructures.py674
-rw-r--r--.venv/lib/python3.12/site-packages/starlette/endpoints.py122
-rw-r--r--.venv/lib/python3.12/site-packages/starlette/exceptions.py33
-rw-r--r--.venv/lib/python3.12/site-packages/starlette/formparsers.py275
-rw-r--r--.venv/lib/python3.12/site-packages/starlette/middleware/__init__.py42
-rw-r--r--.venv/lib/python3.12/site-packages/starlette/middleware/authentication.py52
-rw-r--r--.venv/lib/python3.12/site-packages/starlette/middleware/base.py220
-rw-r--r--.venv/lib/python3.12/site-packages/starlette/middleware/cors.py172
-rw-r--r--.venv/lib/python3.12/site-packages/starlette/middleware/errors.py260
-rw-r--r--.venv/lib/python3.12/site-packages/starlette/middleware/exceptions.py72
-rw-r--r--.venv/lib/python3.12/site-packages/starlette/middleware/gzip.py141
-rw-r--r--.venv/lib/python3.12/site-packages/starlette/middleware/httpsredirect.py19
-rw-r--r--.venv/lib/python3.12/site-packages/starlette/middleware/sessions.py85
-rw-r--r--.venv/lib/python3.12/site-packages/starlette/middleware/trustedhost.py60
-rw-r--r--.venv/lib/python3.12/site-packages/starlette/middleware/wsgi.py152
-rw-r--r--.venv/lib/python3.12/site-packages/starlette/py.typed0
-rw-r--r--.venv/lib/python3.12/site-packages/starlette/requests.py322
-rw-r--r--.venv/lib/python3.12/site-packages/starlette/responses.py536
-rw-r--r--.venv/lib/python3.12/site-packages/starlette/routing.py874
-rw-r--r--.venv/lib/python3.12/site-packages/starlette/schemas.py147
-rw-r--r--.venv/lib/python3.12/site-packages/starlette/staticfiles.py220
-rw-r--r--.venv/lib/python3.12/site-packages/starlette/status.py95
-rw-r--r--.venv/lib/python3.12/site-packages/starlette/templating.py216
-rw-r--r--.venv/lib/python3.12/site-packages/starlette/testclient.py731
-rw-r--r--.venv/lib/python3.12/site-packages/starlette/types.py24
-rw-r--r--.venv/lib/python3.12/site-packages/starlette/websockets.py195
35 files changed, 6631 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/starlette/__init__.py b/.venv/lib/python3.12/site-packages/starlette/__init__.py
new file mode 100644
index 00000000..cffb82c5
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/starlette/__init__.py
@@ -0,0 +1 @@
+__version__ = "0.46.1"
diff --git a/.venv/lib/python3.12/site-packages/starlette/_exception_handler.py b/.venv/lib/python3.12/site-packages/starlette/_exception_handler.py
new file mode 100644
index 00000000..72bc89d9
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/starlette/_exception_handler.py
@@ -0,0 +1,65 @@
+from __future__ import annotations
+
+import typing
+
+from starlette._utils import is_async_callable
+from starlette.concurrency import run_in_threadpool
+from starlette.exceptions import HTTPException
+from starlette.requests import Request
+from starlette.types import ASGIApp, ExceptionHandler, Message, Receive, Scope, Send
+from starlette.websockets import WebSocket
+
+ExceptionHandlers = dict[typing.Any, ExceptionHandler]
+StatusHandlers = dict[int, ExceptionHandler]
+
+
+def _lookup_exception_handler(exc_handlers: ExceptionHandlers, exc: Exception) -> ExceptionHandler | None:
+ for cls in type(exc).__mro__:
+ if cls in exc_handlers:
+ return exc_handlers[cls]
+ return None
+
+
+def wrap_app_handling_exceptions(app: ASGIApp, conn: Request | WebSocket) -> ASGIApp:
+ exception_handlers: ExceptionHandlers
+ status_handlers: StatusHandlers
+ try:
+ exception_handlers, status_handlers = conn.scope["starlette.exception_handlers"]
+ except KeyError:
+ exception_handlers, status_handlers = {}, {}
+
+ async def wrapped_app(scope: Scope, receive: Receive, send: Send) -> None:
+ response_started = False
+
+ async def sender(message: Message) -> None:
+ nonlocal response_started
+
+ if message["type"] == "http.response.start":
+ response_started = True
+ await send(message)
+
+ try:
+ await app(scope, receive, sender)
+ except Exception as exc:
+ handler = None
+
+ if isinstance(exc, HTTPException):
+ handler = status_handlers.get(exc.status_code)
+
+ if handler is None:
+ handler = _lookup_exception_handler(exception_handlers, exc)
+
+ if handler is None:
+ raise exc
+
+ if response_started:
+ raise RuntimeError("Caught handled exception, but response already started.") from exc
+
+ if is_async_callable(handler):
+ response = await handler(conn, exc)
+ else:
+ response = await run_in_threadpool(handler, conn, exc) # type: ignore
+ if response is not None:
+ await response(scope, receive, sender)
+
+ return wrapped_app
diff --git a/.venv/lib/python3.12/site-packages/starlette/_utils.py b/.venv/lib/python3.12/site-packages/starlette/_utils.py
new file mode 100644
index 00000000..8001c472
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/starlette/_utils.py
@@ -0,0 +1,100 @@
+from __future__ import annotations
+
+import functools
+import inspect
+import sys
+import typing
+from contextlib import contextmanager
+
+from starlette.types import Scope
+
+if sys.version_info >= (3, 10): # pragma: no cover
+ from typing import TypeGuard
+else: # pragma: no cover
+ from typing_extensions import TypeGuard
+
+has_exceptiongroups = True
+if sys.version_info < (3, 11): # pragma: no cover
+ try:
+ from exceptiongroup import BaseExceptionGroup # type: ignore[unused-ignore,import-not-found]
+ except ImportError:
+ has_exceptiongroups = False
+
+T = typing.TypeVar("T")
+AwaitableCallable = typing.Callable[..., typing.Awaitable[T]]
+
+
+@typing.overload
+def is_async_callable(obj: AwaitableCallable[T]) -> TypeGuard[AwaitableCallable[T]]: ...
+
+
+@typing.overload
+def is_async_callable(obj: typing.Any) -> TypeGuard[AwaitableCallable[typing.Any]]: ...
+
+
+def is_async_callable(obj: typing.Any) -> typing.Any:
+ while isinstance(obj, functools.partial):
+ obj = obj.func
+
+ return inspect.iscoroutinefunction(obj) or (callable(obj) and inspect.iscoroutinefunction(obj.__call__))
+
+
+T_co = typing.TypeVar("T_co", covariant=True)
+
+
+class AwaitableOrContextManager(typing.Awaitable[T_co], typing.AsyncContextManager[T_co], typing.Protocol[T_co]): ...
+
+
+class SupportsAsyncClose(typing.Protocol):
+ async def close(self) -> None: ... # pragma: no cover
+
+
+SupportsAsyncCloseType = typing.TypeVar("SupportsAsyncCloseType", bound=SupportsAsyncClose, covariant=False)
+
+
+class AwaitableOrContextManagerWrapper(typing.Generic[SupportsAsyncCloseType]):
+ __slots__ = ("aw", "entered")
+
+ def __init__(self, aw: typing.Awaitable[SupportsAsyncCloseType]) -> None:
+ self.aw = aw
+
+ def __await__(self) -> typing.Generator[typing.Any, None, SupportsAsyncCloseType]:
+ return self.aw.__await__()
+
+ async def __aenter__(self) -> SupportsAsyncCloseType:
+ self.entered = await self.aw
+ return self.entered
+
+ async def __aexit__(self, *args: typing.Any) -> None | bool:
+ await self.entered.close()
+ return None
+
+
+@contextmanager
+def collapse_excgroups() -> typing.Generator[None, None, None]:
+ try:
+ yield
+ except BaseException as exc:
+ if has_exceptiongroups: # pragma: no cover
+ while isinstance(exc, BaseExceptionGroup) and len(exc.exceptions) == 1:
+ exc = exc.exceptions[0]
+
+ raise exc
+
+
+def get_route_path(scope: Scope) -> str:
+ path: str = scope["path"]
+ root_path = scope.get("root_path", "")
+ if not root_path:
+ return path
+
+ if not path.startswith(root_path):
+ return path
+
+ if path == root_path:
+ return ""
+
+ if path[len(root_path)] == "/":
+ return path[len(root_path) :]
+
+ return path
diff --git a/.venv/lib/python3.12/site-packages/starlette/applications.py b/.venv/lib/python3.12/site-packages/starlette/applications.py
new file mode 100644
index 00000000..6df5a707
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/starlette/applications.py
@@ -0,0 +1,249 @@
+from __future__ import annotations
+
+import sys
+import typing
+import warnings
+
+if sys.version_info >= (3, 10): # pragma: no cover
+ from typing import ParamSpec
+else: # pragma: no cover
+ from typing_extensions import ParamSpec
+
+from starlette.datastructures import State, URLPath
+from starlette.middleware import Middleware, _MiddlewareFactory
+from starlette.middleware.base import BaseHTTPMiddleware
+from starlette.middleware.errors import ServerErrorMiddleware
+from starlette.middleware.exceptions import ExceptionMiddleware
+from starlette.requests import Request
+from starlette.responses import Response
+from starlette.routing import BaseRoute, Router
+from starlette.types import ASGIApp, ExceptionHandler, Lifespan, Receive, Scope, Send
+from starlette.websockets import WebSocket
+
+AppType = typing.TypeVar("AppType", bound="Starlette")
+P = ParamSpec("P")
+
+
+class Starlette:
+ """Creates an Starlette application."""
+
+ def __init__(
+ self: AppType,
+ debug: bool = False,
+ routes: typing.Sequence[BaseRoute] | None = None,
+ middleware: typing.Sequence[Middleware] | None = None,
+ exception_handlers: typing.Mapping[typing.Any, ExceptionHandler] | None = None,
+ on_startup: typing.Sequence[typing.Callable[[], typing.Any]] | None = None,
+ on_shutdown: typing.Sequence[typing.Callable[[], typing.Any]] | None = None,
+ lifespan: Lifespan[AppType] | None = None,
+ ) -> None:
+ """Initializes the application.
+
+ Parameters:
+ debug: Boolean indicating if debug tracebacks should be returned on errors.
+ routes: A list of routes to serve incoming HTTP and WebSocket requests.
+ middleware: A list of middleware to run for every request. A starlette
+ application will always automatically include two middleware classes.
+ `ServerErrorMiddleware` is added as the very outermost middleware, to handle
+ any uncaught errors occurring anywhere in the entire stack.
+ `ExceptionMiddleware` is added as the very innermost middleware, to deal
+ with handled exception cases occurring in the routing or endpoints.
+ exception_handlers: A mapping of either integer status codes,
+ or exception class types onto callables which handle the exceptions.
+ Exception handler callables should be of the form
+ `handler(request, exc) -> response` and may be either standard functions, or
+ async functions.
+ on_startup: A list of callables to run on application startup.
+ Startup handler callables do not take any arguments, and may be either
+ standard functions, or async functions.
+ on_shutdown: A list of callables to run on application shutdown.
+ Shutdown handler callables do not take any arguments, and may be either
+ standard functions, or async functions.
+ lifespan: A lifespan context function, which can be used to perform
+ startup and shutdown tasks. This is a newer style that replaces the
+ `on_startup` and `on_shutdown` handlers. Use one or the other, not both.
+ """
+ # The lifespan context function is a newer style that replaces
+ # on_startup / on_shutdown handlers. Use one or the other, not both.
+ assert lifespan is None or (on_startup is None and on_shutdown is None), (
+ "Use either 'lifespan' or 'on_startup'/'on_shutdown', not both."
+ )
+
+ self.debug = debug
+ self.state = State()
+ self.router = Router(routes, on_startup=on_startup, on_shutdown=on_shutdown, lifespan=lifespan)
+ self.exception_handlers = {} if exception_handlers is None else dict(exception_handlers)
+ self.user_middleware = [] if middleware is None else list(middleware)
+ self.middleware_stack: ASGIApp | None = None
+
+ def build_middleware_stack(self) -> ASGIApp:
+ debug = self.debug
+ error_handler = None
+ exception_handlers: dict[typing.Any, typing.Callable[[Request, Exception], Response]] = {}
+
+ for key, value in self.exception_handlers.items():
+ if key in (500, Exception):
+ error_handler = value
+ else:
+ exception_handlers[key] = value
+
+ middleware = (
+ [Middleware(ServerErrorMiddleware, handler=error_handler, debug=debug)]
+ + self.user_middleware
+ + [Middleware(ExceptionMiddleware, handlers=exception_handlers, debug=debug)]
+ )
+
+ app = self.router
+ for cls, args, kwargs in reversed(middleware):
+ app = cls(app, *args, **kwargs)
+ return app
+
+ @property
+ def routes(self) -> list[BaseRoute]:
+ return self.router.routes
+
+ def url_path_for(self, name: str, /, **path_params: typing.Any) -> URLPath:
+ return self.router.url_path_for(name, **path_params)
+
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
+ scope["app"] = self
+ if self.middleware_stack is None:
+ self.middleware_stack = self.build_middleware_stack()
+ await self.middleware_stack(scope, receive, send)
+
+ def on_event(self, event_type: str) -> typing.Callable: # type: ignore[type-arg]
+ return self.router.on_event(event_type) # pragma: no cover
+
+ def mount(self, path: str, app: ASGIApp, name: str | None = None) -> None:
+ self.router.mount(path, app=app, name=name) # pragma: no cover
+
+ def host(self, host: str, app: ASGIApp, name: str | None = None) -> None:
+ self.router.host(host, app=app, name=name) # pragma: no cover
+
+ def add_middleware(
+ self,
+ middleware_class: _MiddlewareFactory[P],
+ *args: P.args,
+ **kwargs: P.kwargs,
+ ) -> None:
+ if self.middleware_stack is not None: # pragma: no cover
+ raise RuntimeError("Cannot add middleware after an application has started")
+ self.user_middleware.insert(0, Middleware(middleware_class, *args, **kwargs))
+
+ def add_exception_handler(
+ self,
+ exc_class_or_status_code: int | type[Exception],
+ handler: ExceptionHandler,
+ ) -> None: # pragma: no cover
+ self.exception_handlers[exc_class_or_status_code] = handler
+
+ def add_event_handler(
+ self,
+ event_type: str,
+ func: typing.Callable, # type: ignore[type-arg]
+ ) -> None: # pragma: no cover
+ self.router.add_event_handler(event_type, func)
+
+ def add_route(
+ self,
+ path: str,
+ route: typing.Callable[[Request], typing.Awaitable[Response] | Response],
+ methods: list[str] | None = None,
+ name: str | None = None,
+ include_in_schema: bool = True,
+ ) -> None: # pragma: no cover
+ self.router.add_route(path, route, methods=methods, name=name, include_in_schema=include_in_schema)
+
+ def add_websocket_route(
+ self,
+ path: str,
+ route: typing.Callable[[WebSocket], typing.Awaitable[None]],
+ name: str | None = None,
+ ) -> None: # pragma: no cover
+ self.router.add_websocket_route(path, route, name=name)
+
+ def exception_handler(self, exc_class_or_status_code: int | type[Exception]) -> typing.Callable: # type: ignore[type-arg]
+ warnings.warn(
+ "The `exception_handler` decorator is deprecated, and will be removed in version 1.0.0. "
+ "Refer to https://www.starlette.io/exceptions/ for the recommended approach.",
+ DeprecationWarning,
+ )
+
+ def decorator(func: typing.Callable) -> typing.Callable: # type: ignore[type-arg]
+ self.add_exception_handler(exc_class_or_status_code, func)
+ return func
+
+ return decorator
+
+ def route(
+ self,
+ path: str,
+ methods: list[str] | None = None,
+ name: str | None = None,
+ include_in_schema: bool = True,
+ ) -> typing.Callable: # type: ignore[type-arg]
+ """
+ We no longer document this decorator style API, and its usage is discouraged.
+ Instead you should use the following approach:
+
+ >>> routes = [Route(path, endpoint=...), ...]
+ >>> app = Starlette(routes=routes)
+ """
+ warnings.warn(
+ "The `route` decorator is deprecated, and will be removed in version 1.0.0. "
+ "Refer to https://www.starlette.io/routing/ for the recommended approach.",
+ DeprecationWarning,
+ )
+
+ def decorator(func: typing.Callable) -> typing.Callable: # type: ignore[type-arg]
+ self.router.add_route(
+ path,
+ func,
+ methods=methods,
+ name=name,
+ include_in_schema=include_in_schema,
+ )
+ return func
+
+ return decorator
+
+ def websocket_route(self, path: str, name: str | None = None) -> typing.Callable: # type: ignore[type-arg]
+ """
+ We no longer document this decorator style API, and its usage is discouraged.
+ Instead you should use the following approach:
+
+ >>> routes = [WebSocketRoute(path, endpoint=...), ...]
+ >>> app = Starlette(routes=routes)
+ """
+ warnings.warn(
+ "The `websocket_route` decorator is deprecated, and will be removed in version 1.0.0. "
+ "Refer to https://www.starlette.io/routing/#websocket-routing for the recommended approach.",
+ DeprecationWarning,
+ )
+
+ def decorator(func: typing.Callable) -> typing.Callable: # type: ignore[type-arg]
+ self.router.add_websocket_route(path, func, name=name)
+ return func
+
+ return decorator
+
+ def middleware(self, middleware_type: str) -> typing.Callable: # type: ignore[type-arg]
+ """
+ We no longer document this decorator style API, and its usage is discouraged.
+ Instead you should use the following approach:
+
+ >>> middleware = [Middleware(...), ...]
+ >>> app = Starlette(middleware=middleware)
+ """
+ warnings.warn(
+ "The `middleware` decorator is deprecated, and will be removed in version 1.0.0. "
+ "Refer to https://www.starlette.io/middleware/#using-middleware for recommended approach.",
+ DeprecationWarning,
+ )
+ assert middleware_type == "http", 'Currently only middleware("http") is supported.'
+
+ def decorator(func: typing.Callable) -> typing.Callable: # type: ignore[type-arg]
+ self.add_middleware(BaseHTTPMiddleware, dispatch=func)
+ return func
+
+ return decorator
diff --git a/.venv/lib/python3.12/site-packages/starlette/authentication.py b/.venv/lib/python3.12/site-packages/starlette/authentication.py
new file mode 100644
index 00000000..4fd86641
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/starlette/authentication.py
@@ -0,0 +1,147 @@
+from __future__ import annotations
+
+import functools
+import inspect
+import sys
+import typing
+from urllib.parse import urlencode
+
+if sys.version_info >= (3, 10): # pragma: no cover
+ from typing import ParamSpec
+else: # pragma: no cover
+ from typing_extensions import ParamSpec
+
+from starlette._utils import is_async_callable
+from starlette.exceptions import HTTPException
+from starlette.requests import HTTPConnection, Request
+from starlette.responses import RedirectResponse
+from starlette.websockets import WebSocket
+
+_P = ParamSpec("_P")
+
+
+def has_required_scope(conn: HTTPConnection, scopes: typing.Sequence[str]) -> bool:
+ for scope in scopes:
+ if scope not in conn.auth.scopes:
+ return False
+ return True
+
+
+def requires(
+ scopes: str | typing.Sequence[str],
+ status_code: int = 403,
+ redirect: str | None = None,
+) -> typing.Callable[[typing.Callable[_P, typing.Any]], typing.Callable[_P, typing.Any]]:
+ scopes_list = [scopes] if isinstance(scopes, str) else list(scopes)
+
+ def decorator(
+ func: typing.Callable[_P, typing.Any],
+ ) -> typing.Callable[_P, typing.Any]:
+ sig = inspect.signature(func)
+ for idx, parameter in enumerate(sig.parameters.values()):
+ if parameter.name == "request" or parameter.name == "websocket":
+ type_ = parameter.name
+ break
+ else:
+ raise Exception(f'No "request" or "websocket" argument on function "{func}"')
+
+ if type_ == "websocket":
+ # Handle websocket functions. (Always async)
+ @functools.wraps(func)
+ async def websocket_wrapper(*args: _P.args, **kwargs: _P.kwargs) -> None:
+ websocket = kwargs.get("websocket", args[idx] if idx < len(args) else None)
+ assert isinstance(websocket, WebSocket)
+
+ if not has_required_scope(websocket, scopes_list):
+ await websocket.close()
+ else:
+ await func(*args, **kwargs)
+
+ return websocket_wrapper
+
+ elif is_async_callable(func):
+ # Handle async request/response functions.
+ @functools.wraps(func)
+ async def async_wrapper(*args: _P.args, **kwargs: _P.kwargs) -> typing.Any:
+ request = kwargs.get("request", args[idx] if idx < len(args) else None)
+ assert isinstance(request, Request)
+
+ if not has_required_scope(request, scopes_list):
+ if redirect is not None:
+ orig_request_qparam = urlencode({"next": str(request.url)})
+ next_url = f"{request.url_for(redirect)}?{orig_request_qparam}"
+ return RedirectResponse(url=next_url, status_code=303)
+ raise HTTPException(status_code=status_code)
+ return await func(*args, **kwargs)
+
+ return async_wrapper
+
+ else:
+ # Handle sync request/response functions.
+ @functools.wraps(func)
+ def sync_wrapper(*args: _P.args, **kwargs: _P.kwargs) -> typing.Any:
+ request = kwargs.get("request", args[idx] if idx < len(args) else None)
+ assert isinstance(request, Request)
+
+ if not has_required_scope(request, scopes_list):
+ if redirect is not None:
+ orig_request_qparam = urlencode({"next": str(request.url)})
+ next_url = f"{request.url_for(redirect)}?{orig_request_qparam}"
+ return RedirectResponse(url=next_url, status_code=303)
+ raise HTTPException(status_code=status_code)
+ return func(*args, **kwargs)
+
+ return sync_wrapper
+
+ return decorator
+
+
+class AuthenticationError(Exception):
+ pass
+
+
+class AuthenticationBackend:
+ async def authenticate(self, conn: HTTPConnection) -> tuple[AuthCredentials, BaseUser] | None:
+ raise NotImplementedError() # pragma: no cover
+
+
+class AuthCredentials:
+ def __init__(self, scopes: typing.Sequence[str] | None = None):
+ self.scopes = [] if scopes is None else list(scopes)
+
+
+class BaseUser:
+ @property
+ def is_authenticated(self) -> bool:
+ raise NotImplementedError() # pragma: no cover
+
+ @property
+ def display_name(self) -> str:
+ raise NotImplementedError() # pragma: no cover
+
+ @property
+ def identity(self) -> str:
+ raise NotImplementedError() # pragma: no cover
+
+
+class SimpleUser(BaseUser):
+ def __init__(self, username: str) -> None:
+ self.username = username
+
+ @property
+ def is_authenticated(self) -> bool:
+ return True
+
+ @property
+ def display_name(self) -> str:
+ return self.username
+
+
+class UnauthenticatedUser(BaseUser):
+ @property
+ def is_authenticated(self) -> bool:
+ return False
+
+ @property
+ def display_name(self) -> str:
+ return ""
diff --git a/.venv/lib/python3.12/site-packages/starlette/background.py b/.venv/lib/python3.12/site-packages/starlette/background.py
new file mode 100644
index 00000000..0430fc08
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/starlette/background.py
@@ -0,0 +1,41 @@
+from __future__ import annotations
+
+import sys
+import typing
+
+if sys.version_info >= (3, 10): # pragma: no cover
+ from typing import ParamSpec
+else: # pragma: no cover
+ from typing_extensions import ParamSpec
+
+from starlette._utils import is_async_callable
+from starlette.concurrency import run_in_threadpool
+
+P = ParamSpec("P")
+
+
+class BackgroundTask:
+ def __init__(self, func: typing.Callable[P, typing.Any], *args: P.args, **kwargs: P.kwargs) -> None:
+ self.func = func
+ self.args = args
+ self.kwargs = kwargs
+ self.is_async = is_async_callable(func)
+
+ async def __call__(self) -> None:
+ if self.is_async:
+ await self.func(*self.args, **self.kwargs)
+ else:
+ await run_in_threadpool(self.func, *self.args, **self.kwargs)
+
+
+class BackgroundTasks(BackgroundTask):
+ def __init__(self, tasks: typing.Sequence[BackgroundTask] | None = None):
+ self.tasks = list(tasks) if tasks else []
+
+ def add_task(self, func: typing.Callable[P, typing.Any], *args: P.args, **kwargs: P.kwargs) -> None:
+ task = BackgroundTask(func, *args, **kwargs)
+ self.tasks.append(task)
+
+ async def __call__(self) -> None:
+ for task in self.tasks:
+ await task()
diff --git a/.venv/lib/python3.12/site-packages/starlette/concurrency.py b/.venv/lib/python3.12/site-packages/starlette/concurrency.py
new file mode 100644
index 00000000..494f3420
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/starlette/concurrency.py
@@ -0,0 +1,62 @@
+from __future__ import annotations
+
+import functools
+import sys
+import typing
+import warnings
+
+import anyio.to_thread
+
+if sys.version_info >= (3, 10): # pragma: no cover
+ from typing import ParamSpec
+else: # pragma: no cover
+ from typing_extensions import ParamSpec
+
+P = ParamSpec("P")
+T = typing.TypeVar("T")
+
+
+async def run_until_first_complete(*args: tuple[typing.Callable, dict]) -> None: # type: ignore[type-arg]
+ warnings.warn(
+ "run_until_first_complete is deprecated and will be removed in a future version.",
+ DeprecationWarning,
+ )
+
+ async with anyio.create_task_group() as task_group:
+
+ async def run(func: typing.Callable[[], typing.Coroutine]) -> None: # type: ignore[type-arg]
+ await func()
+ task_group.cancel_scope.cancel()
+
+ for func, kwargs in args:
+ task_group.start_soon(run, functools.partial(func, **kwargs))
+
+
+async def run_in_threadpool(func: typing.Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T:
+ func = functools.partial(func, *args, **kwargs)
+ return await anyio.to_thread.run_sync(func)
+
+
+class _StopIteration(Exception):
+ pass
+
+
+def _next(iterator: typing.Iterator[T]) -> T:
+ # We can't raise `StopIteration` from within the threadpool iterator
+ # and catch it outside that context, so we coerce them into a different
+ # exception type.
+ try:
+ return next(iterator)
+ except StopIteration:
+ raise _StopIteration
+
+
+async def iterate_in_threadpool(
+ iterator: typing.Iterable[T],
+) -> typing.AsyncIterator[T]:
+ as_iterator = iter(iterator)
+ while True:
+ try:
+ yield await anyio.to_thread.run_sync(_next, as_iterator)
+ except _StopIteration:
+ break
diff --git a/.venv/lib/python3.12/site-packages/starlette/config.py b/.venv/lib/python3.12/site-packages/starlette/config.py
new file mode 100644
index 00000000..ca15c564
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/starlette/config.py
@@ -0,0 +1,138 @@
+from __future__ import annotations
+
+import os
+import typing
+import warnings
+from pathlib import Path
+
+
+class undefined:
+ pass
+
+
+class EnvironError(Exception):
+ pass
+
+
+class Environ(typing.MutableMapping[str, str]):
+ def __init__(self, environ: typing.MutableMapping[str, str] = os.environ):
+ self._environ = environ
+ self._has_been_read: set[str] = set()
+
+ def __getitem__(self, key: str) -> str:
+ self._has_been_read.add(key)
+ return self._environ.__getitem__(key)
+
+ def __setitem__(self, key: str, value: str) -> None:
+ if key in self._has_been_read:
+ raise EnvironError(f"Attempting to set environ['{key}'], but the value has already been read.")
+ self._environ.__setitem__(key, value)
+
+ def __delitem__(self, key: str) -> None:
+ if key in self._has_been_read:
+ raise EnvironError(f"Attempting to delete environ['{key}'], but the value has already been read.")
+ self._environ.__delitem__(key)
+
+ def __iter__(self) -> typing.Iterator[str]:
+ return iter(self._environ)
+
+ def __len__(self) -> int:
+ return len(self._environ)
+
+
+environ = Environ()
+
+T = typing.TypeVar("T")
+
+
+class Config:
+ def __init__(
+ self,
+ env_file: str | Path | None = None,
+ environ: typing.Mapping[str, str] = environ,
+ env_prefix: str = "",
+ ) -> None:
+ self.environ = environ
+ self.env_prefix = env_prefix
+ self.file_values: dict[str, str] = {}
+ if env_file is not None:
+ if not os.path.isfile(env_file):
+ warnings.warn(f"Config file '{env_file}' not found.")
+ else:
+ self.file_values = self._read_file(env_file)
+
+ @typing.overload
+ def __call__(self, key: str, *, default: None) -> str | None: ...
+
+ @typing.overload
+ def __call__(self, key: str, cast: type[T], default: T = ...) -> T: ...
+
+ @typing.overload
+ def __call__(self, key: str, cast: type[str] = ..., default: str = ...) -> str: ...
+
+ @typing.overload
+ def __call__(
+ self,
+ key: str,
+ cast: typing.Callable[[typing.Any], T] = ...,
+ default: typing.Any = ...,
+ ) -> T: ...
+
+ @typing.overload
+ def __call__(self, key: str, cast: type[str] = ..., default: T = ...) -> T | str: ...
+
+ def __call__(
+ self,
+ key: str,
+ cast: typing.Callable[[typing.Any], typing.Any] | None = None,
+ default: typing.Any = undefined,
+ ) -> typing.Any:
+ return self.get(key, cast, default)
+
+ def get(
+ self,
+ key: str,
+ cast: typing.Callable[[typing.Any], typing.Any] | None = None,
+ default: typing.Any = undefined,
+ ) -> typing.Any:
+ key = self.env_prefix + key
+ if key in self.environ:
+ value = self.environ[key]
+ return self._perform_cast(key, value, cast)
+ if key in self.file_values:
+ value = self.file_values[key]
+ return self._perform_cast(key, value, cast)
+ if default is not undefined:
+ return self._perform_cast(key, default, cast)
+ raise KeyError(f"Config '{key}' is missing, and has no default.")
+
+ def _read_file(self, file_name: str | Path) -> dict[str, str]:
+ file_values: dict[str, str] = {}
+ with open(file_name) as input_file:
+ for line in input_file.readlines():
+ line = line.strip()
+ if "=" in line and not line.startswith("#"):
+ key, value = line.split("=", 1)
+ key = key.strip()
+ value = value.strip().strip("\"'")
+ file_values[key] = value
+ return file_values
+
+ def _perform_cast(
+ self,
+ key: str,
+ value: typing.Any,
+ cast: typing.Callable[[typing.Any], typing.Any] | None = None,
+ ) -> typing.Any:
+ if cast is None or value is None:
+ return value
+ elif cast is bool and isinstance(value, str):
+ mapping = {"true": True, "1": True, "false": False, "0": False}
+ value = value.lower()
+ if value not in mapping:
+ raise ValueError(f"Config '{key}' has value '{value}'. Not a valid bool.")
+ return mapping[value]
+ try:
+ return cast(value)
+ except (TypeError, ValueError):
+ raise ValueError(f"Config '{key}' has value '{value}'. Not a valid {cast.__name__}.")
diff --git a/.venv/lib/python3.12/site-packages/starlette/convertors.py b/.venv/lib/python3.12/site-packages/starlette/convertors.py
new file mode 100644
index 00000000..84df87a5
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/starlette/convertors.py
@@ -0,0 +1,89 @@
+from __future__ import annotations
+
+import math
+import typing
+import uuid
+
+T = typing.TypeVar("T")
+
+
+class Convertor(typing.Generic[T]):
+ regex: typing.ClassVar[str] = ""
+
+ def convert(self, value: str) -> T:
+ raise NotImplementedError() # pragma: no cover
+
+ def to_string(self, value: T) -> str:
+ raise NotImplementedError() # pragma: no cover
+
+
+class StringConvertor(Convertor[str]):
+ regex = "[^/]+"
+
+ def convert(self, value: str) -> str:
+ return value
+
+ def to_string(self, value: str) -> str:
+ value = str(value)
+ assert "/" not in value, "May not contain path separators"
+ assert value, "Must not be empty"
+ return value
+
+
+class PathConvertor(Convertor[str]):
+ regex = ".*"
+
+ def convert(self, value: str) -> str:
+ return str(value)
+
+ def to_string(self, value: str) -> str:
+ return str(value)
+
+
+class IntegerConvertor(Convertor[int]):
+ regex = "[0-9]+"
+
+ def convert(self, value: str) -> int:
+ return int(value)
+
+ def to_string(self, value: int) -> str:
+ value = int(value)
+ assert value >= 0, "Negative integers are not supported"
+ return str(value)
+
+
+class FloatConvertor(Convertor[float]):
+ regex = r"[0-9]+(\.[0-9]+)?"
+
+ def convert(self, value: str) -> float:
+ return float(value)
+
+ def to_string(self, value: float) -> str:
+ value = float(value)
+ assert value >= 0.0, "Negative floats are not supported"
+ assert not math.isnan(value), "NaN values are not supported"
+ assert not math.isinf(value), "Infinite values are not supported"
+ return ("%0.20f" % value).rstrip("0").rstrip(".")
+
+
+class UUIDConvertor(Convertor[uuid.UUID]):
+ regex = "[0-9a-fA-F]{8}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{12}"
+
+ def convert(self, value: str) -> uuid.UUID:
+ return uuid.UUID(value)
+
+ def to_string(self, value: uuid.UUID) -> str:
+ return str(value)
+
+
+CONVERTOR_TYPES: dict[str, Convertor[typing.Any]] = {
+ "str": StringConvertor(),
+ "path": PathConvertor(),
+ "int": IntegerConvertor(),
+ "float": FloatConvertor(),
+ "uuid": UUIDConvertor(),
+}
+
+
+def register_url_convertor(key: str, convertor: Convertor[typing.Any]) -> None:
+ CONVERTOR_TYPES[key] = convertor
diff --git a/.venv/lib/python3.12/site-packages/starlette/datastructures.py b/.venv/lib/python3.12/site-packages/starlette/datastructures.py
new file mode 100644
index 00000000..f5d74d25
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/starlette/datastructures.py
@@ -0,0 +1,674 @@
+from __future__ import annotations
+
+import typing
+from shlex import shlex
+from urllib.parse import SplitResult, parse_qsl, urlencode, urlsplit
+
+from starlette.concurrency import run_in_threadpool
+from starlette.types import Scope
+
+
+class Address(typing.NamedTuple):
+ host: str
+ port: int
+
+
+_KeyType = typing.TypeVar("_KeyType")
+# Mapping keys are invariant but their values are covariant since
+# you can only read them
+# that is, you can't do `Mapping[str, Animal]()["fido"] = Dog()`
+_CovariantValueType = typing.TypeVar("_CovariantValueType", covariant=True)
+
+
+class URL:
+ def __init__(
+ self,
+ url: str = "",
+ scope: Scope | None = None,
+ **components: typing.Any,
+ ) -> None:
+ if scope is not None:
+ assert not url, 'Cannot set both "url" and "scope".'
+ assert not components, 'Cannot set both "scope" and "**components".'
+ scheme = scope.get("scheme", "http")
+ server = scope.get("server", None)
+ path = scope["path"]
+ query_string = scope.get("query_string", b"")
+
+ host_header = None
+ for key, value in scope["headers"]:
+ if key == b"host":
+ host_header = value.decode("latin-1")
+ break
+
+ if host_header is not None:
+ url = f"{scheme}://{host_header}{path}"
+ elif server is None:
+ url = path
+ else:
+ host, port = server
+ default_port = {"http": 80, "https": 443, "ws": 80, "wss": 443}[scheme]
+ if port == default_port:
+ url = f"{scheme}://{host}{path}"
+ else:
+ url = f"{scheme}://{host}:{port}{path}"
+
+ if query_string:
+ url += "?" + query_string.decode()
+ elif components:
+ assert not url, 'Cannot set both "url" and "**components".'
+ url = URL("").replace(**components).components.geturl()
+
+ self._url = url
+
+ @property
+ def components(self) -> SplitResult:
+ if not hasattr(self, "_components"):
+ self._components = urlsplit(self._url)
+ return self._components
+
+ @property
+ def scheme(self) -> str:
+ return self.components.scheme
+
+ @property
+ def netloc(self) -> str:
+ return self.components.netloc
+
+ @property
+ def path(self) -> str:
+ return self.components.path
+
+ @property
+ def query(self) -> str:
+ return self.components.query
+
+ @property
+ def fragment(self) -> str:
+ return self.components.fragment
+
+ @property
+ def username(self) -> None | str:
+ return self.components.username
+
+ @property
+ def password(self) -> None | str:
+ return self.components.password
+
+ @property
+ def hostname(self) -> None | str:
+ return self.components.hostname
+
+ @property
+ def port(self) -> int | None:
+ return self.components.port
+
+ @property
+ def is_secure(self) -> bool:
+ return self.scheme in ("https", "wss")
+
+ def replace(self, **kwargs: typing.Any) -> URL:
+ if "username" in kwargs or "password" in kwargs or "hostname" in kwargs or "port" in kwargs:
+ hostname = kwargs.pop("hostname", None)
+ port = kwargs.pop("port", self.port)
+ username = kwargs.pop("username", self.username)
+ password = kwargs.pop("password", self.password)
+
+ if hostname is None:
+ netloc = self.netloc
+ _, _, hostname = netloc.rpartition("@")
+
+ if hostname[-1] != "]":
+ hostname = hostname.rsplit(":", 1)[0]
+
+ netloc = hostname
+ if port is not None:
+ netloc += f":{port}"
+ if username is not None:
+ userpass = username
+ if password is not None:
+ userpass += f":{password}"
+ netloc = f"{userpass}@{netloc}"
+
+ kwargs["netloc"] = netloc
+
+ components = self.components._replace(**kwargs)
+ return self.__class__(components.geturl())
+
+ def include_query_params(self, **kwargs: typing.Any) -> URL:
+ params = MultiDict(parse_qsl(self.query, keep_blank_values=True))
+ params.update({str(key): str(value) for key, value in kwargs.items()})
+ query = urlencode(params.multi_items())
+ return self.replace(query=query)
+
+ def replace_query_params(self, **kwargs: typing.Any) -> URL:
+ query = urlencode([(str(key), str(value)) for key, value in kwargs.items()])
+ return self.replace(query=query)
+
+ def remove_query_params(self, keys: str | typing.Sequence[str]) -> URL:
+ if isinstance(keys, str):
+ keys = [keys]
+ params = MultiDict(parse_qsl(self.query, keep_blank_values=True))
+ for key in keys:
+ params.pop(key, None)
+ query = urlencode(params.multi_items())
+ return self.replace(query=query)
+
+ def __eq__(self, other: typing.Any) -> bool:
+ return str(self) == str(other)
+
+ def __str__(self) -> str:
+ return self._url
+
+ def __repr__(self) -> str:
+ url = str(self)
+ if self.password:
+ url = str(self.replace(password="********"))
+ return f"{self.__class__.__name__}({repr(url)})"
+
+
+class URLPath(str):
+ """
+ A URL path string that may also hold an associated protocol and/or host.
+ Used by the routing to return `url_path_for` matches.
+ """
+
+ def __new__(cls, path: str, protocol: str = "", host: str = "") -> URLPath:
+ assert protocol in ("http", "websocket", "")
+ return str.__new__(cls, path)
+
+ def __init__(self, path: str, protocol: str = "", host: str = "") -> None:
+ self.protocol = protocol
+ self.host = host
+
+ def make_absolute_url(self, base_url: str | URL) -> URL:
+ if isinstance(base_url, str):
+ base_url = URL(base_url)
+ if self.protocol:
+ scheme = {
+ "http": {True: "https", False: "http"},
+ "websocket": {True: "wss", False: "ws"},
+ }[self.protocol][base_url.is_secure]
+ else:
+ scheme = base_url.scheme
+
+ netloc = self.host or base_url.netloc
+ path = base_url.path.rstrip("/") + str(self)
+ return URL(scheme=scheme, netloc=netloc, path=path)
+
+
+class Secret:
+ """
+ Holds a string value that should not be revealed in tracebacks etc.
+ You should cast the value to `str` at the point it is required.
+ """
+
+ def __init__(self, value: str):
+ self._value = value
+
+ def __repr__(self) -> str:
+ class_name = self.__class__.__name__
+ return f"{class_name}('**********')"
+
+ def __str__(self) -> str:
+ return self._value
+
+ def __bool__(self) -> bool:
+ return bool(self._value)
+
+
+class CommaSeparatedStrings(typing.Sequence[str]):
+ def __init__(self, value: str | typing.Sequence[str]):
+ if isinstance(value, str):
+ splitter = shlex(value, posix=True)
+ splitter.whitespace = ","
+ splitter.whitespace_split = True
+ self._items = [item.strip() for item in splitter]
+ else:
+ self._items = list(value)
+
+ def __len__(self) -> int:
+ return len(self._items)
+
+ def __getitem__(self, index: int | slice) -> typing.Any:
+ return self._items[index]
+
+ def __iter__(self) -> typing.Iterator[str]:
+ return iter(self._items)
+
+ def __repr__(self) -> str:
+ class_name = self.__class__.__name__
+ items = [item for item in self]
+ return f"{class_name}({items!r})"
+
+ def __str__(self) -> str:
+ return ", ".join(repr(item) for item in self)
+
+
+class ImmutableMultiDict(typing.Mapping[_KeyType, _CovariantValueType]):
+ _dict: dict[_KeyType, _CovariantValueType]
+
+ def __init__(
+ self,
+ *args: ImmutableMultiDict[_KeyType, _CovariantValueType]
+ | typing.Mapping[_KeyType, _CovariantValueType]
+ | typing.Iterable[tuple[_KeyType, _CovariantValueType]],
+ **kwargs: typing.Any,
+ ) -> None:
+ assert len(args) < 2, "Too many arguments."
+
+ value: typing.Any = args[0] if args else []
+ if kwargs:
+ value = ImmutableMultiDict(value).multi_items() + ImmutableMultiDict(kwargs).multi_items()
+
+ if not value:
+ _items: list[tuple[typing.Any, typing.Any]] = []
+ elif hasattr(value, "multi_items"):
+ value = typing.cast(ImmutableMultiDict[_KeyType, _CovariantValueType], value)
+ _items = list(value.multi_items())
+ elif hasattr(value, "items"):
+ value = typing.cast(typing.Mapping[_KeyType, _CovariantValueType], value)
+ _items = list(value.items())
+ else:
+ value = typing.cast("list[tuple[typing.Any, typing.Any]]", value)
+ _items = list(value)
+
+ self._dict = {k: v for k, v in _items}
+ self._list = _items
+
+ def getlist(self, key: typing.Any) -> list[_CovariantValueType]:
+ return [item_value for item_key, item_value in self._list if item_key == key]
+
+ def keys(self) -> typing.KeysView[_KeyType]:
+ return self._dict.keys()
+
+ def values(self) -> typing.ValuesView[_CovariantValueType]:
+ return self._dict.values()
+
+ def items(self) -> typing.ItemsView[_KeyType, _CovariantValueType]:
+ return self._dict.items()
+
+ def multi_items(self) -> list[tuple[_KeyType, _CovariantValueType]]:
+ return list(self._list)
+
+ def __getitem__(self, key: _KeyType) -> _CovariantValueType:
+ return self._dict[key]
+
+ def __contains__(self, key: typing.Any) -> bool:
+ return key in self._dict
+
+ def __iter__(self) -> typing.Iterator[_KeyType]:
+ return iter(self.keys())
+
+ def __len__(self) -> int:
+ return len(self._dict)
+
+ def __eq__(self, other: typing.Any) -> bool:
+ if not isinstance(other, self.__class__):
+ return False
+ return sorted(self._list) == sorted(other._list)
+
+ def __repr__(self) -> str:
+ class_name = self.__class__.__name__
+ items = self.multi_items()
+ return f"{class_name}({items!r})"
+
+
+class MultiDict(ImmutableMultiDict[typing.Any, typing.Any]):
+ def __setitem__(self, key: typing.Any, value: typing.Any) -> None:
+ self.setlist(key, [value])
+
+ def __delitem__(self, key: typing.Any) -> None:
+ self._list = [(k, v) for k, v in self._list if k != key]
+ del self._dict[key]
+
+ def pop(self, key: typing.Any, default: typing.Any = None) -> typing.Any:
+ self._list = [(k, v) for k, v in self._list if k != key]
+ return self._dict.pop(key, default)
+
+ def popitem(self) -> tuple[typing.Any, typing.Any]:
+ key, value = self._dict.popitem()
+ self._list = [(k, v) for k, v in self._list if k != key]
+ return key, value
+
+ def poplist(self, key: typing.Any) -> list[typing.Any]:
+ values = [v for k, v in self._list if k == key]
+ self.pop(key)
+ return values
+
+ def clear(self) -> None:
+ self._dict.clear()
+ self._list.clear()
+
+ def setdefault(self, key: typing.Any, default: typing.Any = None) -> typing.Any:
+ if key not in self:
+ self._dict[key] = default
+ self._list.append((key, default))
+
+ return self[key]
+
+ def setlist(self, key: typing.Any, values: list[typing.Any]) -> None:
+ if not values:
+ self.pop(key, None)
+ else:
+ existing_items = [(k, v) for (k, v) in self._list if k != key]
+ self._list = existing_items + [(key, value) for value in values]
+ self._dict[key] = values[-1]
+
+ def append(self, key: typing.Any, value: typing.Any) -> None:
+ self._list.append((key, value))
+ self._dict[key] = value
+
+ def update(
+ self,
+ *args: MultiDict | typing.Mapping[typing.Any, typing.Any] | list[tuple[typing.Any, typing.Any]],
+ **kwargs: typing.Any,
+ ) -> None:
+ value = MultiDict(*args, **kwargs)
+ existing_items = [(k, v) for (k, v) in self._list if k not in value.keys()]
+ self._list = existing_items + value.multi_items()
+ self._dict.update(value)
+
+
+class QueryParams(ImmutableMultiDict[str, str]):
+ """
+ An immutable multidict.
+ """
+
+ def __init__(
+ self,
+ *args: ImmutableMultiDict[typing.Any, typing.Any]
+ | typing.Mapping[typing.Any, typing.Any]
+ | list[tuple[typing.Any, typing.Any]]
+ | str
+ | bytes,
+ **kwargs: typing.Any,
+ ) -> None:
+ assert len(args) < 2, "Too many arguments."
+
+ value = args[0] if args else []
+
+ if isinstance(value, str):
+ super().__init__(parse_qsl(value, keep_blank_values=True), **kwargs)
+ elif isinstance(value, bytes):
+ super().__init__(parse_qsl(value.decode("latin-1"), keep_blank_values=True), **kwargs)
+ else:
+ super().__init__(*args, **kwargs) # type: ignore[arg-type]
+ self._list = [(str(k), str(v)) for k, v in self._list]
+ self._dict = {str(k): str(v) for k, v in self._dict.items()}
+
+ def __str__(self) -> str:
+ return urlencode(self._list)
+
+ def __repr__(self) -> str:
+ class_name = self.__class__.__name__
+ query_string = str(self)
+ return f"{class_name}({query_string!r})"
+
+
+class UploadFile:
+ """
+ An uploaded file included as part of the request data.
+ """
+
+ def __init__(
+ self,
+ file: typing.BinaryIO,
+ *,
+ size: int | None = None,
+ filename: str | None = None,
+ headers: Headers | None = None,
+ ) -> None:
+ self.filename = filename
+ self.file = file
+ self.size = size
+ self.headers = headers or Headers()
+
+ @property
+ def content_type(self) -> str | None:
+ return self.headers.get("content-type", None)
+
+ @property
+ def _in_memory(self) -> bool:
+ # check for SpooledTemporaryFile._rolled
+ rolled_to_disk = getattr(self.file, "_rolled", True)
+ return not rolled_to_disk
+
+ async def write(self, data: bytes) -> None:
+ if self.size is not None:
+ self.size += len(data)
+
+ if self._in_memory:
+ self.file.write(data)
+ else:
+ await run_in_threadpool(self.file.write, data)
+
+ async def read(self, size: int = -1) -> bytes:
+ if self._in_memory:
+ return self.file.read(size)
+ return await run_in_threadpool(self.file.read, size)
+
+ async def seek(self, offset: int) -> None:
+ if self._in_memory:
+ self.file.seek(offset)
+ else:
+ await run_in_threadpool(self.file.seek, offset)
+
+ async def close(self) -> None:
+ if self._in_memory:
+ self.file.close()
+ else:
+ await run_in_threadpool(self.file.close)
+
+ def __repr__(self) -> str:
+ return f"{self.__class__.__name__}(filename={self.filename!r}, size={self.size!r}, headers={self.headers!r})"
+
+
+class FormData(ImmutableMultiDict[str, typing.Union[UploadFile, str]]):
+ """
+ An immutable multidict, containing both file uploads and text input.
+ """
+
+ def __init__(
+ self,
+ *args: FormData | typing.Mapping[str, str | UploadFile] | list[tuple[str, str | UploadFile]],
+ **kwargs: str | UploadFile,
+ ) -> None:
+ super().__init__(*args, **kwargs)
+
+ async def close(self) -> None:
+ for key, value in self.multi_items():
+ if isinstance(value, UploadFile):
+ await value.close()
+
+
+class Headers(typing.Mapping[str, str]):
+ """
+ An immutable, case-insensitive multidict.
+ """
+
+ def __init__(
+ self,
+ headers: typing.Mapping[str, str] | None = None,
+ raw: list[tuple[bytes, bytes]] | None = None,
+ scope: typing.MutableMapping[str, typing.Any] | None = None,
+ ) -> None:
+ self._list: list[tuple[bytes, bytes]] = []
+ if headers is not None:
+ assert raw is None, 'Cannot set both "headers" and "raw".'
+ assert scope is None, 'Cannot set both "headers" and "scope".'
+ self._list = [(key.lower().encode("latin-1"), value.encode("latin-1")) for key, value in headers.items()]
+ elif raw is not None:
+ assert scope is None, 'Cannot set both "raw" and "scope".'
+ self._list = raw
+ elif scope is not None:
+ # scope["headers"] isn't necessarily a list
+ # it might be a tuple or other iterable
+ self._list = scope["headers"] = list(scope["headers"])
+
+ @property
+ def raw(self) -> list[tuple[bytes, bytes]]:
+ return list(self._list)
+
+ def keys(self) -> list[str]: # type: ignore[override]
+ return [key.decode("latin-1") for key, value in self._list]
+
+ def values(self) -> list[str]: # type: ignore[override]
+ return [value.decode("latin-1") for key, value in self._list]
+
+ def items(self) -> list[tuple[str, str]]: # type: ignore[override]
+ return [(key.decode("latin-1"), value.decode("latin-1")) for key, value in self._list]
+
+ def getlist(self, key: str) -> list[str]:
+ get_header_key = key.lower().encode("latin-1")
+ return [item_value.decode("latin-1") for item_key, item_value in self._list if item_key == get_header_key]
+
+ def mutablecopy(self) -> MutableHeaders:
+ return MutableHeaders(raw=self._list[:])
+
+ def __getitem__(self, key: str) -> str:
+ get_header_key = key.lower().encode("latin-1")
+ for header_key, header_value in self._list:
+ if header_key == get_header_key:
+ return header_value.decode("latin-1")
+ raise KeyError(key)
+
+ def __contains__(self, key: typing.Any) -> bool:
+ get_header_key = key.lower().encode("latin-1")
+ for header_key, header_value in self._list:
+ if header_key == get_header_key:
+ return True
+ return False
+
+ def __iter__(self) -> typing.Iterator[typing.Any]:
+ return iter(self.keys())
+
+ def __len__(self) -> int:
+ return len(self._list)
+
+ def __eq__(self, other: typing.Any) -> bool:
+ if not isinstance(other, Headers):
+ return False
+ return sorted(self._list) == sorted(other._list)
+
+ def __repr__(self) -> str:
+ class_name = self.__class__.__name__
+ as_dict = dict(self.items())
+ if len(as_dict) == len(self):
+ return f"{class_name}({as_dict!r})"
+ return f"{class_name}(raw={self.raw!r})"
+
+
+class MutableHeaders(Headers):
+ def __setitem__(self, key: str, value: str) -> None:
+ """
+ Set the header `key` to `value`, removing any duplicate entries.
+ Retains insertion order.
+ """
+ set_key = key.lower().encode("latin-1")
+ set_value = value.encode("latin-1")
+
+ found_indexes: list[int] = []
+ for idx, (item_key, item_value) in enumerate(self._list):
+ if item_key == set_key:
+ found_indexes.append(idx)
+
+ for idx in reversed(found_indexes[1:]):
+ del self._list[idx]
+
+ if found_indexes:
+ idx = found_indexes[0]
+ self._list[idx] = (set_key, set_value)
+ else:
+ self._list.append((set_key, set_value))
+
+ def __delitem__(self, key: str) -> None:
+ """
+ Remove the header `key`.
+ """
+ del_key = key.lower().encode("latin-1")
+
+ pop_indexes: list[int] = []
+ for idx, (item_key, item_value) in enumerate(self._list):
+ if item_key == del_key:
+ pop_indexes.append(idx)
+
+ for idx in reversed(pop_indexes):
+ del self._list[idx]
+
+ def __ior__(self, other: typing.Mapping[str, str]) -> MutableHeaders:
+ if not isinstance(other, typing.Mapping):
+ raise TypeError(f"Expected a mapping but got {other.__class__.__name__}")
+ self.update(other)
+ return self
+
+ def __or__(self, other: typing.Mapping[str, str]) -> MutableHeaders:
+ if not isinstance(other, typing.Mapping):
+ raise TypeError(f"Expected a mapping but got {other.__class__.__name__}")
+ new = self.mutablecopy()
+ new.update(other)
+ return new
+
+ @property
+ def raw(self) -> list[tuple[bytes, bytes]]:
+ return self._list
+
+ def setdefault(self, key: str, value: str) -> str:
+ """
+ If the header `key` does not exist, then set it to `value`.
+ Returns the header value.
+ """
+ set_key = key.lower().encode("latin-1")
+ set_value = value.encode("latin-1")
+
+ for idx, (item_key, item_value) in enumerate(self._list):
+ if item_key == set_key:
+ return item_value.decode("latin-1")
+ self._list.append((set_key, set_value))
+ return value
+
+ def update(self, other: typing.Mapping[str, str]) -> None:
+ for key, val in other.items():
+ self[key] = val
+
+ def append(self, key: str, value: str) -> None:
+ """
+ Append a header, preserving any duplicate entries.
+ """
+ append_key = key.lower().encode("latin-1")
+ append_value = value.encode("latin-1")
+ self._list.append((append_key, append_value))
+
+ def add_vary_header(self, vary: str) -> None:
+ existing = self.get("vary")
+ if existing is not None:
+ vary = ", ".join([existing, vary])
+ self["vary"] = vary
+
+
+class State:
+ """
+ An object that can be used to store arbitrary state.
+
+ Used for `request.state` and `app.state`.
+ """
+
+ _state: dict[str, typing.Any]
+
+ def __init__(self, state: dict[str, typing.Any] | None = None):
+ if state is None:
+ state = {}
+ super().__setattr__("_state", state)
+
+ def __setattr__(self, key: typing.Any, value: typing.Any) -> None:
+ self._state[key] = value
+
+ def __getattr__(self, key: typing.Any) -> typing.Any:
+ try:
+ return self._state[key]
+ except KeyError:
+ message = "'{}' object has no attribute '{}'"
+ raise AttributeError(message.format(self.__class__.__name__, key))
+
+ def __delattr__(self, key: typing.Any) -> None:
+ del self._state[key]
diff --git a/.venv/lib/python3.12/site-packages/starlette/endpoints.py b/.venv/lib/python3.12/site-packages/starlette/endpoints.py
new file mode 100644
index 00000000..10769026
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/starlette/endpoints.py
@@ -0,0 +1,122 @@
+from __future__ import annotations
+
+import json
+import typing
+
+from starlette import status
+from starlette._utils import is_async_callable
+from starlette.concurrency import run_in_threadpool
+from starlette.exceptions import HTTPException
+from starlette.requests import Request
+from starlette.responses import PlainTextResponse, Response
+from starlette.types import Message, Receive, Scope, Send
+from starlette.websockets import WebSocket
+
+
+class HTTPEndpoint:
+ def __init__(self, scope: Scope, receive: Receive, send: Send) -> None:
+ assert scope["type"] == "http"
+ self.scope = scope
+ self.receive = receive
+ self.send = send
+ self._allowed_methods = [
+ method
+ for method in ("GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
+ if getattr(self, method.lower(), None) is not None
+ ]
+
+ def __await__(self) -> typing.Generator[typing.Any, None, None]:
+ return self.dispatch().__await__()
+
+ async def dispatch(self) -> None:
+ request = Request(self.scope, receive=self.receive)
+ handler_name = "get" if request.method == "HEAD" and not hasattr(self, "head") else request.method.lower()
+
+ handler: typing.Callable[[Request], typing.Any] = getattr(self, handler_name, self.method_not_allowed)
+ is_async = is_async_callable(handler)
+ if is_async:
+ response = await handler(request)
+ else:
+ response = await run_in_threadpool(handler, request)
+ await response(self.scope, self.receive, self.send)
+
+ async def method_not_allowed(self, request: Request) -> Response:
+ # If we're running inside a starlette application then raise an
+ # exception, so that the configurable exception handler can deal with
+ # returning the response. For plain ASGI apps, just return the response.
+ headers = {"Allow": ", ".join(self._allowed_methods)}
+ if "app" in self.scope:
+ raise HTTPException(status_code=405, headers=headers)
+ return PlainTextResponse("Method Not Allowed", status_code=405, headers=headers)
+
+
+class WebSocketEndpoint:
+ encoding: str | None = None # May be "text", "bytes", or "json".
+
+ def __init__(self, scope: Scope, receive: Receive, send: Send) -> None:
+ assert scope["type"] == "websocket"
+ self.scope = scope
+ self.receive = receive
+ self.send = send
+
+ def __await__(self) -> typing.Generator[typing.Any, None, None]:
+ return self.dispatch().__await__()
+
+ async def dispatch(self) -> None:
+ websocket = WebSocket(self.scope, receive=self.receive, send=self.send)
+ await self.on_connect(websocket)
+
+ close_code = status.WS_1000_NORMAL_CLOSURE
+
+ try:
+ while True:
+ message = await websocket.receive()
+ if message["type"] == "websocket.receive":
+ data = await self.decode(websocket, message)
+ await self.on_receive(websocket, data)
+ elif message["type"] == "websocket.disconnect": # pragma: no branch
+ close_code = int(message.get("code") or status.WS_1000_NORMAL_CLOSURE)
+ break
+ except Exception as exc:
+ close_code = status.WS_1011_INTERNAL_ERROR
+ raise exc
+ finally:
+ await self.on_disconnect(websocket, close_code)
+
+ async def decode(self, websocket: WebSocket, message: Message) -> typing.Any:
+ if self.encoding == "text":
+ if "text" not in message:
+ await websocket.close(code=status.WS_1003_UNSUPPORTED_DATA)
+ raise RuntimeError("Expected text websocket messages, but got bytes")
+ return message["text"]
+
+ elif self.encoding == "bytes":
+ if "bytes" not in message:
+ await websocket.close(code=status.WS_1003_UNSUPPORTED_DATA)
+ raise RuntimeError("Expected bytes websocket messages, but got text")
+ return message["bytes"]
+
+ elif self.encoding == "json":
+ if message.get("text") is not None:
+ text = message["text"]
+ else:
+ text = message["bytes"].decode("utf-8")
+
+ try:
+ return json.loads(text)
+ except json.decoder.JSONDecodeError:
+ await websocket.close(code=status.WS_1003_UNSUPPORTED_DATA)
+ raise RuntimeError("Malformed JSON data received.")
+
+ assert self.encoding is None, f"Unsupported 'encoding' attribute {self.encoding}"
+ return message["text"] if message.get("text") else message["bytes"]
+
+ async def on_connect(self, websocket: WebSocket) -> None:
+ """Override to handle an incoming websocket connection"""
+ await websocket.accept()
+
+ async def on_receive(self, websocket: WebSocket, data: typing.Any) -> None:
+ """Override to handle an incoming websocket message"""
+
+ async def on_disconnect(self, websocket: WebSocket, close_code: int) -> None:
+ """Override to handle a disconnecting websocket"""
diff --git a/.venv/lib/python3.12/site-packages/starlette/exceptions.py b/.venv/lib/python3.12/site-packages/starlette/exceptions.py
new file mode 100644
index 00000000..9ad3527b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/starlette/exceptions.py
@@ -0,0 +1,33 @@
+from __future__ import annotations
+
+import http
+from collections.abc import Mapping
+
+
+class HTTPException(Exception):
+ def __init__(self, status_code: int, detail: str | None = None, headers: Mapping[str, str] | None = None) -> None:
+ if detail is None:
+ detail = http.HTTPStatus(status_code).phrase
+ self.status_code = status_code
+ self.detail = detail
+ self.headers = headers
+
+ def __str__(self) -> str:
+ return f"{self.status_code}: {self.detail}"
+
+ def __repr__(self) -> str:
+ class_name = self.__class__.__name__
+ return f"{class_name}(status_code={self.status_code!r}, detail={self.detail!r})"
+
+
+class WebSocketException(Exception):
+ def __init__(self, code: int, reason: str | None = None) -> None:
+ self.code = code
+ self.reason = reason or ""
+
+ def __str__(self) -> str:
+ return f"{self.code}: {self.reason}"
+
+ def __repr__(self) -> str:
+ class_name = self.__class__.__name__
+ return f"{class_name}(code={self.code!r}, reason={self.reason!r})"
diff --git a/.venv/lib/python3.12/site-packages/starlette/formparsers.py b/.venv/lib/python3.12/site-packages/starlette/formparsers.py
new file mode 100644
index 00000000..4551d688
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/starlette/formparsers.py
@@ -0,0 +1,275 @@
+from __future__ import annotations
+
+import typing
+from dataclasses import dataclass, field
+from enum import Enum
+from tempfile import SpooledTemporaryFile
+from urllib.parse import unquote_plus
+
+from starlette.datastructures import FormData, Headers, UploadFile
+
+if typing.TYPE_CHECKING:
+ import python_multipart as multipart
+ from python_multipart.multipart import MultipartCallbacks, QuerystringCallbacks, parse_options_header
+else:
+ try:
+ try:
+ import python_multipart as multipart
+ from python_multipart.multipart import parse_options_header
+ except ModuleNotFoundError: # pragma: no cover
+ import multipart
+ from multipart.multipart import parse_options_header
+ except ModuleNotFoundError: # pragma: no cover
+ multipart = None
+ parse_options_header = None
+
+
+class FormMessage(Enum):
+ FIELD_START = 1
+ FIELD_NAME = 2
+ FIELD_DATA = 3
+ FIELD_END = 4
+ END = 5
+
+
+@dataclass
+class MultipartPart:
+ content_disposition: bytes | None = None
+ field_name: str = ""
+ data: bytearray = field(default_factory=bytearray)
+ file: UploadFile | None = None
+ item_headers: list[tuple[bytes, bytes]] = field(default_factory=list)
+
+
+def _user_safe_decode(src: bytes | bytearray, codec: str) -> str:
+ try:
+ return src.decode(codec)
+ except (UnicodeDecodeError, LookupError):
+ return src.decode("latin-1")
+
+
+class MultiPartException(Exception):
+ def __init__(self, message: str) -> None:
+ self.message = message
+
+
+class FormParser:
+ def __init__(self, headers: Headers, stream: typing.AsyncGenerator[bytes, None]) -> None:
+ assert multipart is not None, "The `python-multipart` library must be installed to use form parsing."
+ self.headers = headers
+ self.stream = stream
+ self.messages: list[tuple[FormMessage, bytes]] = []
+
+ def on_field_start(self) -> None:
+ message = (FormMessage.FIELD_START, b"")
+ self.messages.append(message)
+
+ def on_field_name(self, data: bytes, start: int, end: int) -> None:
+ message = (FormMessage.FIELD_NAME, data[start:end])
+ self.messages.append(message)
+
+ def on_field_data(self, data: bytes, start: int, end: int) -> None:
+ message = (FormMessage.FIELD_DATA, data[start:end])
+ self.messages.append(message)
+
+ def on_field_end(self) -> None:
+ message = (FormMessage.FIELD_END, b"")
+ self.messages.append(message)
+
+ def on_end(self) -> None:
+ message = (FormMessage.END, b"")
+ self.messages.append(message)
+
+ async def parse(self) -> FormData:
+ # Callbacks dictionary.
+ callbacks: QuerystringCallbacks = {
+ "on_field_start": self.on_field_start,
+ "on_field_name": self.on_field_name,
+ "on_field_data": self.on_field_data,
+ "on_field_end": self.on_field_end,
+ "on_end": self.on_end,
+ }
+
+ # Create the parser.
+ parser = multipart.QuerystringParser(callbacks)
+ field_name = b""
+ field_value = b""
+
+ items: list[tuple[str, str | UploadFile]] = []
+
+ # Feed the parser with data from the request.
+ async for chunk in self.stream:
+ if chunk:
+ parser.write(chunk)
+ else:
+ parser.finalize()
+ messages = list(self.messages)
+ self.messages.clear()
+ for message_type, message_bytes in messages:
+ if message_type == FormMessage.FIELD_START:
+ field_name = b""
+ field_value = b""
+ elif message_type == FormMessage.FIELD_NAME:
+ field_name += message_bytes
+ elif message_type == FormMessage.FIELD_DATA:
+ field_value += message_bytes
+ elif message_type == FormMessage.FIELD_END:
+ name = unquote_plus(field_name.decode("latin-1"))
+ value = unquote_plus(field_value.decode("latin-1"))
+ items.append((name, value))
+
+ return FormData(items)
+
+
+class MultiPartParser:
+ spool_max_size = 1024 * 1024 # 1MB
+ """The maximum size of the spooled temporary file used to store file data."""
+ max_part_size = 1024 * 1024 # 1MB
+ """The maximum size of a part in the multipart request."""
+
+ def __init__(
+ self,
+ headers: Headers,
+ stream: typing.AsyncGenerator[bytes, None],
+ *,
+ max_files: int | float = 1000,
+ max_fields: int | float = 1000,
+ max_part_size: int = 1024 * 1024, # 1MB
+ ) -> None:
+ assert multipart is not None, "The `python-multipart` library must be installed to use form parsing."
+ self.headers = headers
+ self.stream = stream
+ self.max_files = max_files
+ self.max_fields = max_fields
+ self.items: list[tuple[str, str | UploadFile]] = []
+ self._current_files = 0
+ self._current_fields = 0
+ self._current_partial_header_name: bytes = b""
+ self._current_partial_header_value: bytes = b""
+ self._current_part = MultipartPart()
+ self._charset = ""
+ self._file_parts_to_write: list[tuple[MultipartPart, bytes]] = []
+ self._file_parts_to_finish: list[MultipartPart] = []
+ self._files_to_close_on_error: list[SpooledTemporaryFile[bytes]] = []
+ self.max_part_size = max_part_size
+
+ def on_part_begin(self) -> None:
+ self._current_part = MultipartPart()
+
+ def on_part_data(self, data: bytes, start: int, end: int) -> None:
+ message_bytes = data[start:end]
+ if self._current_part.file is None:
+ if len(self._current_part.data) + len(message_bytes) > self.max_part_size:
+ raise MultiPartException(f"Part exceeded maximum size of {int(self.max_part_size / 1024)}KB.")
+ self._current_part.data.extend(message_bytes)
+ else:
+ self._file_parts_to_write.append((self._current_part, message_bytes))
+
+ def on_part_end(self) -> None:
+ if self._current_part.file is None:
+ self.items.append(
+ (
+ self._current_part.field_name,
+ _user_safe_decode(self._current_part.data, self._charset),
+ )
+ )
+ else:
+ self._file_parts_to_finish.append(self._current_part)
+ # The file can be added to the items right now even though it's not
+ # finished yet, because it will be finished in the `parse()` method, before
+ # self.items is used in the return value.
+ self.items.append((self._current_part.field_name, self._current_part.file))
+
+ def on_header_field(self, data: bytes, start: int, end: int) -> None:
+ self._current_partial_header_name += data[start:end]
+
+ def on_header_value(self, data: bytes, start: int, end: int) -> None:
+ self._current_partial_header_value += data[start:end]
+
+ def on_header_end(self) -> None:
+ field = self._current_partial_header_name.lower()
+ if field == b"content-disposition":
+ self._current_part.content_disposition = self._current_partial_header_value
+ self._current_part.item_headers.append((field, self._current_partial_header_value))
+ self._current_partial_header_name = b""
+ self._current_partial_header_value = b""
+
+ def on_headers_finished(self) -> None:
+ disposition, options = parse_options_header(self._current_part.content_disposition)
+ try:
+ self._current_part.field_name = _user_safe_decode(options[b"name"], self._charset)
+ except KeyError:
+ raise MultiPartException('The Content-Disposition header field "name" must be provided.')
+ if b"filename" in options:
+ self._current_files += 1
+ if self._current_files > self.max_files:
+ raise MultiPartException(f"Too many files. Maximum number of files is {self.max_files}.")
+ filename = _user_safe_decode(options[b"filename"], self._charset)
+ tempfile = SpooledTemporaryFile(max_size=self.spool_max_size)
+ self._files_to_close_on_error.append(tempfile)
+ self._current_part.file = UploadFile(
+ file=tempfile, # type: ignore[arg-type]
+ size=0,
+ filename=filename,
+ headers=Headers(raw=self._current_part.item_headers),
+ )
+ else:
+ self._current_fields += 1
+ if self._current_fields > self.max_fields:
+ raise MultiPartException(f"Too many fields. Maximum number of fields is {self.max_fields}.")
+ self._current_part.file = None
+
+ def on_end(self) -> None:
+ pass
+
+ async def parse(self) -> FormData:
+ # Parse the Content-Type header to get the multipart boundary.
+ _, params = parse_options_header(self.headers["Content-Type"])
+ charset = params.get(b"charset", "utf-8")
+ if isinstance(charset, bytes):
+ charset = charset.decode("latin-1")
+ self._charset = charset
+ try:
+ boundary = params[b"boundary"]
+ except KeyError:
+ raise MultiPartException("Missing boundary in multipart.")
+
+ # Callbacks dictionary.
+ callbacks: MultipartCallbacks = {
+ "on_part_begin": self.on_part_begin,
+ "on_part_data": self.on_part_data,
+ "on_part_end": self.on_part_end,
+ "on_header_field": self.on_header_field,
+ "on_header_value": self.on_header_value,
+ "on_header_end": self.on_header_end,
+ "on_headers_finished": self.on_headers_finished,
+ "on_end": self.on_end,
+ }
+
+ # Create the parser.
+ parser = multipart.MultipartParser(boundary, callbacks)
+ try:
+ # Feed the parser with data from the request.
+ async for chunk in self.stream:
+ parser.write(chunk)
+ # Write file data, it needs to use await with the UploadFile methods
+ # that call the corresponding file methods *in a threadpool*,
+ # otherwise, if they were called directly in the callback methods above
+ # (regular, non-async functions), that would block the event loop in
+ # the main thread.
+ for part, data in self._file_parts_to_write:
+ assert part.file # for type checkers
+ await part.file.write(data)
+ for part in self._file_parts_to_finish:
+ assert part.file # for type checkers
+ await part.file.seek(0)
+ self._file_parts_to_write.clear()
+ self._file_parts_to_finish.clear()
+ except MultiPartException as exc:
+ # Close all the files if there was an error.
+ for file in self._files_to_close_on_error:
+ file.close()
+ raise exc
+
+ parser.finalize()
+ return FormData(self.items)
diff --git a/.venv/lib/python3.12/site-packages/starlette/middleware/__init__.py b/.venv/lib/python3.12/site-packages/starlette/middleware/__init__.py
new file mode 100644
index 00000000..b99538a2
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/starlette/middleware/__init__.py
@@ -0,0 +1,42 @@
+from __future__ import annotations
+
+import sys
+from collections.abc import Iterator
+from typing import Any, Protocol
+
+if sys.version_info >= (3, 10): # pragma: no cover
+ from typing import ParamSpec
+else: # pragma: no cover
+ from typing_extensions import ParamSpec
+
+from starlette.types import ASGIApp
+
+P = ParamSpec("P")
+
+
+class _MiddlewareFactory(Protocol[P]):
+ def __call__(self, app: ASGIApp, /, *args: P.args, **kwargs: P.kwargs) -> ASGIApp: ... # pragma: no cover
+
+
+class Middleware:
+ def __init__(
+ self,
+ cls: _MiddlewareFactory[P],
+ *args: P.args,
+ **kwargs: P.kwargs,
+ ) -> None:
+ self.cls = cls
+ self.args = args
+ self.kwargs = kwargs
+
+ def __iter__(self) -> Iterator[Any]:
+ as_tuple = (self.cls, self.args, self.kwargs)
+ return iter(as_tuple)
+
+ def __repr__(self) -> str:
+ class_name = self.__class__.__name__
+ args_strings = [f"{value!r}" for value in self.args]
+ option_strings = [f"{key}={value!r}" for key, value in self.kwargs.items()]
+ name = getattr(self.cls, "__name__", "")
+ args_repr = ", ".join([name] + args_strings + option_strings)
+ return f"{class_name}({args_repr})"
diff --git a/.venv/lib/python3.12/site-packages/starlette/middleware/authentication.py b/.venv/lib/python3.12/site-packages/starlette/middleware/authentication.py
new file mode 100644
index 00000000..8555ee07
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/starlette/middleware/authentication.py
@@ -0,0 +1,52 @@
+from __future__ import annotations
+
+import typing
+
+from starlette.authentication import (
+ AuthCredentials,
+ AuthenticationBackend,
+ AuthenticationError,
+ UnauthenticatedUser,
+)
+from starlette.requests import HTTPConnection
+from starlette.responses import PlainTextResponse, Response
+from starlette.types import ASGIApp, Receive, Scope, Send
+
+
+class AuthenticationMiddleware:
+ def __init__(
+ self,
+ app: ASGIApp,
+ backend: AuthenticationBackend,
+ on_error: typing.Callable[[HTTPConnection, AuthenticationError], Response] | None = None,
+ ) -> None:
+ self.app = app
+ self.backend = backend
+ self.on_error: typing.Callable[[HTTPConnection, AuthenticationError], Response] = (
+ on_error if on_error is not None else self.default_on_error
+ )
+
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
+ if scope["type"] not in ["http", "websocket"]:
+ await self.app(scope, receive, send)
+ return
+
+ conn = HTTPConnection(scope)
+ try:
+ auth_result = await self.backend.authenticate(conn)
+ except AuthenticationError as exc:
+ response = self.on_error(conn, exc)
+ if scope["type"] == "websocket":
+ await send({"type": "websocket.close", "code": 1000})
+ else:
+ await response(scope, receive, send)
+ return
+
+ if auth_result is None:
+ auth_result = AuthCredentials(), UnauthenticatedUser()
+ scope["auth"], scope["user"] = auth_result
+ await self.app(scope, receive, send)
+
+ @staticmethod
+ def default_on_error(conn: HTTPConnection, exc: Exception) -> Response:
+ return PlainTextResponse(str(exc), status_code=400)
diff --git a/.venv/lib/python3.12/site-packages/starlette/middleware/base.py b/.venv/lib/python3.12/site-packages/starlette/middleware/base.py
new file mode 100644
index 00000000..2a59337e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/starlette/middleware/base.py
@@ -0,0 +1,220 @@
+from __future__ import annotations
+
+import typing
+
+import anyio
+
+from starlette._utils import collapse_excgroups
+from starlette.requests import ClientDisconnect, Request
+from starlette.responses import AsyncContentStream, Response
+from starlette.types import ASGIApp, Message, Receive, Scope, Send
+
+RequestResponseEndpoint = typing.Callable[[Request], typing.Awaitable[Response]]
+DispatchFunction = typing.Callable[[Request, RequestResponseEndpoint], typing.Awaitable[Response]]
+T = typing.TypeVar("T")
+
+
+class _CachedRequest(Request):
+ """
+ If the user calls Request.body() from their dispatch function
+ we cache the entire request body in memory and pass that to downstream middlewares,
+ but if they call Request.stream() then all we do is send an
+ empty body so that downstream things don't hang forever.
+ """
+
+ def __init__(self, scope: Scope, receive: Receive):
+ super().__init__(scope, receive)
+ self._wrapped_rcv_disconnected = False
+ self._wrapped_rcv_consumed = False
+ self._wrapped_rc_stream = self.stream()
+
+ async def wrapped_receive(self) -> Message:
+ # wrapped_rcv state 1: disconnected
+ if self._wrapped_rcv_disconnected:
+ # we've already sent a disconnect to the downstream app
+ # we don't need to wait to get another one
+ # (although most ASGI servers will just keep sending it)
+ return {"type": "http.disconnect"}
+ # wrapped_rcv state 1: consumed but not yet disconnected
+ if self._wrapped_rcv_consumed:
+ # since the downstream app has consumed us all that is left
+ # is to send it a disconnect
+ if self._is_disconnected:
+ # the middleware has already seen the disconnect
+ # since we know the client is disconnected no need to wait
+ # for the message
+ self._wrapped_rcv_disconnected = True
+ return {"type": "http.disconnect"}
+ # we don't know yet if the client is disconnected or not
+ # so we'll wait until we get that message
+ msg = await self.receive()
+ if msg["type"] != "http.disconnect": # pragma: no cover
+ # at this point a disconnect is all that we should be receiving
+ # if we get something else, things went wrong somewhere
+ raise RuntimeError(f"Unexpected message received: {msg['type']}")
+ self._wrapped_rcv_disconnected = True
+ return msg
+
+ # wrapped_rcv state 3: not yet consumed
+ if getattr(self, "_body", None) is not None:
+ # body() was called, we return it even if the client disconnected
+ self._wrapped_rcv_consumed = True
+ return {
+ "type": "http.request",
+ "body": self._body,
+ "more_body": False,
+ }
+ elif self._stream_consumed:
+ # stream() was called to completion
+ # return an empty body so that downstream apps don't hang
+ # waiting for a disconnect
+ self._wrapped_rcv_consumed = True
+ return {
+ "type": "http.request",
+ "body": b"",
+ "more_body": False,
+ }
+ else:
+ # body() was never called and stream() wasn't consumed
+ try:
+ stream = self.stream()
+ chunk = await stream.__anext__()
+ self._wrapped_rcv_consumed = self._stream_consumed
+ return {
+ "type": "http.request",
+ "body": chunk,
+ "more_body": not self._stream_consumed,
+ }
+ except ClientDisconnect:
+ self._wrapped_rcv_disconnected = True
+ return {"type": "http.disconnect"}
+
+
+class BaseHTTPMiddleware:
+ def __init__(self, app: ASGIApp, dispatch: DispatchFunction | None = None) -> None:
+ self.app = app
+ self.dispatch_func = self.dispatch if dispatch is None else dispatch
+
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
+ if scope["type"] != "http":
+ await self.app(scope, receive, send)
+ return
+
+ request = _CachedRequest(scope, receive)
+ wrapped_receive = request.wrapped_receive
+ response_sent = anyio.Event()
+ app_exc: Exception | None = None
+
+ async def call_next(request: Request) -> Response:
+ async def receive_or_disconnect() -> Message:
+ if response_sent.is_set():
+ return {"type": "http.disconnect"}
+
+ async with anyio.create_task_group() as task_group:
+
+ async def wrap(func: typing.Callable[[], typing.Awaitable[T]]) -> T:
+ result = await func()
+ task_group.cancel_scope.cancel()
+ return result
+
+ task_group.start_soon(wrap, response_sent.wait)
+ message = await wrap(wrapped_receive)
+
+ if response_sent.is_set():
+ return {"type": "http.disconnect"}
+
+ return message
+
+ async def send_no_error(message: Message) -> None:
+ try:
+ await send_stream.send(message)
+ except anyio.BrokenResourceError:
+ # recv_stream has been closed, i.e. response_sent has been set.
+ return
+
+ async def coro() -> None:
+ nonlocal app_exc
+
+ with send_stream:
+ try:
+ await self.app(scope, receive_or_disconnect, send_no_error)
+ except Exception as exc:
+ app_exc = exc
+
+ task_group.start_soon(coro)
+
+ try:
+ message = await recv_stream.receive()
+ info = message.get("info", None)
+ if message["type"] == "http.response.debug" and info is not None:
+ message = await recv_stream.receive()
+ except anyio.EndOfStream:
+ if app_exc is not None:
+ raise app_exc
+ raise RuntimeError("No response returned.")
+
+ assert message["type"] == "http.response.start"
+
+ async def body_stream() -> typing.AsyncGenerator[bytes, None]:
+ async for message in recv_stream:
+ assert message["type"] == "http.response.body"
+ body = message.get("body", b"")
+ if body:
+ yield body
+ if not message.get("more_body", False):
+ break
+
+ response = _StreamingResponse(status_code=message["status"], content=body_stream(), info=info)
+ response.raw_headers = message["headers"]
+ return response
+
+ streams: anyio.create_memory_object_stream[Message] = anyio.create_memory_object_stream()
+ send_stream, recv_stream = streams
+ with recv_stream, send_stream, collapse_excgroups():
+ async with anyio.create_task_group() as task_group:
+ response = await self.dispatch_func(request, call_next)
+ await response(scope, wrapped_receive, send)
+ response_sent.set()
+ recv_stream.close()
+
+ if app_exc is not None:
+ raise app_exc
+
+ async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
+ raise NotImplementedError() # pragma: no cover
+
+
+class _StreamingResponse(Response):
+ def __init__(
+ self,
+ content: AsyncContentStream,
+ status_code: int = 200,
+ headers: typing.Mapping[str, str] | None = None,
+ media_type: str | None = None,
+ info: typing.Mapping[str, typing.Any] | None = None,
+ ) -> None:
+ self.info = info
+ self.body_iterator = content
+ self.status_code = status_code
+ self.media_type = media_type
+ self.init_headers(headers)
+ self.background = None
+
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
+ if self.info is not None:
+ await send({"type": "http.response.debug", "info": self.info})
+ await send(
+ {
+ "type": "http.response.start",
+ "status": self.status_code,
+ "headers": self.raw_headers,
+ }
+ )
+
+ async for chunk in self.body_iterator:
+ await send({"type": "http.response.body", "body": chunk, "more_body": True})
+
+ await send({"type": "http.response.body", "body": b"", "more_body": False})
+
+ if self.background:
+ await self.background()
diff --git a/.venv/lib/python3.12/site-packages/starlette/middleware/cors.py b/.venv/lib/python3.12/site-packages/starlette/middleware/cors.py
new file mode 100644
index 00000000..61502691
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/starlette/middleware/cors.py
@@ -0,0 +1,172 @@
+from __future__ import annotations
+
+import functools
+import re
+import typing
+
+from starlette.datastructures import Headers, MutableHeaders
+from starlette.responses import PlainTextResponse, Response
+from starlette.types import ASGIApp, Message, Receive, Scope, Send
+
+ALL_METHODS = ("DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT")
+SAFELISTED_HEADERS = {"Accept", "Accept-Language", "Content-Language", "Content-Type"}
+
+
+class CORSMiddleware:
+ def __init__(
+ self,
+ app: ASGIApp,
+ allow_origins: typing.Sequence[str] = (),
+ allow_methods: typing.Sequence[str] = ("GET",),
+ allow_headers: typing.Sequence[str] = (),
+ allow_credentials: bool = False,
+ allow_origin_regex: str | None = None,
+ expose_headers: typing.Sequence[str] = (),
+ max_age: int = 600,
+ ) -> None:
+ if "*" in allow_methods:
+ allow_methods = ALL_METHODS
+
+ compiled_allow_origin_regex = None
+ if allow_origin_regex is not None:
+ compiled_allow_origin_regex = re.compile(allow_origin_regex)
+
+ allow_all_origins = "*" in allow_origins
+ allow_all_headers = "*" in allow_headers
+ preflight_explicit_allow_origin = not allow_all_origins or allow_credentials
+
+ simple_headers = {}
+ if allow_all_origins:
+ simple_headers["Access-Control-Allow-Origin"] = "*"
+ if allow_credentials:
+ simple_headers["Access-Control-Allow-Credentials"] = "true"
+ if expose_headers:
+ simple_headers["Access-Control-Expose-Headers"] = ", ".join(expose_headers)
+
+ preflight_headers = {}
+ if preflight_explicit_allow_origin:
+ # The origin value will be set in preflight_response() if it is allowed.
+ preflight_headers["Vary"] = "Origin"
+ else:
+ preflight_headers["Access-Control-Allow-Origin"] = "*"
+ preflight_headers.update(
+ {
+ "Access-Control-Allow-Methods": ", ".join(allow_methods),
+ "Access-Control-Max-Age": str(max_age),
+ }
+ )
+ allow_headers = sorted(SAFELISTED_HEADERS | set(allow_headers))
+ if allow_headers and not allow_all_headers:
+ preflight_headers["Access-Control-Allow-Headers"] = ", ".join(allow_headers)
+ if allow_credentials:
+ preflight_headers["Access-Control-Allow-Credentials"] = "true"
+
+ self.app = app
+ self.allow_origins = allow_origins
+ self.allow_methods = allow_methods
+ self.allow_headers = [h.lower() for h in allow_headers]
+ self.allow_all_origins = allow_all_origins
+ self.allow_all_headers = allow_all_headers
+ self.preflight_explicit_allow_origin = preflight_explicit_allow_origin
+ self.allow_origin_regex = compiled_allow_origin_regex
+ self.simple_headers = simple_headers
+ self.preflight_headers = preflight_headers
+
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
+ if scope["type"] != "http": # pragma: no cover
+ await self.app(scope, receive, send)
+ return
+
+ method = scope["method"]
+ headers = Headers(scope=scope)
+ origin = headers.get("origin")
+
+ if origin is None:
+ await self.app(scope, receive, send)
+ return
+
+ if method == "OPTIONS" and "access-control-request-method" in headers:
+ response = self.preflight_response(request_headers=headers)
+ await response(scope, receive, send)
+ return
+
+ await self.simple_response(scope, receive, send, request_headers=headers)
+
+ def is_allowed_origin(self, origin: str) -> bool:
+ if self.allow_all_origins:
+ return True
+
+ if self.allow_origin_regex is not None and self.allow_origin_regex.fullmatch(origin):
+ return True
+
+ return origin in self.allow_origins
+
+ def preflight_response(self, request_headers: Headers) -> Response:
+ requested_origin = request_headers["origin"]
+ requested_method = request_headers["access-control-request-method"]
+ requested_headers = request_headers.get("access-control-request-headers")
+
+ headers = dict(self.preflight_headers)
+ failures = []
+
+ if self.is_allowed_origin(origin=requested_origin):
+ if self.preflight_explicit_allow_origin:
+ # The "else" case is already accounted for in self.preflight_headers
+ # and the value would be "*".
+ headers["Access-Control-Allow-Origin"] = requested_origin
+ else:
+ failures.append("origin")
+
+ if requested_method not in self.allow_methods:
+ failures.append("method")
+
+ # If we allow all headers, then we have to mirror back any requested
+ # headers in the response.
+ if self.allow_all_headers and requested_headers is not None:
+ headers["Access-Control-Allow-Headers"] = requested_headers
+ elif requested_headers is not None:
+ for header in [h.lower() for h in requested_headers.split(",")]:
+ if header.strip() not in self.allow_headers:
+ failures.append("headers")
+ break
+
+ # We don't strictly need to use 400 responses here, since its up to
+ # the browser to enforce the CORS policy, but its more informative
+ # if we do.
+ if failures:
+ failure_text = "Disallowed CORS " + ", ".join(failures)
+ return PlainTextResponse(failure_text, status_code=400, headers=headers)
+
+ return PlainTextResponse("OK", status_code=200, headers=headers)
+
+ async def simple_response(self, scope: Scope, receive: Receive, send: Send, request_headers: Headers) -> None:
+ send = functools.partial(self.send, send=send, request_headers=request_headers)
+ await self.app(scope, receive, send)
+
+ async def send(self, message: Message, send: Send, request_headers: Headers) -> None:
+ if message["type"] != "http.response.start":
+ await send(message)
+ return
+
+ message.setdefault("headers", [])
+ headers = MutableHeaders(scope=message)
+ headers.update(self.simple_headers)
+ origin = request_headers["Origin"]
+ has_cookie = "cookie" in request_headers
+
+ # If request includes any cookie headers, then we must respond
+ # with the specific origin instead of '*'.
+ if self.allow_all_origins and has_cookie:
+ self.allow_explicit_origin(headers, origin)
+
+ # If we only allow specific origins, then we have to mirror back
+ # the Origin header in the response.
+ elif not self.allow_all_origins and self.is_allowed_origin(origin=origin):
+ self.allow_explicit_origin(headers, origin)
+
+ await send(message)
+
+ @staticmethod
+ def allow_explicit_origin(headers: MutableHeaders, origin: str) -> None:
+ headers["Access-Control-Allow-Origin"] = origin
+ headers.add_vary_header("Origin")
diff --git a/.venv/lib/python3.12/site-packages/starlette/middleware/errors.py b/.venv/lib/python3.12/site-packages/starlette/middleware/errors.py
new file mode 100644
index 00000000..76ad776b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/starlette/middleware/errors.py
@@ -0,0 +1,260 @@
+from __future__ import annotations
+
+import html
+import inspect
+import sys
+import traceback
+import typing
+
+from starlette._utils import is_async_callable
+from starlette.concurrency import run_in_threadpool
+from starlette.requests import Request
+from starlette.responses import HTMLResponse, PlainTextResponse, Response
+from starlette.types import ASGIApp, Message, Receive, Scope, Send
+
+STYLES = """
+p {
+ color: #211c1c;
+}
+.traceback-container {
+ border: 1px solid #038BB8;
+}
+.traceback-title {
+ background-color: #038BB8;
+ color: lemonchiffon;
+ padding: 12px;
+ font-size: 20px;
+ margin-top: 0px;
+}
+.frame-line {
+ padding-left: 10px;
+ font-family: monospace;
+}
+.frame-filename {
+ font-family: monospace;
+}
+.center-line {
+ background-color: #038BB8;
+ color: #f9f6e1;
+ padding: 5px 0px 5px 5px;
+}
+.lineno {
+ margin-right: 5px;
+}
+.frame-title {
+ font-weight: unset;
+ padding: 10px 10px 10px 10px;
+ background-color: #E4F4FD;
+ margin-right: 10px;
+ color: #191f21;
+ font-size: 17px;
+ border: 1px solid #c7dce8;
+}
+.collapse-btn {
+ float: right;
+ padding: 0px 5px 1px 5px;
+ border: solid 1px #96aebb;
+ cursor: pointer;
+}
+.collapsed {
+ display: none;
+}
+.source-code {
+ font-family: courier;
+ font-size: small;
+ padding-bottom: 10px;
+}
+"""
+
+JS = """
+<script type="text/javascript">
+ function collapse(element){
+ const frameId = element.getAttribute("data-frame-id");
+ const frame = document.getElementById(frameId);
+
+ if (frame.classList.contains("collapsed")){
+ element.innerHTML = "&#8210;";
+ frame.classList.remove("collapsed");
+ } else {
+ element.innerHTML = "+";
+ frame.classList.add("collapsed");
+ }
+ }
+</script>
+"""
+
+TEMPLATE = """
+<html>
+ <head>
+ <style type='text/css'>
+ {styles}
+ </style>
+ <title>Starlette Debugger</title>
+ </head>
+ <body>
+ <h1>500 Server Error</h1>
+ <h2>{error}</h2>
+ <div class="traceback-container">
+ <p class="traceback-title">Traceback</p>
+ <div>{exc_html}</div>
+ </div>
+ {js}
+ </body>
+</html>
+"""
+
+FRAME_TEMPLATE = """
+<div>
+ <p class="frame-title">File <span class="frame-filename">{frame_filename}</span>,
+ line <i>{frame_lineno}</i>,
+ in <b>{frame_name}</b>
+ <span class="collapse-btn" data-frame-id="{frame_filename}-{frame_lineno}" onclick="collapse(this)">{collapse_button}</span>
+ </p>
+ <div id="{frame_filename}-{frame_lineno}" class="source-code {collapsed}">{code_context}</div>
+</div>
+""" # noqa: E501
+
+LINE = """
+<p><span class="frame-line">
+<span class="lineno">{lineno}.</span> {line}</span></p>
+"""
+
+CENTER_LINE = """
+<p class="center-line"><span class="frame-line center-line">
+<span class="lineno">{lineno}.</span> {line}</span></p>
+"""
+
+
+class ServerErrorMiddleware:
+ """
+ Handles returning 500 responses when a server error occurs.
+
+ If 'debug' is set, then traceback responses will be returned,
+ otherwise the designated 'handler' will be called.
+
+ This middleware class should generally be used to wrap *everything*
+ else up, so that unhandled exceptions anywhere in the stack
+ always result in an appropriate 500 response.
+ """
+
+ def __init__(
+ self,
+ app: ASGIApp,
+ handler: typing.Callable[[Request, Exception], typing.Any] | None = None,
+ debug: bool = False,
+ ) -> None:
+ self.app = app
+ self.handler = handler
+ self.debug = debug
+
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
+ if scope["type"] != "http":
+ await self.app(scope, receive, send)
+ return
+
+ response_started = False
+
+ async def _send(message: Message) -> None:
+ nonlocal response_started, send
+
+ if message["type"] == "http.response.start":
+ response_started = True
+ await send(message)
+
+ try:
+ await self.app(scope, receive, _send)
+ except Exception as exc:
+ request = Request(scope)
+ if self.debug:
+ # In debug mode, return traceback responses.
+ response = self.debug_response(request, exc)
+ elif self.handler is None:
+ # Use our default 500 error handler.
+ response = self.error_response(request, exc)
+ else:
+ # Use an installed 500 error handler.
+ if is_async_callable(self.handler):
+ response = await self.handler(request, exc)
+ else:
+ response = await run_in_threadpool(self.handler, request, exc)
+
+ if not response_started:
+ await response(scope, receive, send)
+
+ # We always continue to raise the exception.
+ # This allows servers to log the error, or allows test clients
+ # to optionally raise the error within the test case.
+ raise exc
+
+ def format_line(self, index: int, line: str, frame_lineno: int, frame_index: int) -> str:
+ values = {
+ # HTML escape - line could contain < or >
+ "line": html.escape(line).replace(" ", "&nbsp"),
+ "lineno": (frame_lineno - frame_index) + index,
+ }
+
+ if index != frame_index:
+ return LINE.format(**values)
+ return CENTER_LINE.format(**values)
+
+ def generate_frame_html(self, frame: inspect.FrameInfo, is_collapsed: bool) -> str:
+ code_context = "".join(
+ self.format_line(
+ index,
+ line,
+ frame.lineno,
+ frame.index, # type: ignore[arg-type]
+ )
+ for index, line in enumerate(frame.code_context or [])
+ )
+
+ values = {
+ # HTML escape - filename could contain < or >, especially if it's a virtual
+ # file e.g. <stdin> in the REPL
+ "frame_filename": html.escape(frame.filename),
+ "frame_lineno": frame.lineno,
+ # HTML escape - if you try very hard it's possible to name a function with <
+ # or >
+ "frame_name": html.escape(frame.function),
+ "code_context": code_context,
+ "collapsed": "collapsed" if is_collapsed else "",
+ "collapse_button": "+" if is_collapsed else "&#8210;",
+ }
+ return FRAME_TEMPLATE.format(**values)
+
+ def generate_html(self, exc: Exception, limit: int = 7) -> str:
+ traceback_obj = traceback.TracebackException.from_exception(exc, capture_locals=True)
+
+ exc_html = ""
+ is_collapsed = False
+ exc_traceback = exc.__traceback__
+ if exc_traceback is not None:
+ frames = inspect.getinnerframes(exc_traceback, limit)
+ for frame in reversed(frames):
+ exc_html += self.generate_frame_html(frame, is_collapsed)
+ is_collapsed = True
+
+ if sys.version_info >= (3, 13): # pragma: no cover
+ exc_type_str = traceback_obj.exc_type_str
+ else: # pragma: no cover
+ exc_type_str = traceback_obj.exc_type.__name__
+
+ # escape error class and text
+ error = f"{html.escape(exc_type_str)}: {html.escape(str(traceback_obj))}"
+
+ return TEMPLATE.format(styles=STYLES, js=JS, error=error, exc_html=exc_html)
+
+ def generate_plain_text(self, exc: Exception) -> str:
+ return "".join(traceback.format_exception(type(exc), exc, exc.__traceback__))
+
+ def debug_response(self, request: Request, exc: Exception) -> Response:
+ accept = request.headers.get("accept", "")
+
+ if "text/html" in accept:
+ content = self.generate_html(exc)
+ return HTMLResponse(content, status_code=500)
+ content = self.generate_plain_text(exc)
+ return PlainTextResponse(content, status_code=500)
+
+ def error_response(self, request: Request, exc: Exception) -> Response:
+ return PlainTextResponse("Internal Server Error", status_code=500)
diff --git a/.venv/lib/python3.12/site-packages/starlette/middleware/exceptions.py b/.venv/lib/python3.12/site-packages/starlette/middleware/exceptions.py
new file mode 100644
index 00000000..981d2fca
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/starlette/middleware/exceptions.py
@@ -0,0 +1,72 @@
+from __future__ import annotations
+
+import typing
+
+from starlette._exception_handler import (
+ ExceptionHandlers,
+ StatusHandlers,
+ wrap_app_handling_exceptions,
+)
+from starlette.exceptions import HTTPException, WebSocketException
+from starlette.requests import Request
+from starlette.responses import PlainTextResponse, Response
+from starlette.types import ASGIApp, Receive, Scope, Send
+from starlette.websockets import WebSocket
+
+
+class ExceptionMiddleware:
+ def __init__(
+ self,
+ app: ASGIApp,
+ handlers: typing.Mapping[typing.Any, typing.Callable[[Request, Exception], Response]] | None = None,
+ debug: bool = False,
+ ) -> None:
+ self.app = app
+ self.debug = debug # TODO: We ought to handle 404 cases if debug is set.
+ self._status_handlers: StatusHandlers = {}
+ self._exception_handlers: ExceptionHandlers = {
+ HTTPException: self.http_exception,
+ WebSocketException: self.websocket_exception,
+ }
+ if handlers is not None: # pragma: no branch
+ for key, value in handlers.items():
+ self.add_exception_handler(key, value)
+
+ def add_exception_handler(
+ self,
+ exc_class_or_status_code: int | type[Exception],
+ handler: typing.Callable[[Request, Exception], Response],
+ ) -> None:
+ if isinstance(exc_class_or_status_code, int):
+ self._status_handlers[exc_class_or_status_code] = handler
+ else:
+ assert issubclass(exc_class_or_status_code, Exception)
+ self._exception_handlers[exc_class_or_status_code] = handler
+
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
+ if scope["type"] not in ("http", "websocket"):
+ await self.app(scope, receive, send)
+ return
+
+ scope["starlette.exception_handlers"] = (
+ self._exception_handlers,
+ self._status_handlers,
+ )
+
+ conn: Request | WebSocket
+ if scope["type"] == "http":
+ conn = Request(scope, receive, send)
+ else:
+ conn = WebSocket(scope, receive, send)
+
+ await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
+
+ def http_exception(self, request: Request, exc: Exception) -> Response:
+ assert isinstance(exc, HTTPException)
+ if exc.status_code in {204, 304}:
+ return Response(status_code=exc.status_code, headers=exc.headers)
+ return PlainTextResponse(exc.detail, status_code=exc.status_code, headers=exc.headers)
+
+ async def websocket_exception(self, websocket: WebSocket, exc: Exception) -> None:
+ assert isinstance(exc, WebSocketException)
+ await websocket.close(code=exc.code, reason=exc.reason) # pragma: no cover
diff --git a/.venv/lib/python3.12/site-packages/starlette/middleware/gzip.py b/.venv/lib/python3.12/site-packages/starlette/middleware/gzip.py
new file mode 100644
index 00000000..c7fd5b77
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/starlette/middleware/gzip.py
@@ -0,0 +1,141 @@
+import gzip
+import io
+import typing
+
+from starlette.datastructures import Headers, MutableHeaders
+from starlette.types import ASGIApp, Message, Receive, Scope, Send
+
+DEFAULT_EXCLUDED_CONTENT_TYPES = ("text/event-stream",)
+
+
+class GZipMiddleware:
+ def __init__(self, app: ASGIApp, minimum_size: int = 500, compresslevel: int = 9) -> None:
+ self.app = app
+ self.minimum_size = minimum_size
+ self.compresslevel = compresslevel
+
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
+ if scope["type"] != "http": # pragma: no cover
+ await self.app(scope, receive, send)
+ return
+
+ headers = Headers(scope=scope)
+ responder: ASGIApp
+ if "gzip" in headers.get("Accept-Encoding", ""):
+ responder = GZipResponder(self.app, self.minimum_size, compresslevel=self.compresslevel)
+ else:
+ responder = IdentityResponder(self.app, self.minimum_size)
+
+ await responder(scope, receive, send)
+
+
+class IdentityResponder:
+ content_encoding: str
+
+ def __init__(self, app: ASGIApp, minimum_size: int) -> None:
+ self.app = app
+ self.minimum_size = minimum_size
+ self.send: Send = unattached_send
+ self.initial_message: Message = {}
+ self.started = False
+ self.content_encoding_set = False
+ self.content_type_is_excluded = False
+
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
+ self.send = send
+ await self.app(scope, receive, self.send_with_compression)
+
+ async def send_with_compression(self, message: Message) -> None:
+ message_type = message["type"]
+ if message_type == "http.response.start":
+ # Don't send the initial message until we've determined how to
+ # modify the outgoing headers correctly.
+ self.initial_message = message
+ headers = Headers(raw=self.initial_message["headers"])
+ self.content_encoding_set = "content-encoding" in headers
+ self.content_type_is_excluded = headers.get("content-type", "").startswith(DEFAULT_EXCLUDED_CONTENT_TYPES)
+ elif message_type == "http.response.body" and (self.content_encoding_set or self.content_type_is_excluded):
+ if not self.started:
+ self.started = True
+ await self.send(self.initial_message)
+ await self.send(message)
+ elif message_type == "http.response.body" and not self.started:
+ self.started = True
+ body = message.get("body", b"")
+ more_body = message.get("more_body", False)
+ if len(body) < self.minimum_size and not more_body:
+ # Don't apply compression to small outgoing responses.
+ await self.send(self.initial_message)
+ await self.send(message)
+ elif not more_body:
+ # Standard response.
+ body = self.apply_compression(body, more_body=False)
+
+ headers = MutableHeaders(raw=self.initial_message["headers"])
+ headers.add_vary_header("Accept-Encoding")
+ if body != message["body"]:
+ headers["Content-Encoding"] = self.content_encoding
+ headers["Content-Length"] = str(len(body))
+ message["body"] = body
+
+ await self.send(self.initial_message)
+ await self.send(message)
+ else:
+ # Initial body in streaming response.
+ body = self.apply_compression(body, more_body=True)
+
+ headers = MutableHeaders(raw=self.initial_message["headers"])
+ headers.add_vary_header("Accept-Encoding")
+ if body != message["body"]:
+ headers["Content-Encoding"] = self.content_encoding
+ del headers["Content-Length"]
+ message["body"] = body
+
+ await self.send(self.initial_message)
+ await self.send(message)
+ elif message_type == "http.response.body": # pragma: no branch
+ # Remaining body in streaming response.
+ body = message.get("body", b"")
+ more_body = message.get("more_body", False)
+
+ message["body"] = self.apply_compression(body, more_body=more_body)
+
+ await self.send(message)
+
+ def apply_compression(self, body: bytes, *, more_body: bool) -> bytes:
+ """Apply compression on the response body.
+
+ If more_body is False, any compression file should be closed. If it
+ isn't, it won't be closed automatically until all background tasks
+ complete.
+ """
+ return body
+
+
+class GZipResponder(IdentityResponder):
+ content_encoding = "gzip"
+
+ def __init__(self, app: ASGIApp, minimum_size: int, compresslevel: int = 9) -> None:
+ super().__init__(app, minimum_size)
+
+ self.gzip_buffer = io.BytesIO()
+ self.gzip_file = gzip.GzipFile(mode="wb", fileobj=self.gzip_buffer, compresslevel=compresslevel)
+
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
+ with self.gzip_buffer, self.gzip_file:
+ await super().__call__(scope, receive, send)
+
+ def apply_compression(self, body: bytes, *, more_body: bool) -> bytes:
+ self.gzip_file.write(body)
+ if not more_body:
+ self.gzip_file.close()
+
+ body = self.gzip_buffer.getvalue()
+ self.gzip_buffer.seek(0)
+ self.gzip_buffer.truncate()
+
+ return body
+
+
+async def unattached_send(message: Message) -> typing.NoReturn:
+ raise RuntimeError("send awaitable not set") # pragma: no cover
diff --git a/.venv/lib/python3.12/site-packages/starlette/middleware/httpsredirect.py b/.venv/lib/python3.12/site-packages/starlette/middleware/httpsredirect.py
new file mode 100644
index 00000000..a8359067
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/starlette/middleware/httpsredirect.py
@@ -0,0 +1,19 @@
+from starlette.datastructures import URL
+from starlette.responses import RedirectResponse
+from starlette.types import ASGIApp, Receive, Scope, Send
+
+
+class HTTPSRedirectMiddleware:
+ def __init__(self, app: ASGIApp) -> None:
+ self.app = app
+
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
+ if scope["type"] in ("http", "websocket") and scope["scheme"] in ("http", "ws"):
+ url = URL(scope=scope)
+ redirect_scheme = {"http": "https", "ws": "wss"}[url.scheme]
+ netloc = url.hostname if url.port in (80, 443) else url.netloc
+ url = url.replace(scheme=redirect_scheme, netloc=netloc)
+ response = RedirectResponse(url, status_code=307)
+ await response(scope, receive, send)
+ else:
+ await self.app(scope, receive, send)
diff --git a/.venv/lib/python3.12/site-packages/starlette/middleware/sessions.py b/.venv/lib/python3.12/site-packages/starlette/middleware/sessions.py
new file mode 100644
index 00000000..5f9fcd88
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/starlette/middleware/sessions.py
@@ -0,0 +1,85 @@
+from __future__ import annotations
+
+import json
+import typing
+from base64 import b64decode, b64encode
+
+import itsdangerous
+from itsdangerous.exc import BadSignature
+
+from starlette.datastructures import MutableHeaders, Secret
+from starlette.requests import HTTPConnection
+from starlette.types import ASGIApp, Message, Receive, Scope, Send
+
+
+class SessionMiddleware:
+ def __init__(
+ self,
+ app: ASGIApp,
+ secret_key: str | Secret,
+ session_cookie: str = "session",
+ max_age: int | None = 14 * 24 * 60 * 60, # 14 days, in seconds
+ path: str = "/",
+ same_site: typing.Literal["lax", "strict", "none"] = "lax",
+ https_only: bool = False,
+ domain: str | None = None,
+ ) -> None:
+ self.app = app
+ self.signer = itsdangerous.TimestampSigner(str(secret_key))
+ self.session_cookie = session_cookie
+ self.max_age = max_age
+ self.path = path
+ self.security_flags = "httponly; samesite=" + same_site
+ if https_only: # Secure flag can be used with HTTPS only
+ self.security_flags += "; secure"
+ if domain is not None:
+ self.security_flags += f"; domain={domain}"
+
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
+ if scope["type"] not in ("http", "websocket"): # pragma: no cover
+ await self.app(scope, receive, send)
+ return
+
+ connection = HTTPConnection(scope)
+ initial_session_was_empty = True
+
+ if self.session_cookie in connection.cookies:
+ data = connection.cookies[self.session_cookie].encode("utf-8")
+ try:
+ data = self.signer.unsign(data, max_age=self.max_age)
+ scope["session"] = json.loads(b64decode(data))
+ initial_session_was_empty = False
+ except BadSignature:
+ scope["session"] = {}
+ else:
+ scope["session"] = {}
+
+ async def send_wrapper(message: Message) -> None:
+ if message["type"] == "http.response.start":
+ if scope["session"]:
+ # We have session data to persist.
+ data = b64encode(json.dumps(scope["session"]).encode("utf-8"))
+ data = self.signer.sign(data)
+ headers = MutableHeaders(scope=message)
+ header_value = "{session_cookie}={data}; path={path}; {max_age}{security_flags}".format(
+ session_cookie=self.session_cookie,
+ data=data.decode("utf-8"),
+ path=self.path,
+ max_age=f"Max-Age={self.max_age}; " if self.max_age else "",
+ security_flags=self.security_flags,
+ )
+ headers.append("Set-Cookie", header_value)
+ elif not initial_session_was_empty:
+ # The session has been cleared.
+ headers = MutableHeaders(scope=message)
+ header_value = "{session_cookie}={data}; path={path}; {expires}{security_flags}".format(
+ session_cookie=self.session_cookie,
+ data="null",
+ path=self.path,
+ expires="expires=Thu, 01 Jan 1970 00:00:00 GMT; ",
+ security_flags=self.security_flags,
+ )
+ headers.append("Set-Cookie", header_value)
+ await send(message)
+
+ await self.app(scope, receive, send_wrapper)
diff --git a/.venv/lib/python3.12/site-packages/starlette/middleware/trustedhost.py b/.venv/lib/python3.12/site-packages/starlette/middleware/trustedhost.py
new file mode 100644
index 00000000..2d1c999e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/starlette/middleware/trustedhost.py
@@ -0,0 +1,60 @@
+from __future__ import annotations
+
+import typing
+
+from starlette.datastructures import URL, Headers
+from starlette.responses import PlainTextResponse, RedirectResponse, Response
+from starlette.types import ASGIApp, Receive, Scope, Send
+
+ENFORCE_DOMAIN_WILDCARD = "Domain wildcard patterns must be like '*.example.com'."
+
+
+class TrustedHostMiddleware:
+ def __init__(
+ self,
+ app: ASGIApp,
+ allowed_hosts: typing.Sequence[str] | None = None,
+ www_redirect: bool = True,
+ ) -> None:
+ if allowed_hosts is None:
+ allowed_hosts = ["*"]
+
+ for pattern in allowed_hosts:
+ assert "*" not in pattern[1:], ENFORCE_DOMAIN_WILDCARD
+ if pattern.startswith("*") and pattern != "*":
+ assert pattern.startswith("*."), ENFORCE_DOMAIN_WILDCARD
+ self.app = app
+ self.allowed_hosts = list(allowed_hosts)
+ self.allow_any = "*" in allowed_hosts
+ self.www_redirect = www_redirect
+
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
+ if self.allow_any or scope["type"] not in (
+ "http",
+ "websocket",
+ ): # pragma: no cover
+ await self.app(scope, receive, send)
+ return
+
+ headers = Headers(scope=scope)
+ host = headers.get("host", "").split(":")[0]
+ is_valid_host = False
+ found_www_redirect = False
+ for pattern in self.allowed_hosts:
+ if host == pattern or (pattern.startswith("*") and host.endswith(pattern[1:])):
+ is_valid_host = True
+ break
+ elif "www." + host == pattern:
+ found_www_redirect = True
+
+ if is_valid_host:
+ await self.app(scope, receive, send)
+ else:
+ response: Response
+ if found_www_redirect and self.www_redirect:
+ url = URL(scope=scope)
+ redirect_url = url.replace(netloc="www." + url.netloc)
+ response = RedirectResponse(url=str(redirect_url))
+ else:
+ response = PlainTextResponse("Invalid host header", status_code=400)
+ await response(scope, receive, send)
diff --git a/.venv/lib/python3.12/site-packages/starlette/middleware/wsgi.py b/.venv/lib/python3.12/site-packages/starlette/middleware/wsgi.py
new file mode 100644
index 00000000..6e0a3fae
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/starlette/middleware/wsgi.py
@@ -0,0 +1,152 @@
+from __future__ import annotations
+
+import io
+import math
+import sys
+import typing
+import warnings
+
+import anyio
+from anyio.abc import ObjectReceiveStream, ObjectSendStream
+
+from starlette.types import Receive, Scope, Send
+
+warnings.warn(
+ "starlette.middleware.wsgi is deprecated and will be removed in a future release. "
+ "Please refer to https://github.com/abersheeran/a2wsgi as a replacement.",
+ DeprecationWarning,
+)
+
+
+def build_environ(scope: Scope, body: bytes) -> dict[str, typing.Any]:
+ """
+ Builds a scope and request body into a WSGI environ object.
+ """
+
+ script_name = scope.get("root_path", "").encode("utf8").decode("latin1")
+ path_info = scope["path"].encode("utf8").decode("latin1")
+ if path_info.startswith(script_name):
+ path_info = path_info[len(script_name) :]
+
+ environ = {
+ "REQUEST_METHOD": scope["method"],
+ "SCRIPT_NAME": script_name,
+ "PATH_INFO": path_info,
+ "QUERY_STRING": scope["query_string"].decode("ascii"),
+ "SERVER_PROTOCOL": f"HTTP/{scope['http_version']}",
+ "wsgi.version": (1, 0),
+ "wsgi.url_scheme": scope.get("scheme", "http"),
+ "wsgi.input": io.BytesIO(body),
+ "wsgi.errors": sys.stdout,
+ "wsgi.multithread": True,
+ "wsgi.multiprocess": True,
+ "wsgi.run_once": False,
+ }
+
+ # Get server name and port - required in WSGI, not in ASGI
+ server = scope.get("server") or ("localhost", 80)
+ environ["SERVER_NAME"] = server[0]
+ environ["SERVER_PORT"] = server[1]
+
+ # Get client IP address
+ if scope.get("client"):
+ environ["REMOTE_ADDR"] = scope["client"][0]
+
+ # Go through headers and make them into environ entries
+ for name, value in scope.get("headers", []):
+ name = name.decode("latin1")
+ if name == "content-length":
+ corrected_name = "CONTENT_LENGTH"
+ elif name == "content-type":
+ corrected_name = "CONTENT_TYPE"
+ else:
+ corrected_name = f"HTTP_{name}".upper().replace("-", "_")
+ # HTTPbis say only ASCII chars are allowed in headers, but we latin1 just in
+ # case
+ value = value.decode("latin1")
+ if corrected_name in environ:
+ value = environ[corrected_name] + "," + value
+ environ[corrected_name] = value
+ return environ
+
+
+class WSGIMiddleware:
+ def __init__(self, app: typing.Callable[..., typing.Any]) -> None:
+ self.app = app
+
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
+ assert scope["type"] == "http"
+ responder = WSGIResponder(self.app, scope)
+ await responder(receive, send)
+
+
+class WSGIResponder:
+ stream_send: ObjectSendStream[typing.MutableMapping[str, typing.Any]]
+ stream_receive: ObjectReceiveStream[typing.MutableMapping[str, typing.Any]]
+
+ def __init__(self, app: typing.Callable[..., typing.Any], scope: Scope) -> None:
+ self.app = app
+ self.scope = scope
+ self.status = None
+ self.response_headers = None
+ self.stream_send, self.stream_receive = anyio.create_memory_object_stream(math.inf)
+ self.response_started = False
+ self.exc_info: typing.Any = None
+
+ async def __call__(self, receive: Receive, send: Send) -> None:
+ body = b""
+ more_body = True
+ while more_body:
+ message = await receive()
+ body += message.get("body", b"")
+ more_body = message.get("more_body", False)
+ environ = build_environ(self.scope, body)
+
+ async with anyio.create_task_group() as task_group:
+ task_group.start_soon(self.sender, send)
+ async with self.stream_send:
+ await anyio.to_thread.run_sync(self.wsgi, environ, self.start_response)
+ if self.exc_info is not None:
+ raise self.exc_info[0].with_traceback(self.exc_info[1], self.exc_info[2])
+
+ async def sender(self, send: Send) -> None:
+ async with self.stream_receive:
+ async for message in self.stream_receive:
+ await send(message)
+
+ def start_response(
+ self,
+ status: str,
+ response_headers: list[tuple[str, str]],
+ exc_info: typing.Any = None,
+ ) -> None:
+ self.exc_info = exc_info
+ if not self.response_started: # pragma: no branch
+ self.response_started = True
+ status_code_string, _ = status.split(" ", 1)
+ status_code = int(status_code_string)
+ headers = [
+ (name.strip().encode("ascii").lower(), value.strip().encode("ascii"))
+ for name, value in response_headers
+ ]
+ anyio.from_thread.run(
+ self.stream_send.send,
+ {
+ "type": "http.response.start",
+ "status": status_code,
+ "headers": headers,
+ },
+ )
+
+ def wsgi(
+ self,
+ environ: dict[str, typing.Any],
+ start_response: typing.Callable[..., typing.Any],
+ ) -> None:
+ for chunk in self.app(environ, start_response):
+ anyio.from_thread.run(
+ self.stream_send.send,
+ {"type": "http.response.body", "body": chunk, "more_body": True},
+ )
+
+ anyio.from_thread.run(self.stream_send.send, {"type": "http.response.body", "body": b""})
diff --git a/.venv/lib/python3.12/site-packages/starlette/py.typed b/.venv/lib/python3.12/site-packages/starlette/py.typed
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/starlette/py.typed
diff --git a/.venv/lib/python3.12/site-packages/starlette/requests.py b/.venv/lib/python3.12/site-packages/starlette/requests.py
new file mode 100644
index 00000000..7dc04a74
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/starlette/requests.py
@@ -0,0 +1,322 @@
+from __future__ import annotations
+
+import json
+import typing
+from http import cookies as http_cookies
+
+import anyio
+
+from starlette._utils import AwaitableOrContextManager, AwaitableOrContextManagerWrapper
+from starlette.datastructures import URL, Address, FormData, Headers, QueryParams, State
+from starlette.exceptions import HTTPException
+from starlette.formparsers import FormParser, MultiPartException, MultiPartParser
+from starlette.types import Message, Receive, Scope, Send
+
+if typing.TYPE_CHECKING:
+ from python_multipart.multipart import parse_options_header
+
+ from starlette.applications import Starlette
+ from starlette.routing import Router
+else:
+ try:
+ try:
+ from python_multipart.multipart import parse_options_header
+ except ModuleNotFoundError: # pragma: no cover
+ from multipart.multipart import parse_options_header
+ except ModuleNotFoundError: # pragma: no cover
+ parse_options_header = None
+
+
+SERVER_PUSH_HEADERS_TO_COPY = {
+ "accept",
+ "accept-encoding",
+ "accept-language",
+ "cache-control",
+ "user-agent",
+}
+
+
+def cookie_parser(cookie_string: str) -> dict[str, str]:
+ """
+ This function parses a ``Cookie`` HTTP header into a dict of key/value pairs.
+
+ It attempts to mimic browser cookie parsing behavior: browsers and web servers
+ frequently disregard the spec (RFC 6265) when setting and reading cookies,
+ so we attempt to suit the common scenarios here.
+
+ This function has been adapted from Django 3.1.0.
+ Note: we are explicitly _NOT_ using `SimpleCookie.load` because it is based
+ on an outdated spec and will fail on lots of input we want to support
+ """
+ cookie_dict: dict[str, str] = {}
+ for chunk in cookie_string.split(";"):
+ if "=" in chunk:
+ key, val = chunk.split("=", 1)
+ else:
+ # Assume an empty name per
+ # https://bugzilla.mozilla.org/show_bug.cgi?id=169091
+ key, val = "", chunk
+ key, val = key.strip(), val.strip()
+ if key or val:
+ # unquote using Python's algorithm.
+ cookie_dict[key] = http_cookies._unquote(val)
+ return cookie_dict
+
+
+class ClientDisconnect(Exception):
+ pass
+
+
+class HTTPConnection(typing.Mapping[str, typing.Any]):
+ """
+ A base class for incoming HTTP connections, that is used to provide
+ any functionality that is common to both `Request` and `WebSocket`.
+ """
+
+ def __init__(self, scope: Scope, receive: Receive | None = None) -> None:
+ assert scope["type"] in ("http", "websocket")
+ self.scope = scope
+
+ def __getitem__(self, key: str) -> typing.Any:
+ return self.scope[key]
+
+ def __iter__(self) -> typing.Iterator[str]:
+ return iter(self.scope)
+
+ def __len__(self) -> int:
+ return len(self.scope)
+
+ # Don't use the `abc.Mapping.__eq__` implementation.
+ # Connection instances should never be considered equal
+ # unless `self is other`.
+ __eq__ = object.__eq__
+ __hash__ = object.__hash__
+
+ @property
+ def app(self) -> typing.Any:
+ return self.scope["app"]
+
+ @property
+ def url(self) -> URL:
+ if not hasattr(self, "_url"): # pragma: no branch
+ self._url = URL(scope=self.scope)
+ return self._url
+
+ @property
+ def base_url(self) -> URL:
+ if not hasattr(self, "_base_url"):
+ base_url_scope = dict(self.scope)
+ # This is used by request.url_for, it might be used inside a Mount which
+ # would have its own child scope with its own root_path, but the base URL
+ # for url_for should still be the top level app root path.
+ app_root_path = base_url_scope.get("app_root_path", base_url_scope.get("root_path", ""))
+ path = app_root_path
+ if not path.endswith("/"):
+ path += "/"
+ base_url_scope["path"] = path
+ base_url_scope["query_string"] = b""
+ base_url_scope["root_path"] = app_root_path
+ self._base_url = URL(scope=base_url_scope)
+ return self._base_url
+
+ @property
+ def headers(self) -> Headers:
+ if not hasattr(self, "_headers"):
+ self._headers = Headers(scope=self.scope)
+ return self._headers
+
+ @property
+ def query_params(self) -> QueryParams:
+ if not hasattr(self, "_query_params"): # pragma: no branch
+ self._query_params = QueryParams(self.scope["query_string"])
+ return self._query_params
+
+ @property
+ def path_params(self) -> dict[str, typing.Any]:
+ return self.scope.get("path_params", {})
+
+ @property
+ def cookies(self) -> dict[str, str]:
+ if not hasattr(self, "_cookies"):
+ cookies: dict[str, str] = {}
+ cookie_header = self.headers.get("cookie")
+
+ if cookie_header:
+ cookies = cookie_parser(cookie_header)
+ self._cookies = cookies
+ return self._cookies
+
+ @property
+ def client(self) -> Address | None:
+ # client is a 2 item tuple of (host, port), None if missing
+ host_port = self.scope.get("client")
+ if host_port is not None:
+ return Address(*host_port)
+ return None
+
+ @property
+ def session(self) -> dict[str, typing.Any]:
+ assert "session" in self.scope, "SessionMiddleware must be installed to access request.session"
+ return self.scope["session"] # type: ignore[no-any-return]
+
+ @property
+ def auth(self) -> typing.Any:
+ assert "auth" in self.scope, "AuthenticationMiddleware must be installed to access request.auth"
+ return self.scope["auth"]
+
+ @property
+ def user(self) -> typing.Any:
+ assert "user" in self.scope, "AuthenticationMiddleware must be installed to access request.user"
+ return self.scope["user"]
+
+ @property
+ def state(self) -> State:
+ if not hasattr(self, "_state"):
+ # Ensure 'state' has an empty dict if it's not already populated.
+ self.scope.setdefault("state", {})
+ # Create a state instance with a reference to the dict in which it should
+ # store info
+ self._state = State(self.scope["state"])
+ return self._state
+
+ def url_for(self, name: str, /, **path_params: typing.Any) -> URL:
+ url_path_provider: Router | Starlette | None = self.scope.get("router") or self.scope.get("app")
+ if url_path_provider is None:
+ raise RuntimeError("The `url_for` method can only be used inside a Starlette application or with a router.")
+ url_path = url_path_provider.url_path_for(name, **path_params)
+ return url_path.make_absolute_url(base_url=self.base_url)
+
+
+async def empty_receive() -> typing.NoReturn:
+ raise RuntimeError("Receive channel has not been made available")
+
+
+async def empty_send(message: Message) -> typing.NoReturn:
+ raise RuntimeError("Send channel has not been made available")
+
+
+class Request(HTTPConnection):
+ _form: FormData | None
+
+ def __init__(self, scope: Scope, receive: Receive = empty_receive, send: Send = empty_send):
+ super().__init__(scope)
+ assert scope["type"] == "http"
+ self._receive = receive
+ self._send = send
+ self._stream_consumed = False
+ self._is_disconnected = False
+ self._form = None
+
+ @property
+ def method(self) -> str:
+ return typing.cast(str, self.scope["method"])
+
+ @property
+ def receive(self) -> Receive:
+ return self._receive
+
+ async def stream(self) -> typing.AsyncGenerator[bytes, None]:
+ if hasattr(self, "_body"):
+ yield self._body
+ yield b""
+ return
+ if self._stream_consumed:
+ raise RuntimeError("Stream consumed")
+ while not self._stream_consumed:
+ message = await self._receive()
+ if message["type"] == "http.request":
+ body = message.get("body", b"")
+ if not message.get("more_body", False):
+ self._stream_consumed = True
+ if body:
+ yield body
+ elif message["type"] == "http.disconnect": # pragma: no branch
+ self._is_disconnected = True
+ raise ClientDisconnect()
+ yield b""
+
+ async def body(self) -> bytes:
+ if not hasattr(self, "_body"):
+ chunks: list[bytes] = []
+ async for chunk in self.stream():
+ chunks.append(chunk)
+ self._body = b"".join(chunks)
+ return self._body
+
+ async def json(self) -> typing.Any:
+ if not hasattr(self, "_json"): # pragma: no branch
+ body = await self.body()
+ self._json = json.loads(body)
+ return self._json
+
+ async def _get_form(
+ self,
+ *,
+ max_files: int | float = 1000,
+ max_fields: int | float = 1000,
+ max_part_size: int = 1024 * 1024,
+ ) -> FormData:
+ if self._form is None: # pragma: no branch
+ assert parse_options_header is not None, (
+ "The `python-multipart` library must be installed to use form parsing."
+ )
+ content_type_header = self.headers.get("Content-Type")
+ content_type: bytes
+ content_type, _ = parse_options_header(content_type_header)
+ if content_type == b"multipart/form-data":
+ try:
+ multipart_parser = MultiPartParser(
+ self.headers,
+ self.stream(),
+ max_files=max_files,
+ max_fields=max_fields,
+ max_part_size=max_part_size,
+ )
+ self._form = await multipart_parser.parse()
+ except MultiPartException as exc:
+ if "app" in self.scope:
+ raise HTTPException(status_code=400, detail=exc.message)
+ raise exc
+ elif content_type == b"application/x-www-form-urlencoded":
+ form_parser = FormParser(self.headers, self.stream())
+ self._form = await form_parser.parse()
+ else:
+ self._form = FormData()
+ return self._form
+
+ def form(
+ self,
+ *,
+ max_files: int | float = 1000,
+ max_fields: int | float = 1000,
+ max_part_size: int = 1024 * 1024,
+ ) -> AwaitableOrContextManager[FormData]:
+ return AwaitableOrContextManagerWrapper(
+ self._get_form(max_files=max_files, max_fields=max_fields, max_part_size=max_part_size)
+ )
+
+ async def close(self) -> None:
+ if self._form is not None: # pragma: no branch
+ await self._form.close()
+
+ async def is_disconnected(self) -> bool:
+ if not self._is_disconnected:
+ message: Message = {}
+
+ # If message isn't immediately available, move on
+ with anyio.CancelScope() as cs:
+ cs.cancel()
+ message = await self._receive()
+
+ if message.get("type") == "http.disconnect":
+ self._is_disconnected = True
+
+ return self._is_disconnected
+
+ async def send_push_promise(self, path: str) -> None:
+ if "http.response.push" in self.scope.get("extensions", {}):
+ raw_headers: list[tuple[bytes, bytes]] = []
+ for name in SERVER_PUSH_HEADERS_TO_COPY:
+ for value in self.headers.getlist(name):
+ raw_headers.append((name.encode("latin-1"), value.encode("latin-1")))
+ await self._send({"type": "http.response.push", "path": path, "headers": raw_headers})
diff --git a/.venv/lib/python3.12/site-packages/starlette/responses.py b/.venv/lib/python3.12/site-packages/starlette/responses.py
new file mode 100644
index 00000000..81e89fae
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/starlette/responses.py
@@ -0,0 +1,536 @@
+from __future__ import annotations
+
+import hashlib
+import http.cookies
+import json
+import os
+import re
+import stat
+import typing
+import warnings
+from datetime import datetime
+from email.utils import format_datetime, formatdate
+from functools import partial
+from mimetypes import guess_type
+from secrets import token_hex
+from urllib.parse import quote
+
+import anyio
+import anyio.to_thread
+
+from starlette._utils import collapse_excgroups
+from starlette.background import BackgroundTask
+from starlette.concurrency import iterate_in_threadpool
+from starlette.datastructures import URL, Headers, MutableHeaders
+from starlette.requests import ClientDisconnect
+from starlette.types import Receive, Scope, Send
+
+
+class Response:
+ media_type = None
+ charset = "utf-8"
+
+ def __init__(
+ self,
+ content: typing.Any = None,
+ status_code: int = 200,
+ headers: typing.Mapping[str, str] | None = None,
+ media_type: str | None = None,
+ background: BackgroundTask | None = None,
+ ) -> None:
+ self.status_code = status_code
+ if media_type is not None:
+ self.media_type = media_type
+ self.background = background
+ self.body = self.render(content)
+ self.init_headers(headers)
+
+ def render(self, content: typing.Any) -> bytes | memoryview:
+ if content is None:
+ return b""
+ if isinstance(content, (bytes, memoryview)):
+ return content
+ return content.encode(self.charset) # type: ignore
+
+ def init_headers(self, headers: typing.Mapping[str, str] | None = None) -> None:
+ if headers is None:
+ raw_headers: list[tuple[bytes, bytes]] = []
+ populate_content_length = True
+ populate_content_type = True
+ else:
+ raw_headers = [(k.lower().encode("latin-1"), v.encode("latin-1")) for k, v in headers.items()]
+ keys = [h[0] for h in raw_headers]
+ populate_content_length = b"content-length" not in keys
+ populate_content_type = b"content-type" not in keys
+
+ body = getattr(self, "body", None)
+ if (
+ body is not None
+ and populate_content_length
+ and not (self.status_code < 200 or self.status_code in (204, 304))
+ ):
+ content_length = str(len(body))
+ raw_headers.append((b"content-length", content_length.encode("latin-1")))
+
+ content_type = self.media_type
+ if content_type is not None and populate_content_type:
+ if content_type.startswith("text/") and "charset=" not in content_type.lower():
+ content_type += "; charset=" + self.charset
+ raw_headers.append((b"content-type", content_type.encode("latin-1")))
+
+ self.raw_headers = raw_headers
+
+ @property
+ def headers(self) -> MutableHeaders:
+ if not hasattr(self, "_headers"):
+ self._headers = MutableHeaders(raw=self.raw_headers)
+ return self._headers
+
+ def set_cookie(
+ self,
+ key: str,
+ value: str = "",
+ max_age: int | None = None,
+ expires: datetime | str | int | None = None,
+ path: str | None = "/",
+ domain: str | None = None,
+ secure: bool = False,
+ httponly: bool = False,
+ samesite: typing.Literal["lax", "strict", "none"] | None = "lax",
+ ) -> None:
+ cookie: http.cookies.BaseCookie[str] = http.cookies.SimpleCookie()
+ cookie[key] = value
+ if max_age is not None:
+ cookie[key]["max-age"] = max_age
+ if expires is not None:
+ if isinstance(expires, datetime):
+ cookie[key]["expires"] = format_datetime(expires, usegmt=True)
+ else:
+ cookie[key]["expires"] = expires
+ if path is not None:
+ cookie[key]["path"] = path
+ if domain is not None:
+ cookie[key]["domain"] = domain
+ if secure:
+ cookie[key]["secure"] = True
+ if httponly:
+ cookie[key]["httponly"] = True
+ if samesite is not None:
+ assert samesite.lower() in [
+ "strict",
+ "lax",
+ "none",
+ ], "samesite must be either 'strict', 'lax' or 'none'"
+ cookie[key]["samesite"] = samesite
+ cookie_val = cookie.output(header="").strip()
+ self.raw_headers.append((b"set-cookie", cookie_val.encode("latin-1")))
+
+ def delete_cookie(
+ self,
+ key: str,
+ path: str = "/",
+ domain: str | None = None,
+ secure: bool = False,
+ httponly: bool = False,
+ samesite: typing.Literal["lax", "strict", "none"] | None = "lax",
+ ) -> None:
+ self.set_cookie(
+ key,
+ max_age=0,
+ expires=0,
+ path=path,
+ domain=domain,
+ secure=secure,
+ httponly=httponly,
+ samesite=samesite,
+ )
+
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
+ prefix = "websocket." if scope["type"] == "websocket" else ""
+ await send(
+ {
+ "type": prefix + "http.response.start",
+ "status": self.status_code,
+ "headers": self.raw_headers,
+ }
+ )
+ await send({"type": prefix + "http.response.body", "body": self.body})
+
+ if self.background is not None:
+ await self.background()
+
+
+class HTMLResponse(Response):
+ media_type = "text/html"
+
+
+class PlainTextResponse(Response):
+ media_type = "text/plain"
+
+
+class JSONResponse(Response):
+ media_type = "application/json"
+
+ def __init__(
+ self,
+ content: typing.Any,
+ status_code: int = 200,
+ headers: typing.Mapping[str, str] | None = None,
+ media_type: str | None = None,
+ background: BackgroundTask | None = None,
+ ) -> None:
+ super().__init__(content, status_code, headers, media_type, background)
+
+ def render(self, content: typing.Any) -> bytes:
+ return json.dumps(
+ content,
+ ensure_ascii=False,
+ allow_nan=False,
+ indent=None,
+ separators=(",", ":"),
+ ).encode("utf-8")
+
+
+class RedirectResponse(Response):
+ def __init__(
+ self,
+ url: str | URL,
+ status_code: int = 307,
+ headers: typing.Mapping[str, str] | None = None,
+ background: BackgroundTask | None = None,
+ ) -> None:
+ super().__init__(content=b"", status_code=status_code, headers=headers, background=background)
+ self.headers["location"] = quote(str(url), safe=":/%#?=@[]!$&'()*+,;")
+
+
+Content = typing.Union[str, bytes, memoryview]
+SyncContentStream = typing.Iterable[Content]
+AsyncContentStream = typing.AsyncIterable[Content]
+ContentStream = typing.Union[AsyncContentStream, SyncContentStream]
+
+
+class StreamingResponse(Response):
+ body_iterator: AsyncContentStream
+
+ def __init__(
+ self,
+ content: ContentStream,
+ status_code: int = 200,
+ headers: typing.Mapping[str, str] | None = None,
+ media_type: str | None = None,
+ background: BackgroundTask | None = None,
+ ) -> None:
+ if isinstance(content, typing.AsyncIterable):
+ self.body_iterator = content
+ else:
+ self.body_iterator = iterate_in_threadpool(content)
+ self.status_code = status_code
+ self.media_type = self.media_type if media_type is None else media_type
+ self.background = background
+ self.init_headers(headers)
+
+ async def listen_for_disconnect(self, receive: Receive) -> None:
+ while True:
+ message = await receive()
+ if message["type"] == "http.disconnect":
+ break
+
+ async def stream_response(self, send: Send) -> None:
+ await send(
+ {
+ "type": "http.response.start",
+ "status": self.status_code,
+ "headers": self.raw_headers,
+ }
+ )
+ async for chunk in self.body_iterator:
+ if not isinstance(chunk, (bytes, memoryview)):
+ chunk = chunk.encode(self.charset)
+ await send({"type": "http.response.body", "body": chunk, "more_body": True})
+
+ await send({"type": "http.response.body", "body": b"", "more_body": False})
+
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
+ spec_version = tuple(map(int, scope.get("asgi", {}).get("spec_version", "2.0").split(".")))
+
+ if spec_version >= (2, 4):
+ try:
+ await self.stream_response(send)
+ except OSError:
+ raise ClientDisconnect()
+ else:
+ with collapse_excgroups():
+ async with anyio.create_task_group() as task_group:
+
+ async def wrap(func: typing.Callable[[], typing.Awaitable[None]]) -> None:
+ await func()
+ task_group.cancel_scope.cancel()
+
+ task_group.start_soon(wrap, partial(self.stream_response, send))
+ await wrap(partial(self.listen_for_disconnect, receive))
+
+ if self.background is not None:
+ await self.background()
+
+
+class MalformedRangeHeader(Exception):
+ def __init__(self, content: str = "Malformed range header.") -> None:
+ self.content = content
+
+
+class RangeNotSatisfiable(Exception):
+ def __init__(self, max_size: int) -> None:
+ self.max_size = max_size
+
+
+_RANGE_PATTERN = re.compile(r"(\d*)-(\d*)")
+
+
+class FileResponse(Response):
+ chunk_size = 64 * 1024
+
+ def __init__(
+ self,
+ path: str | os.PathLike[str],
+ status_code: int = 200,
+ headers: typing.Mapping[str, str] | None = None,
+ media_type: str | None = None,
+ background: BackgroundTask | None = None,
+ filename: str | None = None,
+ stat_result: os.stat_result | None = None,
+ method: str | None = None,
+ content_disposition_type: str = "attachment",
+ ) -> None:
+ self.path = path
+ self.status_code = status_code
+ self.filename = filename
+ if method is not None:
+ warnings.warn(
+ "The 'method' parameter is not used, and it will be removed.",
+ DeprecationWarning,
+ )
+ if media_type is None:
+ media_type = guess_type(filename or path)[0] or "text/plain"
+ self.media_type = media_type
+ self.background = background
+ self.init_headers(headers)
+ self.headers.setdefault("accept-ranges", "bytes")
+ if self.filename is not None:
+ content_disposition_filename = quote(self.filename)
+ if content_disposition_filename != self.filename:
+ content_disposition = f"{content_disposition_type}; filename*=utf-8''{content_disposition_filename}"
+ else:
+ content_disposition = f'{content_disposition_type}; filename="{self.filename}"'
+ self.headers.setdefault("content-disposition", content_disposition)
+ self.stat_result = stat_result
+ if stat_result is not None:
+ self.set_stat_headers(stat_result)
+
+ def set_stat_headers(self, stat_result: os.stat_result) -> None:
+ content_length = str(stat_result.st_size)
+ last_modified = formatdate(stat_result.st_mtime, usegmt=True)
+ etag_base = str(stat_result.st_mtime) + "-" + str(stat_result.st_size)
+ etag = f'"{hashlib.md5(etag_base.encode(), usedforsecurity=False).hexdigest()}"'
+
+ self.headers.setdefault("content-length", content_length)
+ self.headers.setdefault("last-modified", last_modified)
+ self.headers.setdefault("etag", etag)
+
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
+ send_header_only: bool = scope["method"].upper() == "HEAD"
+ if self.stat_result is None:
+ try:
+ stat_result = await anyio.to_thread.run_sync(os.stat, self.path)
+ self.set_stat_headers(stat_result)
+ except FileNotFoundError:
+ raise RuntimeError(f"File at path {self.path} does not exist.")
+ else:
+ mode = stat_result.st_mode
+ if not stat.S_ISREG(mode):
+ raise RuntimeError(f"File at path {self.path} is not a file.")
+ else:
+ stat_result = self.stat_result
+
+ headers = Headers(scope=scope)
+ http_range = headers.get("range")
+ http_if_range = headers.get("if-range")
+
+ if http_range is None or (http_if_range is not None and not self._should_use_range(http_if_range)):
+ await self._handle_simple(send, send_header_only)
+ else:
+ try:
+ ranges = self._parse_range_header(http_range, stat_result.st_size)
+ except MalformedRangeHeader as exc:
+ return await PlainTextResponse(exc.content, status_code=400)(scope, receive, send)
+ except RangeNotSatisfiable as exc:
+ response = PlainTextResponse(status_code=416, headers={"Content-Range": f"*/{exc.max_size}"})
+ return await response(scope, receive, send)
+
+ if len(ranges) == 1:
+ start, end = ranges[0]
+ await self._handle_single_range(send, start, end, stat_result.st_size, send_header_only)
+ else:
+ await self._handle_multiple_ranges(send, ranges, stat_result.st_size, send_header_only)
+
+ if self.background is not None:
+ await self.background()
+
+ async def _handle_simple(self, send: Send, send_header_only: bool) -> None:
+ await send({"type": "http.response.start", "status": self.status_code, "headers": self.raw_headers})
+ if send_header_only:
+ await send({"type": "http.response.body", "body": b"", "more_body": False})
+ else:
+ async with await anyio.open_file(self.path, mode="rb") as file:
+ more_body = True
+ while more_body:
+ chunk = await file.read(self.chunk_size)
+ more_body = len(chunk) == self.chunk_size
+ await send({"type": "http.response.body", "body": chunk, "more_body": more_body})
+
+ async def _handle_single_range(
+ self, send: Send, start: int, end: int, file_size: int, send_header_only: bool
+ ) -> None:
+ self.headers["content-range"] = f"bytes {start}-{end - 1}/{file_size}"
+ self.headers["content-length"] = str(end - start)
+ await send({"type": "http.response.start", "status": 206, "headers": self.raw_headers})
+ if send_header_only:
+ await send({"type": "http.response.body", "body": b"", "more_body": False})
+ else:
+ async with await anyio.open_file(self.path, mode="rb") as file:
+ await file.seek(start)
+ more_body = True
+ while more_body:
+ chunk = await file.read(min(self.chunk_size, end - start))
+ start += len(chunk)
+ more_body = len(chunk) == self.chunk_size and start < end
+ await send({"type": "http.response.body", "body": chunk, "more_body": more_body})
+
+ async def _handle_multiple_ranges(
+ self,
+ send: Send,
+ ranges: list[tuple[int, int]],
+ file_size: int,
+ send_header_only: bool,
+ ) -> None:
+ # In firefox and chrome, they use boundary with 95-96 bits entropy (that's roughly 13 bytes).
+ boundary = token_hex(13)
+ content_length, header_generator = self.generate_multipart(
+ ranges, boundary, file_size, self.headers["content-type"]
+ )
+ self.headers["content-range"] = f"multipart/byteranges; boundary={boundary}"
+ self.headers["content-length"] = str(content_length)
+ await send({"type": "http.response.start", "status": 206, "headers": self.raw_headers})
+ if send_header_only:
+ await send({"type": "http.response.body", "body": b"", "more_body": False})
+ else:
+ async with await anyio.open_file(self.path, mode="rb") as file:
+ for start, end in ranges:
+ await send({"type": "http.response.body", "body": header_generator(start, end), "more_body": True})
+ await file.seek(start)
+ while start < end:
+ chunk = await file.read(min(self.chunk_size, end - start))
+ start += len(chunk)
+ await send({"type": "http.response.body", "body": chunk, "more_body": True})
+ await send({"type": "http.response.body", "body": b"\n", "more_body": True})
+ await send(
+ {
+ "type": "http.response.body",
+ "body": f"\n--{boundary}--\n".encode("latin-1"),
+ "more_body": False,
+ }
+ )
+
+ def _should_use_range(self, http_if_range: str) -> bool:
+ return http_if_range == self.headers["last-modified"] or http_if_range == self.headers["etag"]
+
+ @staticmethod
+ def _parse_range_header(http_range: str, file_size: int) -> list[tuple[int, int]]:
+ ranges: list[tuple[int, int]] = []
+ try:
+ units, range_ = http_range.split("=", 1)
+ except ValueError:
+ raise MalformedRangeHeader()
+
+ units = units.strip().lower()
+
+ if units != "bytes":
+ raise MalformedRangeHeader("Only support bytes range")
+
+ ranges = [
+ (
+ int(_[0]) if _[0] else file_size - int(_[1]),
+ int(_[1]) + 1 if _[0] and _[1] and int(_[1]) < file_size else file_size,
+ )
+ for _ in _RANGE_PATTERN.findall(range_)
+ if _ != ("", "")
+ ]
+
+ if len(ranges) == 0:
+ raise MalformedRangeHeader("Range header: range must be requested")
+
+ if any(not (0 <= start < file_size) for start, _ in ranges):
+ raise RangeNotSatisfiable(file_size)
+
+ if any(start > end for start, end in ranges):
+ raise MalformedRangeHeader("Range header: start must be less than end")
+
+ if len(ranges) == 1:
+ return ranges
+
+ # Merge ranges
+ result: list[tuple[int, int]] = []
+ for start, end in ranges:
+ for p in range(len(result)):
+ p_start, p_end = result[p]
+ if start > p_end:
+ continue
+ elif end < p_start:
+ result.insert(p, (start, end)) # THIS IS NOT REACHED!
+ break
+ else:
+ result[p] = (min(start, p_start), max(end, p_end))
+ break
+ else:
+ result.append((start, end))
+
+ return result
+
+ def generate_multipart(
+ self,
+ ranges: typing.Sequence[tuple[int, int]],
+ boundary: str,
+ max_size: int,
+ content_type: str,
+ ) -> tuple[int, typing.Callable[[int, int], bytes]]:
+ r"""
+ Multipart response headers generator.
+
+ ```
+ --{boundary}\n
+ Content-Type: {content_type}\n
+ Content-Range: bytes {start}-{end-1}/{max_size}\n
+ \n
+ ..........content...........\n
+ --{boundary}\n
+ Content-Type: {content_type}\n
+ Content-Range: bytes {start}-{end-1}/{max_size}\n
+ \n
+ ..........content...........\n
+ --{boundary}--\n
+ ```
+ """
+ boundary_len = len(boundary)
+ static_header_part_len = 44 + boundary_len + len(content_type) + len(str(max_size))
+ content_length = sum(
+ (len(str(start)) + len(str(end - 1)) + static_header_part_len) # Headers
+ + (end - start) # Content
+ for start, end in ranges
+ ) + (
+ 5 + boundary_len # --boundary--\n
+ )
+ return (
+ content_length,
+ lambda start, end: (
+ f"--{boundary}\nContent-Type: {content_type}\nContent-Range: bytes {start}-{end - 1}/{max_size}\n\n"
+ ).encode("latin-1"),
+ )
diff --git a/.venv/lib/python3.12/site-packages/starlette/routing.py b/.venv/lib/python3.12/site-packages/starlette/routing.py
new file mode 100644
index 00000000..add7df0c
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/starlette/routing.py
@@ -0,0 +1,874 @@
+from __future__ import annotations
+
+import contextlib
+import functools
+import inspect
+import re
+import traceback
+import types
+import typing
+import warnings
+from contextlib import asynccontextmanager
+from enum import Enum
+
+from starlette._exception_handler import wrap_app_handling_exceptions
+from starlette._utils import get_route_path, is_async_callable
+from starlette.concurrency import run_in_threadpool
+from starlette.convertors import CONVERTOR_TYPES, Convertor
+from starlette.datastructures import URL, Headers, URLPath
+from starlette.exceptions import HTTPException
+from starlette.middleware import Middleware
+from starlette.requests import Request
+from starlette.responses import PlainTextResponse, RedirectResponse, Response
+from starlette.types import ASGIApp, Lifespan, Receive, Scope, Send
+from starlette.websockets import WebSocket, WebSocketClose
+
+
+class NoMatchFound(Exception):
+ """
+ Raised by `.url_for(name, **path_params)` and `.url_path_for(name, **path_params)`
+ if no matching route exists.
+ """
+
+ def __init__(self, name: str, path_params: dict[str, typing.Any]) -> None:
+ params = ", ".join(list(path_params.keys()))
+ super().__init__(f'No route exists for name "{name}" and params "{params}".')
+
+
+class Match(Enum):
+ NONE = 0
+ PARTIAL = 1
+ FULL = 2
+
+
+def iscoroutinefunction_or_partial(obj: typing.Any) -> bool: # pragma: no cover
+ """
+ Correctly determines if an object is a coroutine function,
+ including those wrapped in functools.partial objects.
+ """
+ warnings.warn(
+ "iscoroutinefunction_or_partial is deprecated, and will be removed in a future release.",
+ DeprecationWarning,
+ )
+ while isinstance(obj, functools.partial):
+ obj = obj.func
+ return inspect.iscoroutinefunction(obj)
+
+
+def request_response(
+ func: typing.Callable[[Request], typing.Awaitable[Response] | Response],
+) -> ASGIApp:
+ """
+ Takes a function or coroutine `func(request) -> response`,
+ and returns an ASGI application.
+ """
+ f: typing.Callable[[Request], typing.Awaitable[Response]] = (
+ func if is_async_callable(func) else functools.partial(run_in_threadpool, func) # type:ignore
+ )
+
+ async def app(scope: Scope, receive: Receive, send: Send) -> None:
+ request = Request(scope, receive, send)
+
+ async def app(scope: Scope, receive: Receive, send: Send) -> None:
+ response = await f(request)
+ await response(scope, receive, send)
+
+ await wrap_app_handling_exceptions(app, request)(scope, receive, send)
+
+ return app
+
+
+def websocket_session(
+ func: typing.Callable[[WebSocket], typing.Awaitable[None]],
+) -> ASGIApp:
+ """
+ Takes a coroutine `func(session)`, and returns an ASGI application.
+ """
+ # assert asyncio.iscoroutinefunction(func), "WebSocket endpoints must be async"
+
+ async def app(scope: Scope, receive: Receive, send: Send) -> None:
+ session = WebSocket(scope, receive=receive, send=send)
+
+ async def app(scope: Scope, receive: Receive, send: Send) -> None:
+ await func(session)
+
+ await wrap_app_handling_exceptions(app, session)(scope, receive, send)
+
+ return app
+
+
+def get_name(endpoint: typing.Callable[..., typing.Any]) -> str:
+ return getattr(endpoint, "__name__", endpoint.__class__.__name__)
+
+
+def replace_params(
+ path: str,
+ param_convertors: dict[str, Convertor[typing.Any]],
+ path_params: dict[str, str],
+) -> tuple[str, dict[str, str]]:
+ for key, value in list(path_params.items()):
+ if "{" + key + "}" in path:
+ convertor = param_convertors[key]
+ value = convertor.to_string(value)
+ path = path.replace("{" + key + "}", value)
+ path_params.pop(key)
+ return path, path_params
+
+
+# Match parameters in URL paths, eg. '{param}', and '{param:int}'
+PARAM_REGEX = re.compile("{([a-zA-Z_][a-zA-Z0-9_]*)(:[a-zA-Z_][a-zA-Z0-9_]*)?}")
+
+
+def compile_path(
+ path: str,
+) -> tuple[typing.Pattern[str], str, dict[str, Convertor[typing.Any]]]:
+ """
+ Given a path string, like: "/{username:str}",
+ or a host string, like: "{subdomain}.mydomain.org", return a three-tuple
+ of (regex, format, {param_name:convertor}).
+
+ regex: "/(?P<username>[^/]+)"
+ format: "/{username}"
+ convertors: {"username": StringConvertor()}
+ """
+ is_host = not path.startswith("/")
+
+ path_regex = "^"
+ path_format = ""
+ duplicated_params = set()
+
+ idx = 0
+ param_convertors = {}
+ for match in PARAM_REGEX.finditer(path):
+ param_name, convertor_type = match.groups("str")
+ convertor_type = convertor_type.lstrip(":")
+ assert convertor_type in CONVERTOR_TYPES, f"Unknown path convertor '{convertor_type}'"
+ convertor = CONVERTOR_TYPES[convertor_type]
+
+ path_regex += re.escape(path[idx : match.start()])
+ path_regex += f"(?P<{param_name}>{convertor.regex})"
+
+ path_format += path[idx : match.start()]
+ path_format += "{%s}" % param_name
+
+ if param_name in param_convertors:
+ duplicated_params.add(param_name)
+
+ param_convertors[param_name] = convertor
+
+ idx = match.end()
+
+ if duplicated_params:
+ names = ", ".join(sorted(duplicated_params))
+ ending = "s" if len(duplicated_params) > 1 else ""
+ raise ValueError(f"Duplicated param name{ending} {names} at path {path}")
+
+ if is_host:
+ # Align with `Host.matches()` behavior, which ignores port.
+ hostname = path[idx:].split(":")[0]
+ path_regex += re.escape(hostname) + "$"
+ else:
+ path_regex += re.escape(path[idx:]) + "$"
+
+ path_format += path[idx:]
+
+ return re.compile(path_regex), path_format, param_convertors
+
+
+class BaseRoute:
+ def matches(self, scope: Scope) -> tuple[Match, Scope]:
+ raise NotImplementedError() # pragma: no cover
+
+ def url_path_for(self, name: str, /, **path_params: typing.Any) -> URLPath:
+ raise NotImplementedError() # pragma: no cover
+
+ async def handle(self, scope: Scope, receive: Receive, send: Send) -> None:
+ raise NotImplementedError() # pragma: no cover
+
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
+ """
+ A route may be used in isolation as a stand-alone ASGI app.
+ This is a somewhat contrived case, as they'll almost always be used
+ within a Router, but could be useful for some tooling and minimal apps.
+ """
+ match, child_scope = self.matches(scope)
+ if match == Match.NONE:
+ if scope["type"] == "http":
+ response = PlainTextResponse("Not Found", status_code=404)
+ await response(scope, receive, send)
+ elif scope["type"] == "websocket": # pragma: no branch
+ websocket_close = WebSocketClose()
+ await websocket_close(scope, receive, send)
+ return
+
+ scope.update(child_scope)
+ await self.handle(scope, receive, send)
+
+
+class Route(BaseRoute):
+ def __init__(
+ self,
+ path: str,
+ endpoint: typing.Callable[..., typing.Any],
+ *,
+ methods: list[str] | None = None,
+ name: str | None = None,
+ include_in_schema: bool = True,
+ middleware: typing.Sequence[Middleware] | None = None,
+ ) -> None:
+ assert path.startswith("/"), "Routed paths must start with '/'"
+ self.path = path
+ self.endpoint = endpoint
+ self.name = get_name(endpoint) if name is None else name
+ self.include_in_schema = include_in_schema
+
+ endpoint_handler = endpoint
+ while isinstance(endpoint_handler, functools.partial):
+ endpoint_handler = endpoint_handler.func
+ if inspect.isfunction(endpoint_handler) or inspect.ismethod(endpoint_handler):
+ # Endpoint is function or method. Treat it as `func(request) -> response`.
+ self.app = request_response(endpoint)
+ if methods is None:
+ methods = ["GET"]
+ else:
+ # Endpoint is a class. Treat it as ASGI.
+ self.app = endpoint
+
+ if middleware is not None:
+ for cls, args, kwargs in reversed(middleware):
+ self.app = cls(self.app, *args, **kwargs)
+
+ if methods is None:
+ self.methods = None
+ else:
+ self.methods = {method.upper() for method in methods}
+ if "GET" in self.methods:
+ self.methods.add("HEAD")
+
+ self.path_regex, self.path_format, self.param_convertors = compile_path(path)
+
+ def matches(self, scope: Scope) -> tuple[Match, Scope]:
+ path_params: dict[str, typing.Any]
+ if scope["type"] == "http":
+ route_path = get_route_path(scope)
+ match = self.path_regex.match(route_path)
+ if match:
+ matched_params = match.groupdict()
+ for key, value in matched_params.items():
+ matched_params[key] = self.param_convertors[key].convert(value)
+ path_params = dict(scope.get("path_params", {}))
+ path_params.update(matched_params)
+ child_scope = {"endpoint": self.endpoint, "path_params": path_params}
+ if self.methods and scope["method"] not in self.methods:
+ return Match.PARTIAL, child_scope
+ else:
+ return Match.FULL, child_scope
+ return Match.NONE, {}
+
+ def url_path_for(self, name: str, /, **path_params: typing.Any) -> URLPath:
+ seen_params = set(path_params.keys())
+ expected_params = set(self.param_convertors.keys())
+
+ if name != self.name or seen_params != expected_params:
+ raise NoMatchFound(name, path_params)
+
+ path, remaining_params = replace_params(self.path_format, self.param_convertors, path_params)
+ assert not remaining_params
+ return URLPath(path=path, protocol="http")
+
+ async def handle(self, scope: Scope, receive: Receive, send: Send) -> None:
+ if self.methods and scope["method"] not in self.methods:
+ headers = {"Allow": ", ".join(self.methods)}
+ if "app" in scope:
+ raise HTTPException(status_code=405, headers=headers)
+ else:
+ response = PlainTextResponse("Method Not Allowed", status_code=405, headers=headers)
+ await response(scope, receive, send)
+ else:
+ await self.app(scope, receive, send)
+
+ def __eq__(self, other: typing.Any) -> bool:
+ return (
+ isinstance(other, Route)
+ and self.path == other.path
+ and self.endpoint == other.endpoint
+ and self.methods == other.methods
+ )
+
+ def __repr__(self) -> str:
+ class_name = self.__class__.__name__
+ methods = sorted(self.methods or [])
+ path, name = self.path, self.name
+ return f"{class_name}(path={path!r}, name={name!r}, methods={methods!r})"
+
+
+class WebSocketRoute(BaseRoute):
+ def __init__(
+ self,
+ path: str,
+ endpoint: typing.Callable[..., typing.Any],
+ *,
+ name: str | None = None,
+ middleware: typing.Sequence[Middleware] | None = None,
+ ) -> None:
+ assert path.startswith("/"), "Routed paths must start with '/'"
+ self.path = path
+ self.endpoint = endpoint
+ self.name = get_name(endpoint) if name is None else name
+
+ endpoint_handler = endpoint
+ while isinstance(endpoint_handler, functools.partial):
+ endpoint_handler = endpoint_handler.func
+ if inspect.isfunction(endpoint_handler) or inspect.ismethod(endpoint_handler):
+ # Endpoint is function or method. Treat it as `func(websocket)`.
+ self.app = websocket_session(endpoint)
+ else:
+ # Endpoint is a class. Treat it as ASGI.
+ self.app = endpoint
+
+ if middleware is not None:
+ for cls, args, kwargs in reversed(middleware):
+ self.app = cls(self.app, *args, **kwargs)
+
+ self.path_regex, self.path_format, self.param_convertors = compile_path(path)
+
+ def matches(self, scope: Scope) -> tuple[Match, Scope]:
+ path_params: dict[str, typing.Any]
+ if scope["type"] == "websocket":
+ route_path = get_route_path(scope)
+ match = self.path_regex.match(route_path)
+ if match:
+ matched_params = match.groupdict()
+ for key, value in matched_params.items():
+ matched_params[key] = self.param_convertors[key].convert(value)
+ path_params = dict(scope.get("path_params", {}))
+ path_params.update(matched_params)
+ child_scope = {"endpoint": self.endpoint, "path_params": path_params}
+ return Match.FULL, child_scope
+ return Match.NONE, {}
+
+ def url_path_for(self, name: str, /, **path_params: typing.Any) -> URLPath:
+ seen_params = set(path_params.keys())
+ expected_params = set(self.param_convertors.keys())
+
+ if name != self.name or seen_params != expected_params:
+ raise NoMatchFound(name, path_params)
+
+ path, remaining_params = replace_params(self.path_format, self.param_convertors, path_params)
+ assert not remaining_params
+ return URLPath(path=path, protocol="websocket")
+
+ async def handle(self, scope: Scope, receive: Receive, send: Send) -> None:
+ await self.app(scope, receive, send)
+
+ def __eq__(self, other: typing.Any) -> bool:
+ return isinstance(other, WebSocketRoute) and self.path == other.path and self.endpoint == other.endpoint
+
+ def __repr__(self) -> str:
+ return f"{self.__class__.__name__}(path={self.path!r}, name={self.name!r})"
+
+
+class Mount(BaseRoute):
+ def __init__(
+ self,
+ path: str,
+ app: ASGIApp | None = None,
+ routes: typing.Sequence[BaseRoute] | None = None,
+ name: str | None = None,
+ *,
+ middleware: typing.Sequence[Middleware] | None = None,
+ ) -> None:
+ assert path == "" or path.startswith("/"), "Routed paths must start with '/'"
+ assert app is not None or routes is not None, "Either 'app=...', or 'routes=' must be specified"
+ self.path = path.rstrip("/")
+ if app is not None:
+ self._base_app: ASGIApp = app
+ else:
+ self._base_app = Router(routes=routes)
+ self.app = self._base_app
+ if middleware is not None:
+ for cls, args, kwargs in reversed(middleware):
+ self.app = cls(self.app, *args, **kwargs)
+ self.name = name
+ self.path_regex, self.path_format, self.param_convertors = compile_path(self.path + "/{path:path}")
+
+ @property
+ def routes(self) -> list[BaseRoute]:
+ return getattr(self._base_app, "routes", [])
+
+ def matches(self, scope: Scope) -> tuple[Match, Scope]:
+ path_params: dict[str, typing.Any]
+ if scope["type"] in ("http", "websocket"): # pragma: no branch
+ root_path = scope.get("root_path", "")
+ route_path = get_route_path(scope)
+ match = self.path_regex.match(route_path)
+ if match:
+ matched_params = match.groupdict()
+ for key, value in matched_params.items():
+ matched_params[key] = self.param_convertors[key].convert(value)
+ remaining_path = "/" + matched_params.pop("path")
+ matched_path = route_path[: -len(remaining_path)]
+ path_params = dict(scope.get("path_params", {}))
+ path_params.update(matched_params)
+ child_scope = {
+ "path_params": path_params,
+ # app_root_path will only be set at the top level scope,
+ # initialized with the (optional) value of a root_path
+ # set above/before Starlette. And even though any
+ # mount will have its own child scope with its own respective
+ # root_path, the app_root_path will always be available in all
+ # the child scopes with the same top level value because it's
+ # set only once here with a default, any other child scope will
+ # just inherit that app_root_path default value stored in the
+ # scope. All this is needed to support Request.url_for(), as it
+ # uses the app_root_path to build the URL path.
+ "app_root_path": scope.get("app_root_path", root_path),
+ "root_path": root_path + matched_path,
+ "endpoint": self.app,
+ }
+ return Match.FULL, child_scope
+ return Match.NONE, {}
+
+ def url_path_for(self, name: str, /, **path_params: typing.Any) -> URLPath:
+ if self.name is not None and name == self.name and "path" in path_params:
+ # 'name' matches "<mount_name>".
+ path_params["path"] = path_params["path"].lstrip("/")
+ path, remaining_params = replace_params(self.path_format, self.param_convertors, path_params)
+ if not remaining_params:
+ return URLPath(path=path)
+ elif self.name is None or name.startswith(self.name + ":"):
+ if self.name is None:
+ # No mount name.
+ remaining_name = name
+ else:
+ # 'name' matches "<mount_name>:<child_name>".
+ remaining_name = name[len(self.name) + 1 :]
+ path_kwarg = path_params.get("path")
+ path_params["path"] = ""
+ path_prefix, remaining_params = replace_params(self.path_format, self.param_convertors, path_params)
+ if path_kwarg is not None:
+ remaining_params["path"] = path_kwarg
+ for route in self.routes or []:
+ try:
+ url = route.url_path_for(remaining_name, **remaining_params)
+ return URLPath(path=path_prefix.rstrip("/") + str(url), protocol=url.protocol)
+ except NoMatchFound:
+ pass
+ raise NoMatchFound(name, path_params)
+
+ async def handle(self, scope: Scope, receive: Receive, send: Send) -> None:
+ await self.app(scope, receive, send)
+
+ def __eq__(self, other: typing.Any) -> bool:
+ return isinstance(other, Mount) and self.path == other.path and self.app == other.app
+
+ def __repr__(self) -> str:
+ class_name = self.__class__.__name__
+ name = self.name or ""
+ return f"{class_name}(path={self.path!r}, name={name!r}, app={self.app!r})"
+
+
+class Host(BaseRoute):
+ def __init__(self, host: str, app: ASGIApp, name: str | None = None) -> None:
+ assert not host.startswith("/"), "Host must not start with '/'"
+ self.host = host
+ self.app = app
+ self.name = name
+ self.host_regex, self.host_format, self.param_convertors = compile_path(host)
+
+ @property
+ def routes(self) -> list[BaseRoute]:
+ return getattr(self.app, "routes", [])
+
+ def matches(self, scope: Scope) -> tuple[Match, Scope]:
+ if scope["type"] in ("http", "websocket"): # pragma:no branch
+ headers = Headers(scope=scope)
+ host = headers.get("host", "").split(":")[0]
+ match = self.host_regex.match(host)
+ if match:
+ matched_params = match.groupdict()
+ for key, value in matched_params.items():
+ matched_params[key] = self.param_convertors[key].convert(value)
+ path_params = dict(scope.get("path_params", {}))
+ path_params.update(matched_params)
+ child_scope = {"path_params": path_params, "endpoint": self.app}
+ return Match.FULL, child_scope
+ return Match.NONE, {}
+
+ def url_path_for(self, name: str, /, **path_params: typing.Any) -> URLPath:
+ if self.name is not None and name == self.name and "path" in path_params:
+ # 'name' matches "<mount_name>".
+ path = path_params.pop("path")
+ host, remaining_params = replace_params(self.host_format, self.param_convertors, path_params)
+ if not remaining_params:
+ return URLPath(path=path, host=host)
+ elif self.name is None or name.startswith(self.name + ":"):
+ if self.name is None:
+ # No mount name.
+ remaining_name = name
+ else:
+ # 'name' matches "<mount_name>:<child_name>".
+ remaining_name = name[len(self.name) + 1 :]
+ host, remaining_params = replace_params(self.host_format, self.param_convertors, path_params)
+ for route in self.routes or []:
+ try:
+ url = route.url_path_for(remaining_name, **remaining_params)
+ return URLPath(path=str(url), protocol=url.protocol, host=host)
+ except NoMatchFound:
+ pass
+ raise NoMatchFound(name, path_params)
+
+ async def handle(self, scope: Scope, receive: Receive, send: Send) -> None:
+ await self.app(scope, receive, send)
+
+ def __eq__(self, other: typing.Any) -> bool:
+ return isinstance(other, Host) and self.host == other.host and self.app == other.app
+
+ def __repr__(self) -> str:
+ class_name = self.__class__.__name__
+ name = self.name or ""
+ return f"{class_name}(host={self.host!r}, name={name!r}, app={self.app!r})"
+
+
+_T = typing.TypeVar("_T")
+
+
+class _AsyncLiftContextManager(typing.AsyncContextManager[_T]):
+ def __init__(self, cm: typing.ContextManager[_T]):
+ self._cm = cm
+
+ async def __aenter__(self) -> _T:
+ return self._cm.__enter__()
+
+ async def __aexit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_value: BaseException | None,
+ traceback: types.TracebackType | None,
+ ) -> bool | None:
+ return self._cm.__exit__(exc_type, exc_value, traceback)
+
+
+def _wrap_gen_lifespan_context(
+ lifespan_context: typing.Callable[[typing.Any], typing.Generator[typing.Any, typing.Any, typing.Any]],
+) -> typing.Callable[[typing.Any], typing.AsyncContextManager[typing.Any]]:
+ cmgr = contextlib.contextmanager(lifespan_context)
+
+ @functools.wraps(cmgr)
+ def wrapper(app: typing.Any) -> _AsyncLiftContextManager[typing.Any]:
+ return _AsyncLiftContextManager(cmgr(app))
+
+ return wrapper
+
+
+class _DefaultLifespan:
+ def __init__(self, router: Router):
+ self._router = router
+
+ async def __aenter__(self) -> None:
+ await self._router.startup()
+
+ async def __aexit__(self, *exc_info: object) -> None:
+ await self._router.shutdown()
+
+ def __call__(self: _T, app: object) -> _T:
+ return self
+
+
+class Router:
+ def __init__(
+ self,
+ routes: typing.Sequence[BaseRoute] | None = None,
+ redirect_slashes: bool = True,
+ default: ASGIApp | None = None,
+ on_startup: typing.Sequence[typing.Callable[[], typing.Any]] | None = None,
+ on_shutdown: typing.Sequence[typing.Callable[[], typing.Any]] | None = None,
+ # the generic to Lifespan[AppType] is the type of the top level application
+ # which the router cannot know statically, so we use typing.Any
+ lifespan: Lifespan[typing.Any] | None = None,
+ *,
+ middleware: typing.Sequence[Middleware] | None = None,
+ ) -> None:
+ self.routes = [] if routes is None else list(routes)
+ self.redirect_slashes = redirect_slashes
+ self.default = self.not_found if default is None else default
+ self.on_startup = [] if on_startup is None else list(on_startup)
+ self.on_shutdown = [] if on_shutdown is None else list(on_shutdown)
+
+ if on_startup or on_shutdown:
+ warnings.warn(
+ "The on_startup and on_shutdown parameters are deprecated, and they "
+ "will be removed on version 1.0. Use the lifespan parameter instead. "
+ "See more about it on https://www.starlette.io/lifespan/.",
+ DeprecationWarning,
+ )
+ if lifespan:
+ warnings.warn(
+ "The `lifespan` parameter cannot be used with `on_startup` or "
+ "`on_shutdown`. Both `on_startup` and `on_shutdown` will be "
+ "ignored."
+ )
+
+ if lifespan is None:
+ self.lifespan_context: Lifespan[typing.Any] = _DefaultLifespan(self)
+
+ elif inspect.isasyncgenfunction(lifespan):
+ warnings.warn(
+ "async generator function lifespans are deprecated, "
+ "use an @contextlib.asynccontextmanager function instead",
+ DeprecationWarning,
+ )
+ self.lifespan_context = asynccontextmanager(
+ lifespan,
+ )
+ elif inspect.isgeneratorfunction(lifespan):
+ warnings.warn(
+ "generator function lifespans are deprecated, use an @contextlib.asynccontextmanager function instead",
+ DeprecationWarning,
+ )
+ self.lifespan_context = _wrap_gen_lifespan_context(
+ lifespan,
+ )
+ else:
+ self.lifespan_context = lifespan
+
+ self.middleware_stack = self.app
+ if middleware:
+ for cls, args, kwargs in reversed(middleware):
+ self.middleware_stack = cls(self.middleware_stack, *args, **kwargs)
+
+ async def not_found(self, scope: Scope, receive: Receive, send: Send) -> None:
+ if scope["type"] == "websocket":
+ websocket_close = WebSocketClose()
+ await websocket_close(scope, receive, send)
+ return
+
+ # If we're running inside a starlette application then raise an
+ # exception, so that the configurable exception handler can deal with
+ # returning the response. For plain ASGI apps, just return the response.
+ if "app" in scope:
+ raise HTTPException(status_code=404)
+ else:
+ response = PlainTextResponse("Not Found", status_code=404)
+ await response(scope, receive, send)
+
+ def url_path_for(self, name: str, /, **path_params: typing.Any) -> URLPath:
+ for route in self.routes:
+ try:
+ return route.url_path_for(name, **path_params)
+ except NoMatchFound:
+ pass
+ raise NoMatchFound(name, path_params)
+
+ async def startup(self) -> None:
+ """
+ Run any `.on_startup` event handlers.
+ """
+ for handler in self.on_startup:
+ if is_async_callable(handler):
+ await handler()
+ else:
+ handler()
+
+ async def shutdown(self) -> None:
+ """
+ Run any `.on_shutdown` event handlers.
+ """
+ for handler in self.on_shutdown:
+ if is_async_callable(handler):
+ await handler()
+ else:
+ handler()
+
+ async def lifespan(self, scope: Scope, receive: Receive, send: Send) -> None:
+ """
+ Handle ASGI lifespan messages, which allows us to manage application
+ startup and shutdown events.
+ """
+ started = False
+ app: typing.Any = scope.get("app")
+ await receive()
+ try:
+ async with self.lifespan_context(app) as maybe_state:
+ if maybe_state is not None:
+ if "state" not in scope:
+ raise RuntimeError('The server does not support "state" in the lifespan scope.')
+ scope["state"].update(maybe_state)
+ await send({"type": "lifespan.startup.complete"})
+ started = True
+ await receive()
+ except BaseException:
+ exc_text = traceback.format_exc()
+ if started:
+ await send({"type": "lifespan.shutdown.failed", "message": exc_text})
+ else:
+ await send({"type": "lifespan.startup.failed", "message": exc_text})
+ raise
+ else:
+ await send({"type": "lifespan.shutdown.complete"})
+
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
+ """
+ The main entry point to the Router class.
+ """
+ await self.middleware_stack(scope, receive, send)
+
+ async def app(self, scope: Scope, receive: Receive, send: Send) -> None:
+ assert scope["type"] in ("http", "websocket", "lifespan")
+
+ if "router" not in scope:
+ scope["router"] = self
+
+ if scope["type"] == "lifespan":
+ await self.lifespan(scope, receive, send)
+ return
+
+ partial = None
+
+ for route in self.routes:
+ # Determine if any route matches the incoming scope,
+ # and hand over to the matching route if found.
+ match, child_scope = route.matches(scope)
+ if match == Match.FULL:
+ scope.update(child_scope)
+ await route.handle(scope, receive, send)
+ return
+ elif match == Match.PARTIAL and partial is None:
+ partial = route
+ partial_scope = child_scope
+
+ if partial is not None:
+ #  Handle partial matches. These are cases where an endpoint is
+ # able to handle the request, but is not a preferred option.
+ # We use this in particular to deal with "405 Method Not Allowed".
+ scope.update(partial_scope)
+ await partial.handle(scope, receive, send)
+ return
+
+ route_path = get_route_path(scope)
+ if scope["type"] == "http" and self.redirect_slashes and route_path != "/":
+ redirect_scope = dict(scope)
+ if route_path.endswith("/"):
+ redirect_scope["path"] = redirect_scope["path"].rstrip("/")
+ else:
+ redirect_scope["path"] = redirect_scope["path"] + "/"
+
+ for route in self.routes:
+ match, child_scope = route.matches(redirect_scope)
+ if match != Match.NONE:
+ redirect_url = URL(scope=redirect_scope)
+ response = RedirectResponse(url=str(redirect_url))
+ await response(scope, receive, send)
+ return
+
+ await self.default(scope, receive, send)
+
+ def __eq__(self, other: typing.Any) -> bool:
+ return isinstance(other, Router) and self.routes == other.routes
+
+ def mount(self, path: str, app: ASGIApp, name: str | None = None) -> None: # pragma: no cover
+ route = Mount(path, app=app, name=name)
+ self.routes.append(route)
+
+ def host(self, host: str, app: ASGIApp, name: str | None = None) -> None: # pragma: no cover
+ route = Host(host, app=app, name=name)
+ self.routes.append(route)
+
+ def add_route(
+ self,
+ path: str,
+ endpoint: typing.Callable[[Request], typing.Awaitable[Response] | Response],
+ methods: list[str] | None = None,
+ name: str | None = None,
+ include_in_schema: bool = True,
+ ) -> None: # pragma: no cover
+ route = Route(
+ path,
+ endpoint=endpoint,
+ methods=methods,
+ name=name,
+ include_in_schema=include_in_schema,
+ )
+ self.routes.append(route)
+
+ def add_websocket_route(
+ self,
+ path: str,
+ endpoint: typing.Callable[[WebSocket], typing.Awaitable[None]],
+ name: str | None = None,
+ ) -> None: # pragma: no cover
+ route = WebSocketRoute(path, endpoint=endpoint, name=name)
+ self.routes.append(route)
+
+ def route(
+ self,
+ path: str,
+ methods: list[str] | None = None,
+ name: str | None = None,
+ include_in_schema: bool = True,
+ ) -> typing.Callable: # type: ignore[type-arg]
+ """
+ We no longer document this decorator style API, and its usage is discouraged.
+ Instead you should use the following approach:
+
+ >>> routes = [Route(path, endpoint=...), ...]
+ >>> app = Starlette(routes=routes)
+ """
+ warnings.warn(
+ "The `route` decorator is deprecated, and will be removed in version 1.0.0."
+ "Refer to https://www.starlette.io/routing/#http-routing for the recommended approach.",
+ DeprecationWarning,
+ )
+
+ def decorator(func: typing.Callable) -> typing.Callable: # type: ignore[type-arg]
+ self.add_route(
+ path,
+ func,
+ methods=methods,
+ name=name,
+ include_in_schema=include_in_schema,
+ )
+ return func
+
+ return decorator
+
+ def websocket_route(self, path: str, name: str | None = None) -> typing.Callable: # type: ignore[type-arg]
+ """
+ We no longer document this decorator style API, and its usage is discouraged.
+ Instead you should use the following approach:
+
+ >>> routes = [WebSocketRoute(path, endpoint=...), ...]
+ >>> app = Starlette(routes=routes)
+ """
+ warnings.warn(
+ "The `websocket_route` decorator is deprecated, and will be removed in version 1.0.0. Refer to "
+ "https://www.starlette.io/routing/#websocket-routing for the recommended approach.",
+ DeprecationWarning,
+ )
+
+ def decorator(func: typing.Callable) -> typing.Callable: # type: ignore[type-arg]
+ self.add_websocket_route(path, func, name=name)
+ return func
+
+ return decorator
+
+ def add_event_handler(self, event_type: str, func: typing.Callable[[], typing.Any]) -> None: # pragma: no cover
+ assert event_type in ("startup", "shutdown")
+
+ if event_type == "startup":
+ self.on_startup.append(func)
+ else:
+ self.on_shutdown.append(func)
+
+ def on_event(self, event_type: str) -> typing.Callable: # type: ignore[type-arg]
+ warnings.warn(
+ "The `on_event` decorator is deprecated, and will be removed in version 1.0.0. "
+ "Refer to https://www.starlette.io/lifespan/ for recommended approach.",
+ DeprecationWarning,
+ )
+
+ def decorator(func: typing.Callable) -> typing.Callable: # type: ignore[type-arg]
+ self.add_event_handler(event_type, func)
+ return func
+
+ return decorator
diff --git a/.venv/lib/python3.12/site-packages/starlette/schemas.py b/.venv/lib/python3.12/site-packages/starlette/schemas.py
new file mode 100644
index 00000000..bfc40e2a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/starlette/schemas.py
@@ -0,0 +1,147 @@
+from __future__ import annotations
+
+import inspect
+import re
+import typing
+
+from starlette.requests import Request
+from starlette.responses import Response
+from starlette.routing import BaseRoute, Host, Mount, Route
+
+try:
+ import yaml
+except ModuleNotFoundError: # pragma: no cover
+ yaml = None # type: ignore[assignment]
+
+
+class OpenAPIResponse(Response):
+ media_type = "application/vnd.oai.openapi"
+
+ def render(self, content: typing.Any) -> bytes:
+ assert yaml is not None, "`pyyaml` must be installed to use OpenAPIResponse."
+ assert isinstance(content, dict), "The schema passed to OpenAPIResponse should be a dictionary."
+ return yaml.dump(content, default_flow_style=False).encode("utf-8")
+
+
+class EndpointInfo(typing.NamedTuple):
+ path: str
+ http_method: str
+ func: typing.Callable[..., typing.Any]
+
+
+_remove_converter_pattern = re.compile(r":\w+}")
+
+
+class BaseSchemaGenerator:
+ def get_schema(self, routes: list[BaseRoute]) -> dict[str, typing.Any]:
+ raise NotImplementedError() # pragma: no cover
+
+ def get_endpoints(self, routes: list[BaseRoute]) -> list[EndpointInfo]:
+ """
+ Given the routes, yields the following information:
+
+ - path
+ eg: /users/
+ - http_method
+ one of 'get', 'post', 'put', 'patch', 'delete', 'options'
+ - func
+ method ready to extract the docstring
+ """
+ endpoints_info: list[EndpointInfo] = []
+
+ for route in routes:
+ if isinstance(route, (Mount, Host)):
+ routes = route.routes or []
+ if isinstance(route, Mount):
+ path = self._remove_converter(route.path)
+ else:
+ path = ""
+ sub_endpoints = [
+ EndpointInfo(
+ path="".join((path, sub_endpoint.path)),
+ http_method=sub_endpoint.http_method,
+ func=sub_endpoint.func,
+ )
+ for sub_endpoint in self.get_endpoints(routes)
+ ]
+ endpoints_info.extend(sub_endpoints)
+
+ elif not isinstance(route, Route) or not route.include_in_schema:
+ continue
+
+ elif inspect.isfunction(route.endpoint) or inspect.ismethod(route.endpoint):
+ path = self._remove_converter(route.path)
+ for method in route.methods or ["GET"]:
+ if method == "HEAD":
+ continue
+ endpoints_info.append(EndpointInfo(path, method.lower(), route.endpoint))
+ else:
+ path = self._remove_converter(route.path)
+ for method in ["get", "post", "put", "patch", "delete", "options"]:
+ if not hasattr(route.endpoint, method):
+ continue
+ func = getattr(route.endpoint, method)
+ endpoints_info.append(EndpointInfo(path, method.lower(), func))
+
+ return endpoints_info
+
+ def _remove_converter(self, path: str) -> str:
+ """
+ Remove the converter from the path.
+ For example, a route like this:
+ Route("/users/{id:int}", endpoint=get_user, methods=["GET"])
+ Should be represented as `/users/{id}` in the OpenAPI schema.
+ """
+ return _remove_converter_pattern.sub("}", path)
+
+ def parse_docstring(self, func_or_method: typing.Callable[..., typing.Any]) -> dict[str, typing.Any]:
+ """
+ Given a function, parse the docstring as YAML and return a dictionary of info.
+ """
+ docstring = func_or_method.__doc__
+ if not docstring:
+ return {}
+
+ assert yaml is not None, "`pyyaml` must be installed to use parse_docstring."
+
+ # We support having regular docstrings before the schema
+ # definition. Here we return just the schema part from
+ # the docstring.
+ docstring = docstring.split("---")[-1]
+
+ parsed = yaml.safe_load(docstring)
+
+ if not isinstance(parsed, dict):
+ # A regular docstring (not yaml formatted) can return
+ # a simple string here, which wouldn't follow the schema.
+ return {}
+
+ return parsed
+
+ def OpenAPIResponse(self, request: Request) -> Response:
+ routes = request.app.routes
+ schema = self.get_schema(routes=routes)
+ return OpenAPIResponse(schema)
+
+
+class SchemaGenerator(BaseSchemaGenerator):
+ def __init__(self, base_schema: dict[str, typing.Any]) -> None:
+ self.base_schema = base_schema
+
+ def get_schema(self, routes: list[BaseRoute]) -> dict[str, typing.Any]:
+ schema = dict(self.base_schema)
+ schema.setdefault("paths", {})
+ endpoints_info = self.get_endpoints(routes)
+
+ for endpoint in endpoints_info:
+ parsed = self.parse_docstring(endpoint.func)
+
+ if not parsed:
+ continue
+
+ if endpoint.path not in schema["paths"]:
+ schema["paths"][endpoint.path] = {}
+
+ schema["paths"][endpoint.path][endpoint.http_method] = parsed
+
+ return schema
diff --git a/.venv/lib/python3.12/site-packages/starlette/staticfiles.py b/.venv/lib/python3.12/site-packages/starlette/staticfiles.py
new file mode 100644
index 00000000..637da648
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/starlette/staticfiles.py
@@ -0,0 +1,220 @@
+from __future__ import annotations
+
+import errno
+import importlib.util
+import os
+import stat
+import typing
+from email.utils import parsedate
+
+import anyio
+import anyio.to_thread
+
+from starlette._utils import get_route_path
+from starlette.datastructures import URL, Headers
+from starlette.exceptions import HTTPException
+from starlette.responses import FileResponse, RedirectResponse, Response
+from starlette.types import Receive, Scope, Send
+
+PathLike = typing.Union[str, "os.PathLike[str]"]
+
+
+class NotModifiedResponse(Response):
+ NOT_MODIFIED_HEADERS = (
+ "cache-control",
+ "content-location",
+ "date",
+ "etag",
+ "expires",
+ "vary",
+ )
+
+ def __init__(self, headers: Headers):
+ super().__init__(
+ status_code=304,
+ headers={name: value for name, value in headers.items() if name in self.NOT_MODIFIED_HEADERS},
+ )
+
+
+class StaticFiles:
+ def __init__(
+ self,
+ *,
+ directory: PathLike | None = None,
+ packages: list[str | tuple[str, str]] | None = None,
+ html: bool = False,
+ check_dir: bool = True,
+ follow_symlink: bool = False,
+ ) -> None:
+ self.directory = directory
+ self.packages = packages
+ self.all_directories = self.get_directories(directory, packages)
+ self.html = html
+ self.config_checked = False
+ self.follow_symlink = follow_symlink
+ if check_dir and directory is not None and not os.path.isdir(directory):
+ raise RuntimeError(f"Directory '{directory}' does not exist")
+
+ def get_directories(
+ self,
+ directory: PathLike | None = None,
+ packages: list[str | tuple[str, str]] | None = None,
+ ) -> list[PathLike]:
+ """
+ Given `directory` and `packages` arguments, return a list of all the
+ directories that should be used for serving static files from.
+ """
+ directories = []
+ if directory is not None:
+ directories.append(directory)
+
+ for package in packages or []:
+ if isinstance(package, tuple):
+ package, statics_dir = package
+ else:
+ statics_dir = "statics"
+ spec = importlib.util.find_spec(package)
+ assert spec is not None, f"Package {package!r} could not be found."
+ assert spec.origin is not None, f"Package {package!r} could not be found."
+ package_directory = os.path.normpath(os.path.join(spec.origin, "..", statics_dir))
+ assert os.path.isdir(package_directory), (
+ f"Directory '{statics_dir!r}' in package {package!r} could not be found."
+ )
+ directories.append(package_directory)
+
+ return directories
+
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
+ """
+ The ASGI entry point.
+ """
+ assert scope["type"] == "http"
+
+ if not self.config_checked:
+ await self.check_config()
+ self.config_checked = True
+
+ path = self.get_path(scope)
+ response = await self.get_response(path, scope)
+ await response(scope, receive, send)
+
+ def get_path(self, scope: Scope) -> str:
+ """
+ Given the ASGI scope, return the `path` string to serve up,
+ with OS specific path separators, and any '..', '.' components removed.
+ """
+ route_path = get_route_path(scope)
+ return os.path.normpath(os.path.join(*route_path.split("/")))
+
+ async def get_response(self, path: str, scope: Scope) -> Response:
+ """
+ Returns an HTTP response, given the incoming path, method and request headers.
+ """
+ if scope["method"] not in ("GET", "HEAD"):
+ raise HTTPException(status_code=405)
+
+ try:
+ full_path, stat_result = await anyio.to_thread.run_sync(self.lookup_path, path)
+ except PermissionError:
+ raise HTTPException(status_code=401)
+ except OSError as exc:
+ # Filename is too long, so it can't be a valid static file.
+ if exc.errno == errno.ENAMETOOLONG:
+ raise HTTPException(status_code=404)
+
+ raise exc
+
+ if stat_result and stat.S_ISREG(stat_result.st_mode):
+ # We have a static file to serve.
+ return self.file_response(full_path, stat_result, scope)
+
+ elif stat_result and stat.S_ISDIR(stat_result.st_mode) and self.html:
+ # We're in HTML mode, and have got a directory URL.
+ # Check if we have 'index.html' file to serve.
+ index_path = os.path.join(path, "index.html")
+ full_path, stat_result = await anyio.to_thread.run_sync(self.lookup_path, index_path)
+ if stat_result is not None and stat.S_ISREG(stat_result.st_mode):
+ if not scope["path"].endswith("/"):
+ # Directory URLs should redirect to always end in "/".
+ url = URL(scope=scope)
+ url = url.replace(path=url.path + "/")
+ return RedirectResponse(url=url)
+ return self.file_response(full_path, stat_result, scope)
+
+ if self.html:
+ # Check for '404.html' if we're in HTML mode.
+ full_path, stat_result = await anyio.to_thread.run_sync(self.lookup_path, "404.html")
+ if stat_result and stat.S_ISREG(stat_result.st_mode):
+ return FileResponse(full_path, stat_result=stat_result, status_code=404)
+ raise HTTPException(status_code=404)
+
+ def lookup_path(self, path: str) -> tuple[str, os.stat_result | None]:
+ for directory in self.all_directories:
+ joined_path = os.path.join(directory, path)
+ if self.follow_symlink:
+ full_path = os.path.abspath(joined_path)
+ directory = os.path.abspath(directory)
+ else:
+ full_path = os.path.realpath(joined_path)
+ directory = os.path.realpath(directory)
+ if os.path.commonpath([full_path, directory]) != str(directory):
+ # Don't allow misbehaving clients to break out of the static files directory.
+ continue
+ try:
+ return full_path, os.stat(full_path)
+ except (FileNotFoundError, NotADirectoryError):
+ continue
+ return "", None
+
+ def file_response(
+ self,
+ full_path: PathLike,
+ stat_result: os.stat_result,
+ scope: Scope,
+ status_code: int = 200,
+ ) -> Response:
+ request_headers = Headers(scope=scope)
+
+ response = FileResponse(full_path, status_code=status_code, stat_result=stat_result)
+ if self.is_not_modified(response.headers, request_headers):
+ return NotModifiedResponse(response.headers)
+ return response
+
+ async def check_config(self) -> None:
+ """
+ Perform a one-off configuration check that StaticFiles is actually
+ pointed at a directory, so that we can raise loud errors rather than
+ just returning 404 responses.
+ """
+ if self.directory is None:
+ return
+
+ try:
+ stat_result = await anyio.to_thread.run_sync(os.stat, self.directory)
+ except FileNotFoundError:
+ raise RuntimeError(f"StaticFiles directory '{self.directory}' does not exist.")
+ if not (stat.S_ISDIR(stat_result.st_mode) or stat.S_ISLNK(stat_result.st_mode)):
+ raise RuntimeError(f"StaticFiles path '{self.directory}' is not a directory.")
+
+ def is_not_modified(self, response_headers: Headers, request_headers: Headers) -> bool:
+ """
+ Given the request and response headers, return `True` if an HTTP
+ "Not Modified" response could be returned instead.
+ """
+ try:
+ if_none_match = request_headers["if-none-match"]
+ etag = response_headers["etag"]
+ if etag in [tag.strip(" W/") for tag in if_none_match.split(",")]:
+ return True
+ except KeyError:
+ pass
+
+ try:
+ if_modified_since = parsedate(request_headers["if-modified-since"])
+ last_modified = parsedate(response_headers["last-modified"])
+ if if_modified_since is not None and last_modified is not None and if_modified_since >= last_modified:
+ return True
+ except KeyError:
+ pass
+
+ return False
diff --git a/.venv/lib/python3.12/site-packages/starlette/status.py b/.venv/lib/python3.12/site-packages/starlette/status.py
new file mode 100644
index 00000000..54c1fb7d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/starlette/status.py
@@ -0,0 +1,95 @@
+"""
+HTTP codes
+See HTTP Status Code Registry:
+https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
+
+And RFC 2324 - https://tools.ietf.org/html/rfc2324
+"""
+
+from __future__ import annotations
+
+HTTP_100_CONTINUE = 100
+HTTP_101_SWITCHING_PROTOCOLS = 101
+HTTP_102_PROCESSING = 102
+HTTP_103_EARLY_HINTS = 103
+HTTP_200_OK = 200
+HTTP_201_CREATED = 201
+HTTP_202_ACCEPTED = 202
+HTTP_203_NON_AUTHORITATIVE_INFORMATION = 203
+HTTP_204_NO_CONTENT = 204
+HTTP_205_RESET_CONTENT = 205
+HTTP_206_PARTIAL_CONTENT = 206
+HTTP_207_MULTI_STATUS = 207
+HTTP_208_ALREADY_REPORTED = 208
+HTTP_226_IM_USED = 226
+HTTP_300_MULTIPLE_CHOICES = 300
+HTTP_301_MOVED_PERMANENTLY = 301
+HTTP_302_FOUND = 302
+HTTP_303_SEE_OTHER = 303
+HTTP_304_NOT_MODIFIED = 304
+HTTP_305_USE_PROXY = 305
+HTTP_306_RESERVED = 306
+HTTP_307_TEMPORARY_REDIRECT = 307
+HTTP_308_PERMANENT_REDIRECT = 308
+HTTP_400_BAD_REQUEST = 400
+HTTP_401_UNAUTHORIZED = 401
+HTTP_402_PAYMENT_REQUIRED = 402
+HTTP_403_FORBIDDEN = 403
+HTTP_404_NOT_FOUND = 404
+HTTP_405_METHOD_NOT_ALLOWED = 405
+HTTP_406_NOT_ACCEPTABLE = 406
+HTTP_407_PROXY_AUTHENTICATION_REQUIRED = 407
+HTTP_408_REQUEST_TIMEOUT = 408
+HTTP_409_CONFLICT = 409
+HTTP_410_GONE = 410
+HTTP_411_LENGTH_REQUIRED = 411
+HTTP_412_PRECONDITION_FAILED = 412
+HTTP_413_REQUEST_ENTITY_TOO_LARGE = 413
+HTTP_414_REQUEST_URI_TOO_LONG = 414
+HTTP_415_UNSUPPORTED_MEDIA_TYPE = 415
+HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE = 416
+HTTP_417_EXPECTATION_FAILED = 417
+HTTP_418_IM_A_TEAPOT = 418
+HTTP_421_MISDIRECTED_REQUEST = 421
+HTTP_422_UNPROCESSABLE_ENTITY = 422
+HTTP_423_LOCKED = 423
+HTTP_424_FAILED_DEPENDENCY = 424
+HTTP_425_TOO_EARLY = 425
+HTTP_426_UPGRADE_REQUIRED = 426
+HTTP_428_PRECONDITION_REQUIRED = 428
+HTTP_429_TOO_MANY_REQUESTS = 429
+HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE = 431
+HTTP_451_UNAVAILABLE_FOR_LEGAL_REASONS = 451
+HTTP_500_INTERNAL_SERVER_ERROR = 500
+HTTP_501_NOT_IMPLEMENTED = 501
+HTTP_502_BAD_GATEWAY = 502
+HTTP_503_SERVICE_UNAVAILABLE = 503
+HTTP_504_GATEWAY_TIMEOUT = 504
+HTTP_505_HTTP_VERSION_NOT_SUPPORTED = 505
+HTTP_506_VARIANT_ALSO_NEGOTIATES = 506
+HTTP_507_INSUFFICIENT_STORAGE = 507
+HTTP_508_LOOP_DETECTED = 508
+HTTP_510_NOT_EXTENDED = 510
+HTTP_511_NETWORK_AUTHENTICATION_REQUIRED = 511
+
+
+"""
+WebSocket codes
+https://www.iana.org/assignments/websocket/websocket.xml#close-code-number
+https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent
+"""
+WS_1000_NORMAL_CLOSURE = 1000
+WS_1001_GOING_AWAY = 1001
+WS_1002_PROTOCOL_ERROR = 1002
+WS_1003_UNSUPPORTED_DATA = 1003
+WS_1005_NO_STATUS_RCVD = 1005
+WS_1006_ABNORMAL_CLOSURE = 1006
+WS_1007_INVALID_FRAME_PAYLOAD_DATA = 1007
+WS_1008_POLICY_VIOLATION = 1008
+WS_1009_MESSAGE_TOO_BIG = 1009
+WS_1010_MANDATORY_EXT = 1010
+WS_1011_INTERNAL_ERROR = 1011
+WS_1012_SERVICE_RESTART = 1012
+WS_1013_TRY_AGAIN_LATER = 1013
+WS_1014_BAD_GATEWAY = 1014
+WS_1015_TLS_HANDSHAKE = 1015
diff --git a/.venv/lib/python3.12/site-packages/starlette/templating.py b/.venv/lib/python3.12/site-packages/starlette/templating.py
new file mode 100644
index 00000000..6b01aac9
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/starlette/templating.py
@@ -0,0 +1,216 @@
+from __future__ import annotations
+
+import typing
+import warnings
+from os import PathLike
+
+from starlette.background import BackgroundTask
+from starlette.datastructures import URL
+from starlette.requests import Request
+from starlette.responses import HTMLResponse
+from starlette.types import Receive, Scope, Send
+
+try:
+ import jinja2
+
+ # @contextfunction was renamed to @pass_context in Jinja 3.0, and was removed in 3.1
+ # hence we try to get pass_context (most installs will be >=3.1)
+ # and fall back to contextfunction,
+ # adding a type ignore for mypy to let us access an attribute that may not exist
+ if hasattr(jinja2, "pass_context"):
+ pass_context = jinja2.pass_context
+ else: # pragma: no cover
+ pass_context = jinja2.contextfunction # type: ignore[attr-defined]
+except ModuleNotFoundError: # pragma: no cover
+ jinja2 = None # type: ignore[assignment]
+
+
+class _TemplateResponse(HTMLResponse):
+ def __init__(
+ self,
+ template: typing.Any,
+ context: dict[str, typing.Any],
+ status_code: int = 200,
+ headers: typing.Mapping[str, str] | None = None,
+ media_type: str | None = None,
+ background: BackgroundTask | None = None,
+ ):
+ self.template = template
+ self.context = context
+ content = template.render(context)
+ super().__init__(content, status_code, headers, media_type, background)
+
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
+ request = self.context.get("request", {})
+ extensions = request.get("extensions", {})
+ if "http.response.debug" in extensions: # pragma: no branch
+ await send(
+ {
+ "type": "http.response.debug",
+ "info": {
+ "template": self.template,
+ "context": self.context,
+ },
+ }
+ )
+ await super().__call__(scope, receive, send)
+
+
+class Jinja2Templates:
+ """
+ templates = Jinja2Templates("templates")
+
+ return templates.TemplateResponse("index.html", {"request": request})
+ """
+
+ @typing.overload
+ def __init__(
+ self,
+ directory: str | PathLike[str] | typing.Sequence[str | PathLike[str]],
+ *,
+ context_processors: list[typing.Callable[[Request], dict[str, typing.Any]]] | None = None,
+ **env_options: typing.Any,
+ ) -> None: ...
+
+ @typing.overload
+ def __init__(
+ self,
+ *,
+ env: jinja2.Environment,
+ context_processors: list[typing.Callable[[Request], dict[str, typing.Any]]] | None = None,
+ ) -> None: ...
+
+ def __init__(
+ self,
+ directory: str | PathLike[str] | typing.Sequence[str | PathLike[str]] | None = None,
+ *,
+ context_processors: list[typing.Callable[[Request], dict[str, typing.Any]]] | None = None,
+ env: jinja2.Environment | None = None,
+ **env_options: typing.Any,
+ ) -> None:
+ if env_options:
+ warnings.warn(
+ "Extra environment options are deprecated. Use a preconfigured jinja2.Environment instead.",
+ DeprecationWarning,
+ )
+ assert jinja2 is not None, "jinja2 must be installed to use Jinja2Templates"
+ assert bool(directory) ^ bool(env), "either 'directory' or 'env' arguments must be passed"
+ self.context_processors = context_processors or []
+ if directory is not None:
+ self.env = self._create_env(directory, **env_options)
+ elif env is not None: # pragma: no branch
+ self.env = env
+
+ self._setup_env_defaults(self.env)
+
+ def _create_env(
+ self,
+ directory: str | PathLike[str] | typing.Sequence[str | PathLike[str]],
+ **env_options: typing.Any,
+ ) -> jinja2.Environment:
+ loader = jinja2.FileSystemLoader(directory)
+ env_options.setdefault("loader", loader)
+ env_options.setdefault("autoescape", True)
+
+ return jinja2.Environment(**env_options)
+
+ def _setup_env_defaults(self, env: jinja2.Environment) -> None:
+ @pass_context
+ def url_for(
+ context: dict[str, typing.Any],
+ name: str,
+ /,
+ **path_params: typing.Any,
+ ) -> URL:
+ request: Request = context["request"]
+ return request.url_for(name, **path_params)
+
+ env.globals.setdefault("url_for", url_for)
+
+ def get_template(self, name: str) -> jinja2.Template:
+ return self.env.get_template(name)
+
+ @typing.overload
+ def TemplateResponse(
+ self,
+ request: Request,
+ name: str,
+ context: dict[str, typing.Any] | None = None,
+ status_code: int = 200,
+ headers: typing.Mapping[str, str] | None = None,
+ media_type: str | None = None,
+ background: BackgroundTask | None = None,
+ ) -> _TemplateResponse: ...
+
+ @typing.overload
+ def TemplateResponse(
+ self,
+ name: str,
+ context: dict[str, typing.Any] | None = None,
+ status_code: int = 200,
+ headers: typing.Mapping[str, str] | None = None,
+ media_type: str | None = None,
+ background: BackgroundTask | None = None,
+ ) -> _TemplateResponse:
+ # Deprecated usage
+ ...
+
+ def TemplateResponse(self, *args: typing.Any, **kwargs: typing.Any) -> _TemplateResponse:
+ if args:
+ if isinstance(args[0], str): # the first argument is template name (old style)
+ warnings.warn(
+ "The `name` is not the first parameter anymore. "
+ "The first parameter should be the `Request` instance.\n"
+ 'Replace `TemplateResponse(name, {"request": request})` by `TemplateResponse(request, name)`.',
+ DeprecationWarning,
+ )
+
+ name = args[0]
+ context = args[1] if len(args) > 1 else kwargs.get("context", {})
+ status_code = args[2] if len(args) > 2 else kwargs.get("status_code", 200)
+ headers = args[2] if len(args) > 2 else kwargs.get("headers")
+ media_type = args[3] if len(args) > 3 else kwargs.get("media_type")
+ background = args[4] if len(args) > 4 else kwargs.get("background")
+
+ if "request" not in context:
+ raise ValueError('context must include a "request" key')
+ request = context["request"]
+ else: # the first argument is a request instance (new style)
+ request = args[0]
+ name = args[1] if len(args) > 1 else kwargs["name"]
+ context = args[2] if len(args) > 2 else kwargs.get("context", {})
+ status_code = args[3] if len(args) > 3 else kwargs.get("status_code", 200)
+ headers = args[4] if len(args) > 4 else kwargs.get("headers")
+ media_type = args[5] if len(args) > 5 else kwargs.get("media_type")
+ background = args[6] if len(args) > 6 else kwargs.get("background")
+ else: # all arguments are kwargs
+ if "request" not in kwargs:
+ warnings.warn(
+ "The `TemplateResponse` now requires the `request` argument.\n"
+ 'Replace `TemplateResponse(name, {"context": context})` by `TemplateResponse(request, name)`.',
+ DeprecationWarning,
+ )
+ if "request" not in kwargs.get("context", {}):
+ raise ValueError('context must include a "request" key')
+
+ context = kwargs.get("context", {})
+ request = kwargs.get("request", context.get("request"))
+ name = typing.cast(str, kwargs["name"])
+ status_code = kwargs.get("status_code", 200)
+ headers = kwargs.get("headers")
+ media_type = kwargs.get("media_type")
+ background = kwargs.get("background")
+
+ context.setdefault("request", request)
+ for context_processor in self.context_processors:
+ context.update(context_processor(request))
+
+ template = self.get_template(name)
+ return _TemplateResponse(
+ template,
+ context,
+ status_code=status_code,
+ headers=headers,
+ media_type=media_type,
+ background=background,
+ )
diff --git a/.venv/lib/python3.12/site-packages/starlette/testclient.py b/.venv/lib/python3.12/site-packages/starlette/testclient.py
new file mode 100644
index 00000000..d54025e5
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/starlette/testclient.py
@@ -0,0 +1,731 @@
+from __future__ import annotations
+
+import contextlib
+import inspect
+import io
+import json
+import math
+import sys
+import typing
+import warnings
+from concurrent.futures import Future
+from types import GeneratorType
+from urllib.parse import unquote, urljoin
+
+import anyio
+import anyio.abc
+import anyio.from_thread
+from anyio.streams.stapled import StapledObjectStream
+
+from starlette._utils import is_async_callable
+from starlette.types import ASGIApp, Message, Receive, Scope, Send
+from starlette.websockets import WebSocketDisconnect
+
+if sys.version_info >= (3, 10): # pragma: no cover
+ from typing import TypeGuard
+else: # pragma: no cover
+ from typing_extensions import TypeGuard
+
+try:
+ import httpx
+except ModuleNotFoundError: # pragma: no cover
+ raise RuntimeError(
+ "The starlette.testclient module requires the httpx package to be installed.\n"
+ "You can install this with:\n"
+ " $ pip install httpx\n"
+ )
+_PortalFactoryType = typing.Callable[[], typing.ContextManager[anyio.abc.BlockingPortal]]
+
+ASGIInstance = typing.Callable[[Receive, Send], typing.Awaitable[None]]
+ASGI2App = typing.Callable[[Scope], ASGIInstance]
+ASGI3App = typing.Callable[[Scope, Receive, Send], typing.Awaitable[None]]
+
+
+_RequestData = typing.Mapping[str, typing.Union[str, typing.Iterable[str], bytes]]
+
+
+def _is_asgi3(app: ASGI2App | ASGI3App) -> TypeGuard[ASGI3App]:
+ if inspect.isclass(app):
+ return hasattr(app, "__await__")
+ return is_async_callable(app)
+
+
+class _WrapASGI2:
+ """
+ Provide an ASGI3 interface onto an ASGI2 app.
+ """
+
+ def __init__(self, app: ASGI2App) -> None:
+ self.app = app
+
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
+ instance = self.app(scope)
+ await instance(receive, send)
+
+
+class _AsyncBackend(typing.TypedDict):
+ backend: str
+ backend_options: dict[str, typing.Any]
+
+
+class _Upgrade(Exception):
+ def __init__(self, session: WebSocketTestSession) -> None:
+ self.session = session
+
+
+class WebSocketDenialResponse( # type: ignore[misc]
+ httpx.Response,
+ WebSocketDisconnect,
+):
+ """
+ A special case of `WebSocketDisconnect`, raised in the `TestClient` if the
+ `WebSocket` is closed before being accepted with a `send_denial_response()`.
+ """
+
+
+class WebSocketTestSession:
+ def __init__(
+ self,
+ app: ASGI3App,
+ scope: Scope,
+ portal_factory: _PortalFactoryType,
+ ) -> None:
+ self.app = app
+ self.scope = scope
+ self.accepted_subprotocol = None
+ self.portal_factory = portal_factory
+ self.extra_headers = None
+
+ def __enter__(self) -> WebSocketTestSession:
+ with contextlib.ExitStack() as stack:
+ self.portal = portal = stack.enter_context(self.portal_factory())
+ fut, cs = portal.start_task(self._run)
+ stack.callback(fut.result)
+ stack.callback(portal.call, cs.cancel)
+ self.send({"type": "websocket.connect"})
+ message = self.receive()
+ self._raise_on_close(message)
+ self.accepted_subprotocol = message.get("subprotocol", None)
+ self.extra_headers = message.get("headers", None)
+ stack.callback(self.close, 1000)
+ self.exit_stack = stack.pop_all()
+ return self
+
+ def __exit__(self, *args: typing.Any) -> bool | None:
+ return self.exit_stack.__exit__(*args)
+
+ async def _run(self, *, task_status: anyio.abc.TaskStatus[anyio.CancelScope]) -> None:
+ """
+ The sub-thread in which the websocket session runs.
+ """
+ send: anyio.create_memory_object_stream[Message] = anyio.create_memory_object_stream(math.inf)
+ send_tx, send_rx = send
+ receive: anyio.create_memory_object_stream[Message] = anyio.create_memory_object_stream(math.inf)
+ receive_tx, receive_rx = receive
+ with send_tx, send_rx, receive_tx, receive_rx, anyio.CancelScope() as cs:
+ self._receive_tx = receive_tx
+ self._send_rx = send_rx
+ task_status.started(cs)
+ await self.app(self.scope, receive_rx.receive, send_tx.send)
+
+ # wait for cs.cancel to be called before closing streams
+ await anyio.sleep_forever()
+
+ def _raise_on_close(self, message: Message) -> None:
+ if message["type"] == "websocket.close":
+ raise WebSocketDisconnect(code=message.get("code", 1000), reason=message.get("reason", ""))
+ elif message["type"] == "websocket.http.response.start":
+ status_code: int = message["status"]
+ headers: list[tuple[bytes, bytes]] = message["headers"]
+ body: list[bytes] = []
+ while True:
+ message = self.receive()
+ assert message["type"] == "websocket.http.response.body"
+ body.append(message["body"])
+ if not message.get("more_body", False):
+ break
+ raise WebSocketDenialResponse(status_code=status_code, headers=headers, content=b"".join(body))
+
+ def send(self, message: Message) -> None:
+ self.portal.call(self._receive_tx.send, message)
+
+ def send_text(self, data: str) -> None:
+ self.send({"type": "websocket.receive", "text": data})
+
+ def send_bytes(self, data: bytes) -> None:
+ self.send({"type": "websocket.receive", "bytes": data})
+
+ def send_json(self, data: typing.Any, mode: typing.Literal["text", "binary"] = "text") -> None:
+ text = json.dumps(data, separators=(",", ":"), ensure_ascii=False)
+ if mode == "text":
+ self.send({"type": "websocket.receive", "text": text})
+ else:
+ self.send({"type": "websocket.receive", "bytes": text.encode("utf-8")})
+
+ def close(self, code: int = 1000, reason: str | None = None) -> None:
+ self.send({"type": "websocket.disconnect", "code": code, "reason": reason})
+
+ def receive(self) -> Message:
+ return self.portal.call(self._send_rx.receive)
+
+ def receive_text(self) -> str:
+ message = self.receive()
+ self._raise_on_close(message)
+ return typing.cast(str, message["text"])
+
+ def receive_bytes(self) -> bytes:
+ message = self.receive()
+ self._raise_on_close(message)
+ return typing.cast(bytes, message["bytes"])
+
+ def receive_json(self, mode: typing.Literal["text", "binary"] = "text") -> typing.Any:
+ message = self.receive()
+ self._raise_on_close(message)
+ if mode == "text":
+ text = message["text"]
+ else:
+ text = message["bytes"].decode("utf-8")
+ return json.loads(text)
+
+
+class _TestClientTransport(httpx.BaseTransport):
+ def __init__(
+ self,
+ app: ASGI3App,
+ portal_factory: _PortalFactoryType,
+ raise_server_exceptions: bool = True,
+ root_path: str = "",
+ *,
+ client: tuple[str, int],
+ app_state: dict[str, typing.Any],
+ ) -> None:
+ self.app = app
+ self.raise_server_exceptions = raise_server_exceptions
+ self.root_path = root_path
+ self.portal_factory = portal_factory
+ self.app_state = app_state
+ self.client = client
+
+ def handle_request(self, request: httpx.Request) -> httpx.Response:
+ scheme = request.url.scheme
+ netloc = request.url.netloc.decode(encoding="ascii")
+ path = request.url.path
+ raw_path = request.url.raw_path
+ query = request.url.query.decode(encoding="ascii")
+
+ default_port = {"http": 80, "ws": 80, "https": 443, "wss": 443}[scheme]
+
+ if ":" in netloc:
+ host, port_string = netloc.split(":", 1)
+ port = int(port_string)
+ else:
+ host = netloc
+ port = default_port
+
+ # Include the 'host' header.
+ if "host" in request.headers:
+ headers: list[tuple[bytes, bytes]] = []
+ elif port == default_port: # pragma: no cover
+ headers = [(b"host", host.encode())]
+ else: # pragma: no cover
+ headers = [(b"host", (f"{host}:{port}").encode())]
+
+ # Include other request headers.
+ headers += [(key.lower().encode(), value.encode()) for key, value in request.headers.multi_items()]
+
+ scope: dict[str, typing.Any]
+
+ if scheme in {"ws", "wss"}:
+ subprotocol = request.headers.get("sec-websocket-protocol", None)
+ if subprotocol is None:
+ subprotocols: typing.Sequence[str] = []
+ else:
+ subprotocols = [value.strip() for value in subprotocol.split(",")]
+ scope = {
+ "type": "websocket",
+ "path": unquote(path),
+ "raw_path": raw_path.split(b"?", 1)[0],
+ "root_path": self.root_path,
+ "scheme": scheme,
+ "query_string": query.encode(),
+ "headers": headers,
+ "client": self.client,
+ "server": [host, port],
+ "subprotocols": subprotocols,
+ "state": self.app_state.copy(),
+ "extensions": {"websocket.http.response": {}},
+ }
+ session = WebSocketTestSession(self.app, scope, self.portal_factory)
+ raise _Upgrade(session)
+
+ scope = {
+ "type": "http",
+ "http_version": "1.1",
+ "method": request.method,
+ "path": unquote(path),
+ "raw_path": raw_path.split(b"?", 1)[0],
+ "root_path": self.root_path,
+ "scheme": scheme,
+ "query_string": query.encode(),
+ "headers": headers,
+ "client": self.client,
+ "server": [host, port],
+ "extensions": {"http.response.debug": {}},
+ "state": self.app_state.copy(),
+ }
+
+ request_complete = False
+ response_started = False
+ response_complete: anyio.Event
+ raw_kwargs: dict[str, typing.Any] = {"stream": io.BytesIO()}
+ template = None
+ context = None
+
+ async def receive() -> Message:
+ nonlocal request_complete
+
+ if request_complete:
+ if not response_complete.is_set():
+ await response_complete.wait()
+ return {"type": "http.disconnect"}
+
+ body = request.read()
+ if isinstance(body, str):
+ body_bytes: bytes = body.encode("utf-8") # pragma: no cover
+ elif body is None:
+ body_bytes = b"" # pragma: no cover
+ elif isinstance(body, GeneratorType):
+ try: # pragma: no cover
+ chunk = body.send(None)
+ if isinstance(chunk, str):
+ chunk = chunk.encode("utf-8")
+ return {"type": "http.request", "body": chunk, "more_body": True}
+ except StopIteration: # pragma: no cover
+ request_complete = True
+ return {"type": "http.request", "body": b""}
+ else:
+ body_bytes = body
+
+ request_complete = True
+ return {"type": "http.request", "body": body_bytes}
+
+ async def send(message: Message) -> None:
+ nonlocal raw_kwargs, response_started, template, context
+
+ if message["type"] == "http.response.start":
+ assert not response_started, 'Received multiple "http.response.start" messages.'
+ raw_kwargs["status_code"] = message["status"]
+ raw_kwargs["headers"] = [(key.decode(), value.decode()) for key, value in message.get("headers", [])]
+ response_started = True
+ elif message["type"] == "http.response.body":
+ assert response_started, 'Received "http.response.body" without "http.response.start".'
+ assert not response_complete.is_set(), 'Received "http.response.body" after response completed.'
+ body = message.get("body", b"")
+ more_body = message.get("more_body", False)
+ if request.method != "HEAD":
+ raw_kwargs["stream"].write(body)
+ if not more_body:
+ raw_kwargs["stream"].seek(0)
+ response_complete.set()
+ elif message["type"] == "http.response.debug":
+ template = message["info"]["template"]
+ context = message["info"]["context"]
+
+ try:
+ with self.portal_factory() as portal:
+ response_complete = portal.call(anyio.Event)
+ portal.call(self.app, scope, receive, send)
+ except BaseException as exc:
+ if self.raise_server_exceptions:
+ raise exc
+
+ if self.raise_server_exceptions:
+ assert response_started, "TestClient did not receive any response."
+ elif not response_started:
+ raw_kwargs = {
+ "status_code": 500,
+ "headers": [],
+ "stream": io.BytesIO(),
+ }
+
+ raw_kwargs["stream"] = httpx.ByteStream(raw_kwargs["stream"].read())
+
+ response = httpx.Response(**raw_kwargs, request=request)
+ if template is not None:
+ response.template = template # type: ignore[attr-defined]
+ response.context = context # type: ignore[attr-defined]
+ return response
+
+
+class TestClient(httpx.Client):
+ __test__ = False
+ task: Future[None]
+ portal: anyio.abc.BlockingPortal | None = None
+
+ def __init__(
+ self,
+ app: ASGIApp,
+ base_url: str = "http://testserver",
+ raise_server_exceptions: bool = True,
+ root_path: str = "",
+ backend: typing.Literal["asyncio", "trio"] = "asyncio",
+ backend_options: dict[str, typing.Any] | None = None,
+ cookies: httpx._types.CookieTypes | None = None,
+ headers: dict[str, str] | None = None,
+ follow_redirects: bool = True,
+ client: tuple[str, int] = ("testclient", 50000),
+ ) -> None:
+ self.async_backend = _AsyncBackend(backend=backend, backend_options=backend_options or {})
+ if _is_asgi3(app):
+ asgi_app = app
+ else:
+ app = typing.cast(ASGI2App, app) # type: ignore[assignment]
+ asgi_app = _WrapASGI2(app) # type: ignore[arg-type]
+ self.app = asgi_app
+ self.app_state: dict[str, typing.Any] = {}
+ transport = _TestClientTransport(
+ self.app,
+ portal_factory=self._portal_factory,
+ raise_server_exceptions=raise_server_exceptions,
+ root_path=root_path,
+ app_state=self.app_state,
+ client=client,
+ )
+ if headers is None:
+ headers = {}
+ headers.setdefault("user-agent", "testclient")
+ super().__init__(
+ base_url=base_url,
+ headers=headers,
+ transport=transport,
+ follow_redirects=follow_redirects,
+ cookies=cookies,
+ )
+
+ @contextlib.contextmanager
+ def _portal_factory(self) -> typing.Generator[anyio.abc.BlockingPortal, None, None]:
+ if self.portal is not None:
+ yield self.portal
+ else:
+ with anyio.from_thread.start_blocking_portal(**self.async_backend) as portal:
+ yield portal
+
+ def request( # type: ignore[override]
+ self,
+ method: str,
+ url: httpx._types.URLTypes,
+ *,
+ content: httpx._types.RequestContent | None = None,
+ data: _RequestData | None = None,
+ files: httpx._types.RequestFiles | None = None,
+ json: typing.Any = None,
+ params: httpx._types.QueryParamTypes | None = None,
+ headers: httpx._types.HeaderTypes | None = None,
+ cookies: httpx._types.CookieTypes | None = None,
+ auth: httpx._types.AuthTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT,
+ follow_redirects: bool | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT,
+ timeout: httpx._types.TimeoutTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT,
+ extensions: dict[str, typing.Any] | None = None,
+ ) -> httpx.Response:
+ if timeout is not httpx.USE_CLIENT_DEFAULT:
+ warnings.warn(
+ "You should not use the 'timeout' argument with the TestClient. "
+ "See https://github.com/encode/starlette/issues/1108 for more information.",
+ DeprecationWarning,
+ )
+ url = self._merge_url(url)
+ return super().request(
+ method,
+ url,
+ content=content,
+ data=data,
+ files=files,
+ json=json,
+ params=params,
+ headers=headers,
+ cookies=cookies,
+ auth=auth,
+ follow_redirects=follow_redirects,
+ timeout=timeout,
+ extensions=extensions,
+ )
+
+ def get( # type: ignore[override]
+ self,
+ url: httpx._types.URLTypes,
+ *,
+ params: httpx._types.QueryParamTypes | None = None,
+ headers: httpx._types.HeaderTypes | None = None,
+ cookies: httpx._types.CookieTypes | None = None,
+ auth: httpx._types.AuthTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT,
+ follow_redirects: bool | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT,
+ timeout: httpx._types.TimeoutTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT,
+ extensions: dict[str, typing.Any] | None = None,
+ ) -> httpx.Response:
+ return super().get(
+ url,
+ params=params,
+ headers=headers,
+ cookies=cookies,
+ auth=auth,
+ follow_redirects=follow_redirects,
+ timeout=timeout,
+ extensions=extensions,
+ )
+
+ def options( # type: ignore[override]
+ self,
+ url: httpx._types.URLTypes,
+ *,
+ params: httpx._types.QueryParamTypes | None = None,
+ headers: httpx._types.HeaderTypes | None = None,
+ cookies: httpx._types.CookieTypes | None = None,
+ auth: httpx._types.AuthTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT,
+ follow_redirects: bool | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT,
+ timeout: httpx._types.TimeoutTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT,
+ extensions: dict[str, typing.Any] | None = None,
+ ) -> httpx.Response:
+ return super().options(
+ url,
+ params=params,
+ headers=headers,
+ cookies=cookies,
+ auth=auth,
+ follow_redirects=follow_redirects,
+ timeout=timeout,
+ extensions=extensions,
+ )
+
+ def head( # type: ignore[override]
+ self,
+ url: httpx._types.URLTypes,
+ *,
+ params: httpx._types.QueryParamTypes | None = None,
+ headers: httpx._types.HeaderTypes | None = None,
+ cookies: httpx._types.CookieTypes | None = None,
+ auth: httpx._types.AuthTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT,
+ follow_redirects: bool | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT,
+ timeout: httpx._types.TimeoutTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT,
+ extensions: dict[str, typing.Any] | None = None,
+ ) -> httpx.Response:
+ return super().head(
+ url,
+ params=params,
+ headers=headers,
+ cookies=cookies,
+ auth=auth,
+ follow_redirects=follow_redirects,
+ timeout=timeout,
+ extensions=extensions,
+ )
+
+ def post( # type: ignore[override]
+ self,
+ url: httpx._types.URLTypes,
+ *,
+ content: httpx._types.RequestContent | None = None,
+ data: _RequestData | None = None,
+ files: httpx._types.RequestFiles | None = None,
+ json: typing.Any = None,
+ params: httpx._types.QueryParamTypes | None = None,
+ headers: httpx._types.HeaderTypes | None = None,
+ cookies: httpx._types.CookieTypes | None = None,
+ auth: httpx._types.AuthTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT,
+ follow_redirects: bool | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT,
+ timeout: httpx._types.TimeoutTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT,
+ extensions: dict[str, typing.Any] | None = None,
+ ) -> httpx.Response:
+ return super().post(
+ url,
+ content=content,
+ data=data,
+ files=files,
+ json=json,
+ params=params,
+ headers=headers,
+ cookies=cookies,
+ auth=auth,
+ follow_redirects=follow_redirects,
+ timeout=timeout,
+ extensions=extensions,
+ )
+
+ def put( # type: ignore[override]
+ self,
+ url: httpx._types.URLTypes,
+ *,
+ content: httpx._types.RequestContent | None = None,
+ data: _RequestData | None = None,
+ files: httpx._types.RequestFiles | None = None,
+ json: typing.Any = None,
+ params: httpx._types.QueryParamTypes | None = None,
+ headers: httpx._types.HeaderTypes | None = None,
+ cookies: httpx._types.CookieTypes | None = None,
+ auth: httpx._types.AuthTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT,
+ follow_redirects: bool | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT,
+ timeout: httpx._types.TimeoutTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT,
+ extensions: dict[str, typing.Any] | None = None,
+ ) -> httpx.Response:
+ return super().put(
+ url,
+ content=content,
+ data=data,
+ files=files,
+ json=json,
+ params=params,
+ headers=headers,
+ cookies=cookies,
+ auth=auth,
+ follow_redirects=follow_redirects,
+ timeout=timeout,
+ extensions=extensions,
+ )
+
+ def patch( # type: ignore[override]
+ self,
+ url: httpx._types.URLTypes,
+ *,
+ content: httpx._types.RequestContent | None = None,
+ data: _RequestData | None = None,
+ files: httpx._types.RequestFiles | None = None,
+ json: typing.Any = None,
+ params: httpx._types.QueryParamTypes | None = None,
+ headers: httpx._types.HeaderTypes | None = None,
+ cookies: httpx._types.CookieTypes | None = None,
+ auth: httpx._types.AuthTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT,
+ follow_redirects: bool | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT,
+ timeout: httpx._types.TimeoutTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT,
+ extensions: dict[str, typing.Any] | None = None,
+ ) -> httpx.Response:
+ return super().patch(
+ url,
+ content=content,
+ data=data,
+ files=files,
+ json=json,
+ params=params,
+ headers=headers,
+ cookies=cookies,
+ auth=auth,
+ follow_redirects=follow_redirects,
+ timeout=timeout,
+ extensions=extensions,
+ )
+
+ def delete( # type: ignore[override]
+ self,
+ url: httpx._types.URLTypes,
+ *,
+ params: httpx._types.QueryParamTypes | None = None,
+ headers: httpx._types.HeaderTypes | None = None,
+ cookies: httpx._types.CookieTypes | None = None,
+ auth: httpx._types.AuthTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT,
+ follow_redirects: bool | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT,
+ timeout: httpx._types.TimeoutTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT,
+ extensions: dict[str, typing.Any] | None = None,
+ ) -> httpx.Response:
+ return super().delete(
+ url,
+ params=params,
+ headers=headers,
+ cookies=cookies,
+ auth=auth,
+ follow_redirects=follow_redirects,
+ timeout=timeout,
+ extensions=extensions,
+ )
+
+ def websocket_connect(
+ self,
+ url: str,
+ subprotocols: typing.Sequence[str] | None = None,
+ **kwargs: typing.Any,
+ ) -> WebSocketTestSession:
+ url = urljoin("ws://testserver", url)
+ headers = kwargs.get("headers", {})
+ headers.setdefault("connection", "upgrade")
+ headers.setdefault("sec-websocket-key", "testserver==")
+ headers.setdefault("sec-websocket-version", "13")
+ if subprotocols is not None:
+ headers.setdefault("sec-websocket-protocol", ", ".join(subprotocols))
+ kwargs["headers"] = headers
+ try:
+ super().request("GET", url, **kwargs)
+ except _Upgrade as exc:
+ session = exc.session
+ else:
+ raise RuntimeError("Expected WebSocket upgrade") # pragma: no cover
+
+ return session
+
+ def __enter__(self) -> TestClient:
+ with contextlib.ExitStack() as stack:
+ self.portal = portal = stack.enter_context(anyio.from_thread.start_blocking_portal(**self.async_backend))
+
+ @stack.callback
+ def reset_portal() -> None:
+ self.portal = None
+
+ send: anyio.create_memory_object_stream[typing.MutableMapping[str, typing.Any] | None] = (
+ anyio.create_memory_object_stream(math.inf)
+ )
+ receive: anyio.create_memory_object_stream[typing.MutableMapping[str, typing.Any]] = (
+ anyio.create_memory_object_stream(math.inf)
+ )
+ for channel in (*send, *receive):
+ stack.callback(channel.close)
+ self.stream_send = StapledObjectStream(*send)
+ self.stream_receive = StapledObjectStream(*receive)
+ self.task = portal.start_task_soon(self.lifespan)
+ portal.call(self.wait_startup)
+
+ @stack.callback
+ def wait_shutdown() -> None:
+ portal.call(self.wait_shutdown)
+
+ self.exit_stack = stack.pop_all()
+
+ return self
+
+ def __exit__(self, *args: typing.Any) -> None:
+ self.exit_stack.close()
+
+ async def lifespan(self) -> None:
+ scope = {"type": "lifespan", "state": self.app_state}
+ try:
+ await self.app(scope, self.stream_receive.receive, self.stream_send.send)
+ finally:
+ await self.stream_send.send(None)
+
+ async def wait_startup(self) -> None:
+ await self.stream_receive.send({"type": "lifespan.startup"})
+
+ async def receive() -> typing.Any:
+ message = await self.stream_send.receive()
+ if message is None:
+ self.task.result()
+ return message
+
+ message = await receive()
+ assert message["type"] in (
+ "lifespan.startup.complete",
+ "lifespan.startup.failed",
+ )
+ if message["type"] == "lifespan.startup.failed":
+ await receive()
+
+ async def wait_shutdown(self) -> None:
+ async def receive() -> typing.Any:
+ message = await self.stream_send.receive()
+ if message is None:
+ self.task.result()
+ return message
+
+ await self.stream_receive.send({"type": "lifespan.shutdown"})
+ message = await receive()
+ assert message["type"] in (
+ "lifespan.shutdown.complete",
+ "lifespan.shutdown.failed",
+ )
+ if message["type"] == "lifespan.shutdown.failed":
+ await receive()
diff --git a/.venv/lib/python3.12/site-packages/starlette/types.py b/.venv/lib/python3.12/site-packages/starlette/types.py
new file mode 100644
index 00000000..893f8729
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/starlette/types.py
@@ -0,0 +1,24 @@
+import typing
+
+if typing.TYPE_CHECKING:
+ from starlette.requests import Request
+ from starlette.responses import Response
+ from starlette.websockets import WebSocket
+
+AppType = typing.TypeVar("AppType")
+
+Scope = typing.MutableMapping[str, typing.Any]
+Message = typing.MutableMapping[str, typing.Any]
+
+Receive = typing.Callable[[], typing.Awaitable[Message]]
+Send = typing.Callable[[Message], typing.Awaitable[None]]
+
+ASGIApp = typing.Callable[[Scope, Receive, Send], typing.Awaitable[None]]
+
+StatelessLifespan = typing.Callable[[AppType], typing.AsyncContextManager[None]]
+StatefulLifespan = typing.Callable[[AppType], typing.AsyncContextManager[typing.Mapping[str, typing.Any]]]
+Lifespan = typing.Union[StatelessLifespan[AppType], StatefulLifespan[AppType]]
+
+HTTPExceptionHandler = typing.Callable[["Request", Exception], "Response | typing.Awaitable[Response]"]
+WebSocketExceptionHandler = typing.Callable[["WebSocket", Exception], typing.Awaitable[None]]
+ExceptionHandler = typing.Union[HTTPExceptionHandler, WebSocketExceptionHandler]
diff --git a/.venv/lib/python3.12/site-packages/starlette/websockets.py b/.venv/lib/python3.12/site-packages/starlette/websockets.py
new file mode 100644
index 00000000..6b46f4ea
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/starlette/websockets.py
@@ -0,0 +1,195 @@
+from __future__ import annotations
+
+import enum
+import json
+import typing
+
+from starlette.requests import HTTPConnection
+from starlette.responses import Response
+from starlette.types import Message, Receive, Scope, Send
+
+
+class WebSocketState(enum.Enum):
+ CONNECTING = 0
+ CONNECTED = 1
+ DISCONNECTED = 2
+ RESPONSE = 3
+
+
+class WebSocketDisconnect(Exception):
+ def __init__(self, code: int = 1000, reason: str | None = None) -> None:
+ self.code = code
+ self.reason = reason or ""
+
+
+class WebSocket(HTTPConnection):
+ def __init__(self, scope: Scope, receive: Receive, send: Send) -> None:
+ super().__init__(scope)
+ assert scope["type"] == "websocket"
+ self._receive = receive
+ self._send = send
+ self.client_state = WebSocketState.CONNECTING
+ self.application_state = WebSocketState.CONNECTING
+
+ async def receive(self) -> Message:
+ """
+ Receive ASGI websocket messages, ensuring valid state transitions.
+ """
+ if self.client_state == WebSocketState.CONNECTING:
+ message = await self._receive()
+ message_type = message["type"]
+ if message_type != "websocket.connect":
+ raise RuntimeError(f'Expected ASGI message "websocket.connect", but got {message_type!r}')
+ self.client_state = WebSocketState.CONNECTED
+ return message
+ elif self.client_state == WebSocketState.CONNECTED:
+ message = await self._receive()
+ message_type = message["type"]
+ if message_type not in {"websocket.receive", "websocket.disconnect"}:
+ raise RuntimeError(
+ f'Expected ASGI message "websocket.receive" or "websocket.disconnect", but got {message_type!r}'
+ )
+ if message_type == "websocket.disconnect":
+ self.client_state = WebSocketState.DISCONNECTED
+ return message
+ else:
+ raise RuntimeError('Cannot call "receive" once a disconnect message has been received.')
+
+ async def send(self, message: Message) -> None:
+ """
+ Send ASGI websocket messages, ensuring valid state transitions.
+ """
+ if self.application_state == WebSocketState.CONNECTING:
+ message_type = message["type"]
+ if message_type not in {"websocket.accept", "websocket.close", "websocket.http.response.start"}:
+ raise RuntimeError(
+ 'Expected ASGI message "websocket.accept", "websocket.close" or "websocket.http.response.start", '
+ f"but got {message_type!r}"
+ )
+ if message_type == "websocket.close":
+ self.application_state = WebSocketState.DISCONNECTED
+ elif message_type == "websocket.http.response.start":
+ self.application_state = WebSocketState.RESPONSE
+ else:
+ self.application_state = WebSocketState.CONNECTED
+ await self._send(message)
+ elif self.application_state == WebSocketState.CONNECTED:
+ message_type = message["type"]
+ if message_type not in {"websocket.send", "websocket.close"}:
+ raise RuntimeError(
+ f'Expected ASGI message "websocket.send" or "websocket.close", but got {message_type!r}'
+ )
+ if message_type == "websocket.close":
+ self.application_state = WebSocketState.DISCONNECTED
+ try:
+ await self._send(message)
+ except OSError:
+ self.application_state = WebSocketState.DISCONNECTED
+ raise WebSocketDisconnect(code=1006)
+ elif self.application_state == WebSocketState.RESPONSE:
+ message_type = message["type"]
+ if message_type != "websocket.http.response.body":
+ raise RuntimeError(f'Expected ASGI message "websocket.http.response.body", but got {message_type!r}')
+ if not message.get("more_body", False):
+ self.application_state = WebSocketState.DISCONNECTED
+ await self._send(message)
+ else:
+ raise RuntimeError('Cannot call "send" once a close message has been sent.')
+
+ async def accept(
+ self,
+ subprotocol: str | None = None,
+ headers: typing.Iterable[tuple[bytes, bytes]] | None = None,
+ ) -> None:
+ headers = headers or []
+
+ if self.client_state == WebSocketState.CONNECTING: # pragma: no branch
+ # If we haven't yet seen the 'connect' message, then wait for it first.
+ await self.receive()
+ await self.send({"type": "websocket.accept", "subprotocol": subprotocol, "headers": headers})
+
+ def _raise_on_disconnect(self, message: Message) -> None:
+ if message["type"] == "websocket.disconnect":
+ raise WebSocketDisconnect(message["code"], message.get("reason"))
+
+ async def receive_text(self) -> str:
+ if self.application_state != WebSocketState.CONNECTED:
+ raise RuntimeError('WebSocket is not connected. Need to call "accept" first.')
+ message = await self.receive()
+ self._raise_on_disconnect(message)
+ return typing.cast(str, message["text"])
+
+ async def receive_bytes(self) -> bytes:
+ if self.application_state != WebSocketState.CONNECTED:
+ raise RuntimeError('WebSocket is not connected. Need to call "accept" first.')
+ message = await self.receive()
+ self._raise_on_disconnect(message)
+ return typing.cast(bytes, message["bytes"])
+
+ async def receive_json(self, mode: str = "text") -> typing.Any:
+ if mode not in {"text", "binary"}:
+ raise RuntimeError('The "mode" argument should be "text" or "binary".')
+ if self.application_state != WebSocketState.CONNECTED:
+ raise RuntimeError('WebSocket is not connected. Need to call "accept" first.')
+ message = await self.receive()
+ self._raise_on_disconnect(message)
+
+ if mode == "text":
+ text = message["text"]
+ else:
+ text = message["bytes"].decode("utf-8")
+ return json.loads(text)
+
+ async def iter_text(self) -> typing.AsyncIterator[str]:
+ try:
+ while True:
+ yield await self.receive_text()
+ except WebSocketDisconnect:
+ pass
+
+ async def iter_bytes(self) -> typing.AsyncIterator[bytes]:
+ try:
+ while True:
+ yield await self.receive_bytes()
+ except WebSocketDisconnect:
+ pass
+
+ async def iter_json(self) -> typing.AsyncIterator[typing.Any]:
+ try:
+ while True:
+ yield await self.receive_json()
+ except WebSocketDisconnect:
+ pass
+
+ async def send_text(self, data: str) -> None:
+ await self.send({"type": "websocket.send", "text": data})
+
+ async def send_bytes(self, data: bytes) -> None:
+ await self.send({"type": "websocket.send", "bytes": data})
+
+ async def send_json(self, data: typing.Any, mode: str = "text") -> None:
+ if mode not in {"text", "binary"}:
+ raise RuntimeError('The "mode" argument should be "text" or "binary".')
+ text = json.dumps(data, separators=(",", ":"), ensure_ascii=False)
+ if mode == "text":
+ await self.send({"type": "websocket.send", "text": text})
+ else:
+ await self.send({"type": "websocket.send", "bytes": text.encode("utf-8")})
+
+ async def close(self, code: int = 1000, reason: str | None = None) -> None:
+ await self.send({"type": "websocket.close", "code": code, "reason": reason or ""})
+
+ async def send_denial_response(self, response: Response) -> None:
+ if "websocket.http.response" in self.scope.get("extensions", {}):
+ await response(self.scope, self.receive, self.send)
+ else:
+ raise RuntimeError("The server doesn't support the Websocket Denial Response extension.")
+
+
+class WebSocketClose:
+ def __init__(self, code: int = 1000, reason: str | None = None) -> None:
+ self.code = code
+ self.reason = reason or ""
+
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
+ await send({"type": "websocket.close", "code": self.code, "reason": self.reason})