Source code for sqlspec.extensions.fastapi.providers

"""Application dependency providers for FastAPI filter injection.

This module provides filter dependency injection for FastAPI routes, allowing
automatic parsing of query parameters into SQLSpec filter objects.
"""

import copy
import datetime
import inspect
import typing
from collections.abc import Callable, Mapping
from enum import Enum
from functools import partial
from inspect import isclass
from types import GenericAlias
from typing import Annotated, Any, Literal, NamedTuple, TypeVar, cast
from uuid import UUID

from fastapi import Depends, Query
from fastapi.exceptions import RequestValidationError
from typing_extensions import NotRequired, TypedDict

from sqlspec.core import (
    BeforeAfterFilter,
    BooleanFilter,
    ChoicesFilter,
    FilterTypes,
    InCollectionFilter,
    LimitOffsetFilter,
    NotInCollectionFilter,
    NotNullFilter,
    NullFilter,
    OrderByFilter,
    SearchFilter,
)
from sqlspec.utils.text import camelize

__all__ = (
    "DEPENDENCY_DEFAULTS",
    "BooleanOrNone",
    "ChoiceField",
    "DTorNone",
    "DependencyDefaults",
    "FieldNameType",
    "FilterConfig",
    "HashableType",
    "HashableValue",
    "IntOrNone",
    "SortField",
    "SortOrder",
    "SortOrderOrNone",
    "StringOrNone",
    "UuidOrNone",
    "dep_cache",
    "normalize_choice_field_types",
    "provide_filters",
)

DTorNone = datetime.datetime | None
StringOrNone = str | None
UuidOrNone = UUID | None
IntOrNone = int | None
BooleanOrNone = bool | None
SortOrder = Literal["asc", "desc"]
SortOrderOrNone = SortOrder | None
SortField = str | set[str] | list[str]
HashableValue = str | int | float | bool | None
HashableType = HashableValue | tuple[Any, ...] | tuple[tuple[str, Any], ...] | tuple[HashableValue, ...]
_ProviderT = TypeVar("_ProviderT")
_FILTER_CONFIG_KEYS = frozenset({
    "id_filter",
    "created_at",
    "updated_at",
    "pagination_type",
    "search",
    "sort_field",
    "not_in_fields",
    "in_fields",
    "null_fields",
    "not_null_fields",
    "boolean_fields",
    "choice_fields",
})


class DependencyDefaults:
    """Default values for dependency generation."""

    CREATED_FILTER_DEPENDENCY_KEY: str = "created_filter"
    ID_FILTER_DEPENDENCY_KEY: str = "id_filter"
    LIMIT_OFFSET_FILTER_DEPENDENCY_KEY: str = "limit_offset_filter"
    UPDATED_FILTER_DEPENDENCY_KEY: str = "updated_filter"
    ORDER_BY_FILTER_DEPENDENCY_KEY: str = "order_by_filter"
    SEARCH_FILTER_DEPENDENCY_KEY: str = "search_filter"
    DEFAULT_PAGINATION_SIZE: int = 20
DEPENDENCY_DEFAULTS = DependencyDefaults() class FieldNameType(NamedTuple): """Type for field name and associated type information for filter configuration.""" name: str """Name of the field to filter on.""" type_hint: type[Any] = str """Type of the filter value. Defaults to str.""" class ChoiceField: """Type for choice field name and allowed choices for filter configuration.""" __slots__ = ("choices", "name") def __init__(self, name: str, choices: list[Any] | tuple[Any, ...] | type[Enum]) -> None: self.name = name self.choices = choices def normalize_choice_field_types(choices: list[Any] | tuple[Any, ...] | type[Enum]) -> Any: """Normalize choices into a generic type hint (Literal or Enum).""" if isclass(choices) and issubclass(choices, Enum): return choices return cast("Any", typing.Literal).__getitem__(tuple(choices)) class _SortFieldResolution(NamedTuple): default_field: str default_query_value: str allowed_fields: frozenset[str] inbound_aliases: dict[str, str] field_display_names: dict[str, str] allowed_display_names: tuple[str, ...] def normalize(self, value: str | None) -> str | None: if value is None: return self.default_field return self.inbound_aliases.get(value) # Keep FilterConfig field unions and provider signatures in sync with sqlspec.extensions.litestar.providers. class FilterConfig(TypedDict): """Configuration for generated FastAPI filter dependencies. All keys are optional. A filter dependency is created only for each enabled key. Field names are SQL-facing allowlist values; generated query parameter names and order-by aliases remain API-facing. """ id_filter: NotRequired[type[UUID | int | str]] """Type of ID filter to enable. When set, creates an ``ids`` collection filter.""" id_field: NotRequired[str] """SQL-facing field name for ID filtering. Defaults to ``"id"``.""" sort_field: NotRequired[SortField] """Allowed SQL-facing field or fields for ``orderBy`` sorting.""" sort_field_aliases: NotRequired[dict[str, str]] """Additional API-facing ``orderBy`` aliases mapped to configured ``sort_field`` values.""" sort_field_camelize: NotRequired[bool] """Whether to accept camel-case aliases for configured sort fields. Defaults to ``True``.""" sort_order: NotRequired[SortOrder] """Default sort order. Defaults to ``"desc"``.""" pagination_type: NotRequired[Literal["limit_offset"]] """Pagination strategy to enable. Currently supports ``"limit_offset"``.""" pagination_size: NotRequired[int] """Default page size for limit/offset pagination.""" search: NotRequired[str | set[str] | list[str]] """SQL-facing field or fields to search. Strings may be comma-separated.""" search_ignore_case: NotRequired[bool] """Whether search filtering is case-insensitive. Defaults to ``False``.""" created_at: NotRequired[bool] """Whether to enable ``created_at`` before/after range filtering.""" updated_at: NotRequired[bool] """Whether to enable ``updated_at`` before/after range filtering.""" not_in_fields: NotRequired[FieldNameType | set[FieldNameType] | list[str | FieldNameType]] """Field or fields that support ``NOT IN`` collection filtering.""" in_fields: NotRequired[FieldNameType | set[FieldNameType] | list[str | FieldNameType]] """Field or fields that support ``IN`` collection filtering.""" null_fields: NotRequired[str | set[str] | list[str]] """Field or fields that support ``IS NULL`` filtering.""" not_null_fields: NotRequired[str | set[str] | list[str]] """Field or fields that support ``IS NOT NULL`` filtering.""" boolean_fields: NotRequired[str | set[str] | list[str]] """Field or fields that support boolean filtering.""" choice_fields: NotRequired[ChoiceField | set[ChoiceField] | list[str | ChoiceField]] """Field or fields that support choices filtering."""