diff options
Diffstat (limited to 'venv/lib/python3.11/site-packages/litestar/static_files')
-rw-r--r-- | venv/lib/python3.11/site-packages/litestar/static_files/__init__.py | 4 | ||||
-rw-r--r-- | venv/lib/python3.11/site-packages/litestar/static_files/__pycache__/__init__.cpython-311.pyc | bin | 0 -> 443 bytes | |||
-rw-r--r-- | venv/lib/python3.11/site-packages/litestar/static_files/__pycache__/base.cpython-311.pyc | bin | 0 -> 7052 bytes | |||
-rw-r--r-- | venv/lib/python3.11/site-packages/litestar/static_files/__pycache__/config.cpython-311.pyc | bin | 0 -> 10192 bytes | |||
-rw-r--r-- | venv/lib/python3.11/site-packages/litestar/static_files/base.py | 141 | ||||
-rw-r--r-- | venv/lib/python3.11/site-packages/litestar/static_files/config.py | 224 |
6 files changed, 369 insertions, 0 deletions
diff --git a/venv/lib/python3.11/site-packages/litestar/static_files/__init__.py b/venv/lib/python3.11/site-packages/litestar/static_files/__init__.py new file mode 100644 index 0000000..3cd4594 --- /dev/null +++ b/venv/lib/python3.11/site-packages/litestar/static_files/__init__.py @@ -0,0 +1,4 @@ +from litestar.static_files.base import StaticFiles +from litestar.static_files.config import StaticFilesConfig, create_static_files_router + +__all__ = ("StaticFiles", "StaticFilesConfig", "create_static_files_router") diff --git a/venv/lib/python3.11/site-packages/litestar/static_files/__pycache__/__init__.cpython-311.pyc b/venv/lib/python3.11/site-packages/litestar/static_files/__pycache__/__init__.cpython-311.pyc Binary files differnew file mode 100644 index 0000000..1cc4497 --- /dev/null +++ b/venv/lib/python3.11/site-packages/litestar/static_files/__pycache__/__init__.cpython-311.pyc diff --git a/venv/lib/python3.11/site-packages/litestar/static_files/__pycache__/base.cpython-311.pyc b/venv/lib/python3.11/site-packages/litestar/static_files/__pycache__/base.cpython-311.pyc Binary files differnew file mode 100644 index 0000000..0ca9dae --- /dev/null +++ b/venv/lib/python3.11/site-packages/litestar/static_files/__pycache__/base.cpython-311.pyc diff --git a/venv/lib/python3.11/site-packages/litestar/static_files/__pycache__/config.cpython-311.pyc b/venv/lib/python3.11/site-packages/litestar/static_files/__pycache__/config.cpython-311.pyc Binary files differnew file mode 100644 index 0000000..fd93af6 --- /dev/null +++ b/venv/lib/python3.11/site-packages/litestar/static_files/__pycache__/config.cpython-311.pyc diff --git a/venv/lib/python3.11/site-packages/litestar/static_files/base.py b/venv/lib/python3.11/site-packages/litestar/static_files/base.py new file mode 100644 index 0000000..9827697 --- /dev/null +++ b/venv/lib/python3.11/site-packages/litestar/static_files/base.py @@ -0,0 +1,141 @@ +from __future__ import annotations + +from os.path import commonpath +from pathlib import Path +from typing import TYPE_CHECKING, Literal, Sequence + +from litestar.enums import ScopeType +from litestar.exceptions import MethodNotAllowedException, NotFoundException +from litestar.file_system import FileSystemAdapter +from litestar.response.file import ASGIFileResponse +from litestar.status_codes import HTTP_404_NOT_FOUND + +__all__ = ("StaticFiles",) + + +if TYPE_CHECKING: + from litestar.types import Receive, Scope, Send + from litestar.types.composite_types import PathType + from litestar.types.file_types import FileInfo, FileSystemProtocol + + +class StaticFiles: + """ASGI App that handles file sending.""" + + __slots__ = ("is_html_mode", "directories", "adapter", "send_as_attachment", "headers") + + def __init__( + self, + is_html_mode: bool, + directories: Sequence[PathType], + file_system: FileSystemProtocol, + send_as_attachment: bool = False, + resolve_symlinks: bool = True, + headers: dict[str, str] | None = None, + ) -> None: + """Initialize the Application. + + Args: + is_html_mode: Flag dictating whether serving html. If true, the default file will be ``index.html``. + directories: A list of directories to serve files from. + file_system: The file_system spec to use for serving files. + send_as_attachment: Whether to send the file with a ``content-disposition`` header of + ``attachment`` or ``inline`` + resolve_symlinks: Resolve symlinks to the directories + headers: Headers that will be sent with every response. + """ + self.adapter = FileSystemAdapter(file_system) + self.directories = tuple(Path(p).resolve() if resolve_symlinks else Path(p) for p in directories) + self.is_html_mode = is_html_mode + self.send_as_attachment = send_as_attachment + self.headers = headers + + async def get_fs_info( + self, directories: Sequence[PathType], file_path: PathType + ) -> tuple[Path, FileInfo] | tuple[None, None]: + """Return the resolved path and a :class:`stat_result <os.stat_result>`. + + Args: + directories: A list of directory paths. + file_path: A file path to resolve + + Returns: + A tuple with an optional resolved :class:`Path <anyio.Path>` instance and an optional + :class:`stat_result <os.stat_result>`. + """ + for directory in directories: + try: + joined_path = Path(directory, file_path) + file_info = await self.adapter.info(joined_path) + if file_info and commonpath([str(directory), file_info["name"], joined_path]) == str(directory): + return joined_path, file_info + except FileNotFoundError: + continue + return None, None + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + """ASGI callable. + + Args: + scope: ASGI scope + receive: ASGI ``receive`` callable + send: ASGI ``send`` callable + + Returns: + None + """ + if scope["type"] != ScopeType.HTTP or scope["method"] not in {"GET", "HEAD"}: + raise MethodNotAllowedException() + + res = await self.handle(path=scope["path"], is_head_response=scope["method"] == "HEAD") + await res(scope=scope, receive=receive, send=send) + + async def handle(self, path: str, is_head_response: bool) -> ASGIFileResponse: + split_path = path.split("/") + filename = split_path[-1] + joined_path = Path(*split_path) + resolved_path, fs_info = await self.get_fs_info(directories=self.directories, file_path=joined_path) + content_disposition_type: Literal["inline", "attachment"] = ( + "attachment" if self.send_as_attachment else "inline" + ) + + if self.is_html_mode and fs_info and fs_info["type"] == "directory": + filename = "index.html" + resolved_path, fs_info = await self.get_fs_info( + directories=self.directories, + file_path=Path(resolved_path or joined_path) / filename, + ) + + if fs_info and fs_info["type"] == "file": + return ASGIFileResponse( + file_path=resolved_path or joined_path, + file_info=fs_info, + file_system=self.adapter.file_system, + filename=filename, + content_disposition_type=content_disposition_type, + is_head_response=is_head_response, + headers=self.headers, + ) + + if self.is_html_mode: + # for some reason coverage doesn't catch these two lines + filename = "404.html" # pragma: no cover + resolved_path, fs_info = await self.get_fs_info( # pragma: no cover + directories=self.directories, file_path=filename + ) + + if fs_info and fs_info["type"] == "file": + return ASGIFileResponse( + file_path=resolved_path or joined_path, + file_info=fs_info, + file_system=self.adapter.file_system, + filename=filename, + status_code=HTTP_404_NOT_FOUND, + content_disposition_type=content_disposition_type, + is_head_response=is_head_response, + headers=self.headers, + ) + + raise NotFoundException( + f"no file or directory match the path {resolved_path or joined_path} was found" + ) # pragma: no cover diff --git a/venv/lib/python3.11/site-packages/litestar/static_files/config.py b/venv/lib/python3.11/site-packages/litestar/static_files/config.py new file mode 100644 index 0000000..22b6620 --- /dev/null +++ b/venv/lib/python3.11/site-packages/litestar/static_files/config.py @@ -0,0 +1,224 @@ +from __future__ import annotations + +from dataclasses import dataclass +from pathlib import PurePath # noqa: TCH003 +from typing import TYPE_CHECKING, Any, Sequence + +from litestar.exceptions import ImproperlyConfiguredException +from litestar.file_system import BaseLocalFileSystem +from litestar.handlers import asgi, get, head +from litestar.response.file import ASGIFileResponse # noqa: TCH001 +from litestar.router import Router +from litestar.static_files.base import StaticFiles +from litestar.types import Empty +from litestar.utils import normalize_path, warn_deprecation + +__all__ = ("StaticFilesConfig",) + +if TYPE_CHECKING: + from litestar.datastructures import CacheControlHeader + from litestar.handlers.asgi_handlers import ASGIRouteHandler + from litestar.openapi.spec import SecurityRequirement + from litestar.types import ( + AfterRequestHookHandler, + AfterResponseHookHandler, + BeforeRequestHookHandler, + EmptyType, + ExceptionHandlersMap, + Guard, + Middleware, + PathType, + ) + + +@dataclass +class StaticFilesConfig: + """Configuration for static file service. + + To enable static files, pass an instance of this class to the :class:`Litestar <litestar.app.Litestar>` constructor using + the 'static_files_config' key. + """ + + path: str + """Path to serve static files from. + + Note that the path cannot contain path parameters. + """ + directories: list[PathType] + """A list of directories to serve files from.""" + html_mode: bool = False + """Flag dictating whether serving html. + + If true, the default file will be 'index.html'. + """ + name: str | None = None + """An optional string identifying the static files handler.""" + file_system: Any = BaseLocalFileSystem() # noqa: RUF009 + """The file_system spec to use for serving files. + + Notes: + - A file_system is a class that adheres to the + :class:`FileSystemProtocol <litestar.types.FileSystemProtocol>`. + - You can use any of the file systems exported from the + [fsspec](https://filesystem-spec.readthedocs.io/en/latest/) library for this purpose. + """ + opt: dict[str, Any] | None = None + """A string key dictionary of arbitrary values that will be added to the static files handler.""" + guards: list[Guard] | None = None + """A list of :class:`Guard <litestar.types.Guard>` callables.""" + exception_handlers: ExceptionHandlersMap | None = None + """A dictionary that maps handler functions to status codes and/or exception types.""" + send_as_attachment: bool = False + """Whether to send the file as an attachment.""" + + def __post_init__(self) -> None: + _validate_config(path=self.path, directories=self.directories, file_system=self.file_system) + self.path = normalize_path(self.path) + warn_deprecation( + "2.6.0", + kind="class", + deprecated_name="StaticFilesConfig", + removal_in="3.0", + alternative="create_static_files_router", + info='Replace static_files_config=[StaticFilesConfig(path="/static", directories=["assets"])] with ' + 'route_handlers=[..., create_static_files_router(path="/static", directories=["assets"])]', + ) + + def to_static_files_app(self) -> ASGIRouteHandler: + """Return an ASGI app serving static files based on the config. + + Returns: + :class:`StaticFiles <litestar.static_files.StaticFiles>` + """ + static_files = StaticFiles( + is_html_mode=self.html_mode, + directories=self.directories, + file_system=self.file_system, + send_as_attachment=self.send_as_attachment, + ) + return asgi( + path=self.path, + name=self.name, + is_static=True, + opt=self.opt, + guards=self.guards, + exception_handlers=self.exception_handlers, + )(static_files) + + +def create_static_files_router( + path: str, + directories: list[PathType], + file_system: Any = None, + send_as_attachment: bool = False, + html_mode: bool = False, + name: str = "static", + after_request: AfterRequestHookHandler | None = None, + after_response: AfterResponseHookHandler | None = None, + before_request: BeforeRequestHookHandler | None = None, + cache_control: CacheControlHeader | None = None, + exception_handlers: ExceptionHandlersMap | None = None, + guards: list[Guard] | None = None, + include_in_schema: bool | EmptyType = Empty, + middleware: Sequence[Middleware] | None = None, + opt: dict[str, Any] | None = None, + security: Sequence[SecurityRequirement] | None = None, + tags: Sequence[str] | None = None, + router_class: type[Router] = Router, + resolve_symlinks: bool = True, +) -> Router: + """Create a router with handlers to serve static files. + + Args: + path: Path to serve static files under + directories: Directories to serve static files from + file_system: A *file system* implementing + :class:`~litestar.types.FileSystemProtocol`. + `fsspec <https://filesystem-spec.readthedocs.io/en/latest/>`_ can be passed + here as well + send_as_attachment: Whether to send the file as an attachment + html_mode: When in HTML: + - Serve an ``index.html`` file from ``/`` + - Serve ``404.html`` when a file could not be found + name: Name to pass to the generated handlers + after_request: ``after_request`` handlers passed to the router + after_response: ``after_response`` handlers passed to the router + before_request: ``before_request`` handlers passed to the router + cache_control: ``cache_control`` passed to the router + exception_handlers: Exception handlers passed to the router + guards: Guards passed to the router + include_in_schema: Include the routes / router in the OpenAPI schema + middleware: Middlewares passed to the router + opt: Opts passed to the router + security: Security options passed to the router + tags: ``tags`` passed to the router + router_class: The class used to construct a router from + resolve_symlinks: Resolve symlinks of ``directories`` + """ + + if file_system is None: + file_system = BaseLocalFileSystem() + + _validate_config(path=path, directories=directories, file_system=file_system) + path = normalize_path(path) + + headers = None + if cache_control: + headers = {cache_control.HEADER_NAME: cache_control.to_header()} + + static_files = StaticFiles( + is_html_mode=html_mode, + directories=directories, + file_system=file_system, + send_as_attachment=send_as_attachment, + resolve_symlinks=resolve_symlinks, + headers=headers, + ) + + @get("{file_path:path}", name=name) + async def get_handler(file_path: PurePath) -> ASGIFileResponse: + return await static_files.handle(path=file_path.as_posix(), is_head_response=False) + + @head("/{file_path:path}", name=f"{name}/head") + async def head_handler(file_path: PurePath) -> ASGIFileResponse: + return await static_files.handle(path=file_path.as_posix(), is_head_response=True) + + handlers = [get_handler, head_handler] + + if html_mode: + + @get("/", name=f"{name}/index") + async def index_handler() -> ASGIFileResponse: + return await static_files.handle(path="/", is_head_response=False) + + handlers.append(index_handler) + + return router_class( + after_request=after_request, + after_response=after_response, + before_request=before_request, + cache_control=cache_control, + exception_handlers=exception_handlers, + guards=guards, + include_in_schema=include_in_schema, + middleware=middleware, + opt=opt, + path=path, + route_handlers=handlers, + security=security, + tags=tags, + ) + + +def _validate_config(path: str, directories: list[PathType], file_system: Any) -> None: + if not path: + raise ImproperlyConfiguredException("path must be a non-zero length string,") + + if not directories or not any(bool(d) for d in directories): + raise ImproperlyConfiguredException("directories must include at least one path.") + + if "{" in path: + raise ImproperlyConfiguredException("path parameters are not supported for static files") + + if not (callable(getattr(file_system, "info", None)) and callable(getattr(file_system, "open", None))): + raise ImproperlyConfiguredException("file_system must adhere to the FileSystemProtocol type") |