From f0223ebde4163a444c3611b7fbb5d90971656600 Mon Sep 17 00:00:00 2001 From: d3vyce <44915747+d3vyce@users.noreply.github.com> Date: Sat, 21 Mar 2026 15:24:11 +0100 Subject: [PATCH] feat: add pages computed field to OffsetPagination schema (#159) --- docs/examples/pagination-search.md | 3 +- docs/module/crud.md | 1 + src/fastapi_toolsets/schemas.py | 14 ++++++- tests/test_schemas.py | 61 ++++++++++++++++++++++++++++++ 4 files changed, 77 insertions(+), 2 deletions(-) diff --git a/docs/examples/pagination-search.md b/docs/examples/pagination-search.md index ac25f24..28ee84b 100644 --- a/docs/examples/pagination-search.md +++ b/docs/examples/pagination-search.md @@ -72,6 +72,7 @@ GET /articles/offset?page=2&items_per_page=10&search=fastapi&status=published&or ], "pagination": { "total_count": 42, + "pages": 5, "page": 2, "items_per_page": 10, "has_more": true @@ -146,7 +147,7 @@ GET /articles/?pagination_type=offset&page=1&items_per_page=10 "status": "SUCCESS", "pagination_type": "offset", "data": ["..."], - "pagination": { "total_count": 42, "page": 1, "items_per_page": 10, "has_more": true } + "pagination": { "total_count": 42, "pages": 5, "page": 1, "items_per_page": 10, "has_more": true } } ``` diff --git a/docs/module/crud.md b/docs/module/crud.md index 6fbfd26..c62ddcd 100644 --- a/docs/module/crud.md +++ b/docs/module/crud.md @@ -182,6 +182,7 @@ The [`offset_paginate`](../reference/crud.md#fastapi_toolsets.crud.factory.Async "data": ["..."], "pagination": { "total_count": 100, + "pages": 5, "page": 1, "items_per_page": 20, "has_more": true diff --git a/src/fastapi_toolsets/schemas.py b/src/fastapi_toolsets/schemas.py index 2a40528..b9c640a 100644 --- a/src/fastapi_toolsets/schemas.py +++ b/src/fastapi_toolsets/schemas.py @@ -1,9 +1,10 @@ """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 +from pydantic import BaseModel, ConfigDict, Field, computed_field from .types import DataT @@ -103,6 +104,7 @@ class OffsetPagination(PydanticBase): 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 @@ -110,6 +112,16 @@ class OffsetPagination(PydanticBase): 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. diff --git a/tests/test_schemas.py b/tests/test_schemas.py index 4b57da6..faf1c81 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -222,6 +222,67 @@ class TestOffsetPagination: data = pagination.model_dump() assert data["total_count"] is None + def test_pages_computed(self): + """pages is ceil(total_count / items_per_page).""" + pagination = OffsetPagination( + total_count=42, + items_per_page=10, + page=1, + has_more=True, + ) + assert pagination.pages == 5 + + def test_pages_exact_division(self): + """pages is exact when total_count is evenly divisible.""" + pagination = OffsetPagination( + total_count=40, + items_per_page=10, + page=1, + has_more=False, + ) + assert pagination.pages == 4 + + def test_pages_zero_total(self): + """pages is 0 when total_count is 0.""" + pagination = OffsetPagination( + total_count=0, + items_per_page=10, + page=1, + has_more=False, + ) + assert pagination.pages == 0 + + def test_pages_zero_items_per_page(self): + """pages is 0 when items_per_page is 0.""" + pagination = OffsetPagination( + total_count=100, + items_per_page=0, + page=1, + has_more=False, + ) + assert pagination.pages == 0 + + def test_pages_none_when_total_count_none(self): + """pages is None when total_count is None (include_total=False).""" + pagination = OffsetPagination( + total_count=None, + items_per_page=20, + page=1, + has_more=True, + ) + assert pagination.pages is None + + def test_pages_in_serialization(self): + """pages appears in model_dump output.""" + pagination = OffsetPagination( + total_count=25, + items_per_page=10, + page=1, + has_more=True, + ) + data = pagination.model_dump() + assert data["pages"] == 3 + class TestCursorPagination: """Tests for CursorPagination schema."""