feat: add pages computed field to OffsetPagination schema (#159)

This commit is contained in:
d3vyce
2026-03-21 15:24:11 +01:00
committed by GitHub
parent f8c9bf69fe
commit f0223ebde4
4 changed files with 77 additions and 2 deletions

View File

@@ -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 }
}
```

View File

@@ -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

View File

@@ -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.

View File

@@ -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."""