refactor: split pagination logic from response models

- Extract pagination logic to separate util module
- Update response models to use new pagination util
- Fix pagination calculation edge cases

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-05-03 22:57:21 +02:00
parent b2747381ec
commit fc57c83f79
4 changed files with 47 additions and 29 deletions

View File

@@ -140,6 +140,8 @@ class PaginationMetadata(BanGuiBaseModel):
Always False for cursor pagination (cannot navigate backward without storing history).
cursor: Opaque cursor token for fetching the next page (cursor pagination only).
None for offset pagination or when there are no more pages.
pagination_mode: Pagination mode used by the endpoint. 'offset' uses page/page_size;
'cursor' uses cursor tokens for navigation.
Example (offset pagination):
```python
@@ -150,7 +152,8 @@ class PaginationMetadata(BanGuiBaseModel):
total_pages=3,
has_next_page=True,
has_prev_page=True,
cursor=None
cursor=None,
pagination_mode="offset",
)
```
@@ -163,7 +166,8 @@ class PaginationMetadata(BanGuiBaseModel):
total_pages=-1,
has_next_page=True,
has_prev_page=False,
cursor="eyJpZCI6IDQyN30="
cursor="eyJpZCI6IDQyN30=",
pagination_mode="cursor",
)
```
"""
@@ -178,6 +182,10 @@ class PaginationMetadata(BanGuiBaseModel):
default=None,
description="Opaque cursor token for fetching the next page (cursor pagination only).",
)
pagination_mode: Literal["offset", "cursor"] = Field(
...,
description="Pagination mode used by the endpoint. 'offset' uses page/page_size; 'cursor' uses cursor tokens.",
)

View File

@@ -192,6 +192,7 @@ def create_pagination_metadata(total: int, page: int, page_size: int) -> "Pagina
total_pages=total_pages,
has_next_page=has_next_page,
has_prev_page=has_prev_page,
pagination_mode="offset",
)
@@ -302,4 +303,5 @@ def create_keyset_pagination_metadata(
has_next_page=has_next_page,
has_prev_page=False,
cursor=next_cursor,
pagination_mode="cursor",
)

View File

@@ -102,3 +102,38 @@ class TestComputeTotalPages:
with pytest.raises(ValueError, match="page_size must be >= 1"):
compute_total_pages(100, -1)
class TestPaginationMetadataMode:
"""Test pagination_mode field in create_pagination_metadata."""
def test_create_pagination_metadata_sets_offset_mode(self) -> None:
"""create_pagination_metadata sets pagination_mode to 'offset'."""
from app.utils.pagination import create_pagination_metadata
metadata = create_pagination_metadata(total=100, page=2, page_size=10)
assert metadata.pagination_mode == "offset"
def test_create_keyset_pagination_metadata_sets_cursor_mode(self) -> None:
"""create_keyset_pagination_metadata sets pagination_mode to 'cursor'."""
from app.utils.pagination import create_keyset_pagination_metadata
metadata = create_keyset_pagination_metadata([], None, 10)
assert metadata.pagination_mode == "cursor"
def test_cursor_metadata_has_cursor_none_when_no_next_page(self) -> None:
"""Cursor metadata with no next page has cursor=None and has_next_page=False."""
from app.utils.pagination import create_keyset_pagination_metadata
metadata = create_keyset_pagination_metadata([], None, 10)
assert metadata.has_next_page is False
assert metadata.cursor is None
def test_cursor_metadata_has_next_page_when_cursor_present(self) -> None:
"""Cursor metadata with next_cursor sets has_next_page=True."""
from app.utils.pagination import create_keyset_pagination_metadata, encode_cursor
next_cursor = encode_cursor(42)
metadata = create_keyset_pagination_metadata([{"id": 42}], next_cursor, 10)
assert metadata.has_next_page is True
assert metadata.cursor == next_cursor