mirror of
https://github.com/d3vyce/fastapi-toolsets.git
synced 2026-04-15 22:26:25 +02:00
206 lines
6.0 KiB
Python
206 lines
6.0 KiB
Python
"""Base Pydantic schemas for API responses."""
|
|
|
|
import math
|
|
from enum import Enum
|
|
from typing import Annotated, Any, ClassVar, Generic, Literal, TypeVar, Union
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field, computed_field
|
|
|
|
from .types import DataT
|
|
|
|
__all__ = [
|
|
"ApiError",
|
|
"CursorPagination",
|
|
"CursorPaginatedResponse",
|
|
"ErrorResponse",
|
|
"OffsetPagination",
|
|
"OffsetPaginatedResponse",
|
|
"PaginatedResponse",
|
|
"PaginationType",
|
|
"PydanticBase",
|
|
"Response",
|
|
"ResponseStatus",
|
|
]
|
|
|
|
|
|
class PydanticBase(BaseModel):
|
|
"""Base class for all Pydantic models with common configuration."""
|
|
|
|
model_config: ClassVar[ConfigDict] = ConfigDict(
|
|
from_attributes=True,
|
|
validate_assignment=True,
|
|
)
|
|
|
|
|
|
class ResponseStatus(str, Enum):
|
|
"""Standard API response status."""
|
|
|
|
SUCCESS = "SUCCESS"
|
|
FAIL = "FAIL"
|
|
|
|
|
|
class ApiError(PydanticBase):
|
|
"""Structured API error definition.
|
|
|
|
Used to define standard error responses with consistent format.
|
|
|
|
Attributes:
|
|
code: HTTP status code
|
|
msg: Short error message
|
|
desc: Detailed error description
|
|
err_code: Application-specific error code (e.g., "AUTH-401")
|
|
"""
|
|
|
|
code: int
|
|
msg: str
|
|
desc: str
|
|
err_code: str
|
|
data: Any | None = None
|
|
|
|
|
|
class BaseResponse(PydanticBase):
|
|
"""Base response structure for all API responses.
|
|
|
|
Attributes:
|
|
status: SUCCESS or FAIL
|
|
message: Human-readable message
|
|
error_code: Error code if status is FAIL, None otherwise
|
|
"""
|
|
|
|
status: ResponseStatus = ResponseStatus.SUCCESS
|
|
message: str = "Success"
|
|
error_code: str | None = None
|
|
|
|
|
|
class Response(BaseResponse, Generic[DataT]):
|
|
"""Generic API response with data payload.
|
|
|
|
Example:
|
|
```python
|
|
Response[UserRead](data=user, message="User retrieved")
|
|
```
|
|
"""
|
|
|
|
data: DataT | None = None
|
|
|
|
|
|
class ErrorResponse(BaseResponse):
|
|
"""Error response with additional description field.
|
|
|
|
Used for error responses that need more context.
|
|
"""
|
|
|
|
status: ResponseStatus = ResponseStatus.FAIL
|
|
description: str | None = None
|
|
data: Any | None = None
|
|
|
|
|
|
class OffsetPagination(PydanticBase):
|
|
"""Pagination metadata for offset-based list responses.
|
|
|
|
Attributes:
|
|
total_count: Total number of items across all pages.
|
|
``None`` when ``include_total=False``.
|
|
items_per_page: Number of items per page
|
|
page: Current page number (1-indexed)
|
|
has_more: Whether there are more pages
|
|
pages: Total number of pages
|
|
"""
|
|
|
|
total_count: int | None
|
|
items_per_page: int
|
|
page: int
|
|
has_more: bool
|
|
|
|
@computed_field
|
|
@property
|
|
def pages(self) -> int | None:
|
|
"""Total number of pages, or ``None`` when ``total_count`` is unknown."""
|
|
if self.total_count is None:
|
|
return None
|
|
if self.items_per_page == 0:
|
|
return 0
|
|
return math.ceil(self.total_count / self.items_per_page)
|
|
|
|
|
|
class CursorPagination(PydanticBase):
|
|
"""Pagination metadata for cursor-based list responses.
|
|
|
|
Attributes:
|
|
next_cursor: Encoded cursor for the next page, or None on the last page.
|
|
prev_cursor: Encoded cursor for the previous page, or None on the first page.
|
|
items_per_page: Number of items requested per page.
|
|
has_more: Whether there is at least one more page after this one.
|
|
"""
|
|
|
|
next_cursor: str | None
|
|
prev_cursor: str | None = None
|
|
items_per_page: int
|
|
has_more: bool
|
|
|
|
|
|
class PaginationType(str, Enum):
|
|
"""Pagination strategy selector for :meth:`.AsyncCrud.paginate`."""
|
|
|
|
OFFSET = "offset"
|
|
CURSOR = "cursor"
|
|
|
|
|
|
class PaginatedResponse(BaseResponse, Generic[DataT]):
|
|
"""Paginated API response for list endpoints.
|
|
|
|
Base class and return type for endpoints that support both pagination
|
|
strategies. Use :class:`OffsetPaginatedResponse` or
|
|
:class:`CursorPaginatedResponse` when the strategy is fixed.
|
|
|
|
When used as ``PaginatedResponse[T]`` in a return annotation, subscripting
|
|
returns ``Annotated[Union[CursorPaginatedResponse[T], OffsetPaginatedResponse[T]], Field(discriminator="pagination_type")]``
|
|
so FastAPI emits a proper ``oneOf`` + discriminator in the OpenAPI schema.
|
|
"""
|
|
|
|
data: list[DataT]
|
|
pagination: OffsetPagination | CursorPagination
|
|
pagination_type: PaginationType | None = None
|
|
filter_attributes: dict[str, list[Any]] | None = None
|
|
search_columns: list[str] | None = None
|
|
|
|
_discriminated_union_cache: ClassVar[dict[Any, Any]] = {}
|
|
|
|
def __class_getitem__( # ty:ignore[invalid-method-override]
|
|
cls, item: type[Any] | tuple[type[Any], ...]
|
|
) -> type[Any]:
|
|
if cls is PaginatedResponse and not isinstance(item, TypeVar):
|
|
cached = cls._discriminated_union_cache.get(item)
|
|
if cached is None:
|
|
cached = Annotated[
|
|
Union[CursorPaginatedResponse[item], OffsetPaginatedResponse[item]], # ty:ignore[invalid-type-form]
|
|
Field(discriminator="pagination_type"),
|
|
]
|
|
cls._discriminated_union_cache[item] = cached
|
|
return cached # ty:ignore[invalid-return-type]
|
|
return super().__class_getitem__(item)
|
|
|
|
|
|
class OffsetPaginatedResponse(PaginatedResponse[DataT]):
|
|
"""Paginated response with typed offset-based pagination metadata.
|
|
|
|
The ``pagination_type`` field is always ``"offset"`` and acts as a
|
|
discriminator, allowing frontend clients to narrow the union type returned
|
|
by a unified ``paginate()`` endpoint.
|
|
"""
|
|
|
|
pagination: OffsetPagination
|
|
pagination_type: Literal[PaginationType.OFFSET] = PaginationType.OFFSET
|
|
|
|
|
|
class CursorPaginatedResponse(PaginatedResponse[DataT]):
|
|
"""Paginated response with typed cursor-based pagination metadata.
|
|
|
|
The ``pagination_type`` field is always ``"cursor"`` and acts as a
|
|
discriminator, allowing frontend clients to narrow the union type returned
|
|
by a unified ``paginate()`` endpoint.
|
|
"""
|
|
|
|
pagination: CursorPagination
|
|
pagination_type: Literal[PaginationType.CURSOR] = PaginationType.CURSOR
|