From 6fa31e293d909dd75b05ca8424b4478927bde191 Mon Sep 17 00:00:00 2001 From: Grant Ramsay Date: Tue, 30 Dec 2025 09:29:28 +0000 Subject: [PATCH 1/4] add pagination to the user list and fix failing test Signed-off-by: Grant Ramsay --- app/managers/user.py | 5 +++++ app/resources/user.py | 13 +++++++------ tests/integration/test_user_routes.py | 4 +++- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/app/managers/user.py b/app/managers/user.py index 055f5063..15ddadbe 100644 --- a/app/managers/user.py +++ b/app/managers/user.py @@ -366,6 +366,11 @@ async def get_all_users(session: AsyncSession) -> Sequence[User]: """Get all Users.""" return await get_all_users_(session) + @staticmethod + def list_users_query() -> Select[tuple[User]]: + """Return the base users query for pagination.""" + return select(User) + @staticmethod async def get_user_by_id(user_id: int, session: AsyncSession) -> User: """Return one user by ID.""" diff --git a/app/resources/user.py b/app/resources/user.py index 1908762c..1c635e3f 100644 --- a/app/resources/user.py +++ b/app/resources/user.py @@ -1,10 +1,9 @@ """Routes for User listing and control.""" -from collections.abc import Sequence from typing import Annotated, cast from fastapi import APIRouter, Depends, Request, status -from fastapi_pagination import Page +from fastapi_pagination import Page, Params from fastapi_pagination.ext.sqlalchemy import apaginate from sqlalchemy.ext.asyncio import AsyncSession @@ -27,13 +26,14 @@ @router.get( "/", dependencies=[Depends(get_current_user), Depends(is_admin)], - response_model=UserResponse | list[UserResponse], + response_model=UserResponse | Page[UserResponse], ) async def get_users( db: Annotated[AsyncSession, Depends(get_database)], + params: Annotated[Params, Depends()], user_id: int | None = None, -) -> Sequence[User] | User: - """Get all users or a specific user by their ID. +) -> Page[UserResponse] | User: + """Get all users (paginated) or a specific user by their ID. user_id is optional, and if omitted then all Users are returned. @@ -41,7 +41,8 @@ async def get_users( """ if user_id: return await UserManager.get_user_by_id(user_id, db) - return await UserManager.get_all_users(db) + query = UserManager.list_users_query() + return cast("Page[UserResponse]", await apaginate(db, query, params)) @router.get( diff --git a/tests/integration/test_user_routes.py b/tests/integration/test_user_routes.py index 0a684fbf..d36926a5 100644 --- a/tests/integration/test_user_routes.py +++ b/tests/integration/test_user_routes.py @@ -100,7 +100,9 @@ async def test_admin_can_get_all_users( ) assert response.status_code == status.HTTP_200_OK - assert len(response.json()) == 4 # noqa: PLR2004 + payload = response.json() + assert payload["total"] == 4 # noqa: PLR2004 + assert len(payload["items"]) == 4 # noqa: PLR2004 async def test_admin_can_get_one_user( self, client: AsyncClient, test_db: AsyncSession From 310dc1293a731d234c696f48b1fbdc775e96dd77 Mon Sep 17 00:00:00 2001 From: Grant Ramsay Date: Tue, 30 Dec 2025 09:43:00 +0000 Subject: [PATCH 2/4] feat: add examples for user response and paginated users in the user listing endpoint Signed-off-by: Grant Ramsay --- app/resources/user.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/app/resources/user.py b/app/resources/user.py index 1c635e3f..d6e1d8c6 100644 --- a/app/resources/user.py +++ b/app/resources/user.py @@ -13,6 +13,7 @@ from app.managers.user import UserManager from app.models.enums import RoleType from app.models.user import User +from app.schemas.examples import ExampleUser from app.schemas.request.user import ( SearchField, UserChangePasswordRequest, @@ -22,11 +23,47 @@ router = APIRouter(tags=["Users"], prefix="/users") +USER_EXAMPLE = { + "id": ExampleUser.id, + "first_name": ExampleUser.first_name, + "last_name": ExampleUser.last_name, + "email": ExampleUser.email, + "role": ExampleUser.role, + "banned": ExampleUser.banned, + "verified": ExampleUser.verified, +} + +PAGINATED_USERS_EXAMPLE = { + "items": [USER_EXAMPLE], + "total": 1, + "page": 1, + "size": 50, + "pages": 1, +} + @router.get( "/", dependencies=[Depends(get_current_user), Depends(is_admin)], response_model=UserResponse | Page[UserResponse], + responses={ + 200: { + "content": { + "application/json": { + "examples": { + "single_user": { + "summary": "Single user", + "value": USER_EXAMPLE, + }, + "paginated_users": { + "summary": "Paginated users", + "value": PAGINATED_USERS_EXAMPLE, + }, + } + } + } + } + }, ) async def get_users( db: Annotated[AsyncSession, Depends(get_database)], From 90ccdd86533ff2f8f9dd1aad174d474b863ee6f0 Mon Sep 17 00:00:00 2001 From: Grant Ramsay Date: Tue, 30 Dec 2025 09:50:18 +0000 Subject: [PATCH 3/4] Order users list for stable pagination --- app/managers/user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/managers/user.py b/app/managers/user.py index 15ddadbe..a2a026fc 100644 --- a/app/managers/user.py +++ b/app/managers/user.py @@ -369,7 +369,7 @@ async def get_all_users(session: AsyncSession) -> Sequence[User]: @staticmethod def list_users_query() -> Select[tuple[User]]: """Return the base users query for pagination.""" - return select(User) + return select(User).order_by(User.id) @staticmethod async def get_user_by_id(user_id: int, session: AsyncSession) -> User: From 9a97f87d06af5710b5beffcc39c8cf9c53f395fa Mon Sep 17 00:00:00 2001 From: Grant Ramsay Date: Tue, 30 Dec 2025 09:59:47 +0000 Subject: [PATCH 4/4] Document users pagination --- README.md | 2 ++ TODO.md | 3 --- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9a92473c..50881c5c 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,8 @@ following advantages to starting your own from scratch : very easily, add users (and make admin), and run a development server. This can easily be modified to add your own functionality (for example bulk add data) since it is based on the excellent [Typer][typer] library. +- Built-in pagination support for user listing and search endpoints, using + `fastapi-pagination`. - Easily batch-add random test users to the database for testing/development purposes using the CLI or seed the database with pre-set users from a CSV file. diff --git a/TODO.md b/TODO.md index a2327820..39d4467a 100644 --- a/TODO.md +++ b/TODO.md @@ -25,9 +25,6 @@ this should be implemented. *This may just need to be in derived projects though, not this template*. - Add Metrics and Observability (eg Prometheus, Grafana, Sentry, etc) -- Add pagination to the user list endpoint. Implement this in a way that is - generic and can be used for other custom endpoints too. The library - 'fastapi-pagination' is really good and performant. - Use an alternative logger if uvicorn is not being used for some reason. - Allow the option of using auto-incerement integers for the User ID (as it is now) or UUID's which is more secure and common these days. This should be