Source code for litestar.routes.base

from __future__ import annotations

import re
from abc import ABC, abstractmethod
from datetime import date, datetime, time, timedelta
from decimal import Decimal
from pathlib import Path
from typing import TYPE_CHECKING, Any, Callable, Generic, TypeVar
from uuid import UUID

import msgspec

from litestar.exceptions import ImproperlyConfiguredException
from litestar.types.internal_types import PathParameterDefinition
from litestar.utils import join_paths, normalize_path

ScopeT = TypeVar("ScopeT", bound="BaseScope")

if TYPE_CHECKING:
    from litestar.types import BaseScope, Receive, Send


def _parse_datetime(value: str) -> datetime:
    return msgspec.convert(value, datetime)


def _parse_date(value: str) -> date:
    return msgspec.convert(value, date)


def _parse_time(value: str) -> time:
    return msgspec.convert(value, time)


def _parse_timedelta(value: str) -> timedelta:
    try:
        return msgspec.convert(value, timedelta)
    except msgspec.ValidationError:
        return timedelta(seconds=int(float(value)))


param_match_regex = re.compile(r"{(.*?)}")

param_type_map = {
    "str": str,
    "int": int,
    "float": float,
    "uuid": UUID,
    "decimal": Decimal,
    "date": date,
    "datetime": datetime,
    "time": time,
    "timedelta": timedelta,
    "path": Path,
}


parsers_map: dict[Any, Callable[[Any], Any]] = {
    float: float,
    int: int,
    Decimal: Decimal,
    UUID: UUID,
    date: _parse_date,
    datetime: _parse_datetime,
    time: _parse_time,
    timedelta: _parse_timedelta,
}


[docs] class BaseRoute(ABC, Generic[ScopeT]): """Base Route class used by Litestar. It's an abstract class meant to be extended. """ __slots__ = ( "app", "handler_names", "methods", "path", "path_components", "path_format", "path_parameters", "scope_type", )
[docs] def __init__( self, *, path: str, ) -> None: """Initialize the route. Args: path: Base path of the route """ self.path, self.path_format, self.path_components, self.path_parameters = self._parse_path(path)
[docs] @abstractmethod async def handle(self, scope: ScopeT, receive: Receive, send: Send) -> None: """ASGI App of the route. Args: scope: The ASGI connection scope. receive: The ASGI receive function. send: The ASGI send function. Returns: None """ raise NotImplementedError("Route subclasses must implement handle which serves as the ASGI app entry point")
@staticmethod def _validate_path_parameter(param: str, path: str) -> None: """Validate that a path parameter adheres to the required format and datatypes. Raises: ImproperlyConfiguredException: If the parameter has an invalid format. """ if len(param.split(":")) != 2: raise ImproperlyConfiguredException( f"Path parameters should be declared with a type using the following pattern: '{{parameter_name:type}}', e.g. '/my-path/{{my_param:int}}' in path: '{path}'" ) param_name, param_type = (p.strip() for p in param.split(":")) if not param_name: raise ImproperlyConfiguredException("Path parameter names should be of length greater than zero") if param_type not in param_type_map: raise ImproperlyConfiguredException( f"Path parameters should be declared with an allowed type, i.e. one of {', '.join(param_type_map.keys())} in path: '{path}'" ) @classmethod def _parse_path( cls, path: str ) -> tuple[str, str, list[str | PathParameterDefinition], dict[str, PathParameterDefinition]]: """Normalize and parse a path. Splits the path into a list of components, parsing any that are path parameters. Also builds the OpenAPI compatible path, which does not include the type of the path parameters. Returns: A 3-tuple of the normalized path, the OpenAPI formatted path, and the list of parsed components. """ path = normalize_path(path) parsed_components: list[str | PathParameterDefinition] = [] path_format_components = [] path_parameters: dict[str, PathParameterDefinition] = {} components = [component for component in path.split("/") if component] for component in components: if param_match := param_match_regex.fullmatch(component): param = param_match.group(1) cls._validate_path_parameter(param, path) param_name, param_type = (p.strip() for p in param.split(":")) type_class = param_type_map[param_type] parser = parsers_map[type_class] if type_class not in {str, Path} else None if param_name in path_parameters: raise ImproperlyConfiguredException(f"Duplicate parameter '{param_name}' detected in '{path}'.") param_definition = PathParameterDefinition(name=param_name, type=type_class, full=param, parser=parser) parsed_components.append(param_definition) path_parameters[param_name] = param_definition path_format_components.append("{" + param_name + "}") else: parsed_components.append(component) path_format_components.append(component) path_format = join_paths(path_format_components) return path, path_format, parsed_components, path_parameters