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.pycBinary files differ new 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.pycBinary files differ new 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.pycBinary files differ new 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") | 
