From d7c75b5808dd5eab8420851274a9f5d3d1619e40 Mon Sep 17 00:00:00 2001 From: Payam Date: Fri, 8 Mar 2024 13:45:00 +0330 Subject: [PATCH 1/3] feat: Added count for queries --- app/songs/handlers.py | 32 +++++++++++++++++++++--------- app/songs/schemas.py | 5 +++++ app/utils/pagination.py | 43 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 9 deletions(-) create mode 100644 app/utils/pagination.py diff --git a/app/songs/handlers.py b/app/songs/handlers.py index ae6cc4b..fe26b8e 100644 --- a/app/songs/handlers.py +++ b/app/songs/handlers.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, Query from fastapi.security import OAuth2PasswordBearer from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession @@ -6,21 +6,35 @@ from ..db import get_db_session from ..redis import r +from ..utils.pagination import paginate from .models import City, Song, Tag -from .schemas import CityCreate, CityRead, SongCreate, SongRead, TagCreate, TagRead +from .schemas import ( + CityCreate, + CityRead, + PaginatedSong, + SongCreate, + TagCreate, + TagRead, +) router = APIRouter() oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") -@router.get("/songs", response_model=list[SongRead]) -async def get_songs(session: AsyncSession = Depends(get_db_session)): - result = await session.scalars( - select(Song).options(selectinload(Song.tags), selectinload(Song.city)) - ) - songs = result.all() - return songs +@router.get( + "/songs", + response_model=PaginatedSong, +) +async def get_songs( + page: int = Query(1, ge=1), + per_page: int = Query(5, le=100), + session: AsyncSession = Depends(get_db_session), +): + query = select(Song).options(selectinload(Song.tags), selectinload(Song.city)) + items, pagination = await paginate(session, query, page, per_page) + + return PaginatedSong(data=items, meta=pagination) @router.post("/songs") diff --git a/app/songs/schemas.py b/app/songs/schemas.py index f919816..2ea790b 100644 --- a/app/songs/schemas.py +++ b/app/songs/schemas.py @@ -50,3 +50,8 @@ class SongRead(BaseModel): # class Config: # from_attributes = True + + +class PaginatedSong(BaseModel): + data: list[SongRead] + meta: dict[str, int | None] diff --git a/app/utils/pagination.py b/app/utils/pagination.py new file mode 100644 index 0000000..6be5954 --- /dev/null +++ b/app/utils/pagination.py @@ -0,0 +1,43 @@ +from fastapi import HTTPException +from sqlalchemy import func, select +from sqlalchemy.ext.asyncio import AsyncSession + + +async def paginate( + session: AsyncSession, + query, + page: int = 1, + per_page: int = 10, +): + """ + Paginate a SQLModel query. + :param session: The SQLAlchemy session instance. + :param query: The SQLModel query to paginate. + :param page: The page number to retrieve. + :param per_page: The number of items per page. + :return: Tuple containing the paginated data and pagination information. + """ + total_items = await session.execute(select(func.count()).select_from(query)) + count = total_items.scalar() + + if count == 0: + return [], {"total_pages": 0, "current_page": 0, "next_page": None} + + total_pages = (count - 1) // per_page + 1 + + if page > total_pages: + raise HTTPException(status_code=404, detail="Page not found") + + offset = (page - 1) * per_page + items = await session.scalars(query.offset(offset).limit(per_page)) + result = items.all() + + next_page = page + 1 if page < total_pages else None + + pagination = { + "total_pages": total_pages, + "current_page": page, + "next_page": next_page, + } + + return result, pagination From 6e6a9378d0bacf80b6e5bb933e1a493e473e54f3 Mon Sep 17 00:00:00 2001 From: Payam Date: Fri, 8 Mar 2024 14:10:18 +0330 Subject: [PATCH 2/3] feat: finalized pagination --- app/songs/handlers.py | 22 +++++++++------------- app/utils/pagination.py | 14 +++++++------- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/app/songs/handlers.py b/app/songs/handlers.py index fe26b8e..15ed741 100644 --- a/app/songs/handlers.py +++ b/app/songs/handlers.py @@ -11,7 +11,6 @@ from .schemas import ( CityCreate, CityRead, - PaginatedSong, SongCreate, TagCreate, TagRead, @@ -22,19 +21,16 @@ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") -@router.get( - "/songs", - response_model=PaginatedSong, -) +@router.get("/songs") async def get_songs( - page: int = Query(1, ge=1), - per_page: int = Query(5, le=100), - session: AsyncSession = Depends(get_db_session), + page: int = Query(1, ge=1), + per_page: int = Query(5, le=100), + session: AsyncSession = Depends(get_db_session), ): query = select(Song).options(selectinload(Song.tags), selectinload(Song.city)) items, pagination = await paginate(session, query, page, per_page) - return PaginatedSong(data=items, meta=pagination) + return {"meta": pagination, "data": items} @router.post("/songs") @@ -52,7 +48,7 @@ async def add_song(song: SongCreate, session: AsyncSession = Depends(get_db_sess @router.post("/city", response_model=CityRead) async def create_city( - *, session: AsyncSession = Depends(get_db_session), city: CityCreate + *, session: AsyncSession = Depends(get_db_session), city: CityCreate ): # new_city = City(**city.dict()) new_city = City(name=city.name) @@ -64,7 +60,7 @@ async def create_city( @router.post("/connect_city_with_song") async def connect_city_with_song( - city_title: str, song_title: str, session: AsyncSession = Depends(get_db_session) + city_title: str, song_title: str, session: AsyncSession = Depends(get_db_session) ): city_in_db = await session.scalars(select(City).where(City.name == city_title)) city = city_in_db.first() @@ -81,7 +77,7 @@ async def connect_city_with_song( @router.post("/tags", response_model=TagRead) async def create_tag( - *, session: AsyncSession = Depends(get_db_session), tag: TagCreate + *, session: AsyncSession = Depends(get_db_session), tag: TagCreate ): input_tag = TagCreate.model_validate(tag) new_tag = Tag(**input_tag.dict()) @@ -93,7 +89,7 @@ async def create_tag( @router.post("/attach-tags") async def attach_tag_to_song( - tag_title: str, song_name: str, session: AsyncSession = Depends(get_db_session) + tag_title: str, song_name: str, session: AsyncSession = Depends(get_db_session) ): tag_in_db = await session.scalars( select(Tag).where(Tag.title.like(f"%{tag_title}%")) diff --git a/app/utils/pagination.py b/app/utils/pagination.py index 6be5954..898a7e1 100644 --- a/app/utils/pagination.py +++ b/app/utils/pagination.py @@ -4,10 +4,10 @@ async def paginate( - session: AsyncSession, - query, - page: int = 1, - per_page: int = 10, + session: AsyncSession, + query, + page: int = 1, + per_page: int = 10, ): """ Paginate a SQLModel query. @@ -29,14 +29,14 @@ async def paginate( raise HTTPException(status_code=404, detail="Page not found") offset = (page - 1) * per_page - items = await session.scalars(query.offset(offset).limit(per_page)) - result = items.all() + items = await session.execute(query.offset(offset).limit(per_page)) + result = items.scalars().all() next_page = page + 1 if page < total_pages else None pagination = { - "total_pages": total_pages, "current_page": page, + "total_pages": total_pages, "next_page": next_page, } From 8d8e5874136168f65108be604df69413de2f7fda Mon Sep 17 00:00:00 2001 From: Payam Date: Fri, 8 Mar 2024 14:11:29 +0330 Subject: [PATCH 3/3] fix: pre commit correction --- app/songs/handlers.py | 14 +++++++------- app/utils/pagination.py | 8 ++++---- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/songs/handlers.py b/app/songs/handlers.py index 15ed741..85e044c 100644 --- a/app/songs/handlers.py +++ b/app/songs/handlers.py @@ -23,9 +23,9 @@ @router.get("/songs") async def get_songs( - page: int = Query(1, ge=1), - per_page: int = Query(5, le=100), - session: AsyncSession = Depends(get_db_session), + page: int = Query(1, ge=1), + per_page: int = Query(5, le=100), + session: AsyncSession = Depends(get_db_session), ): query = select(Song).options(selectinload(Song.tags), selectinload(Song.city)) items, pagination = await paginate(session, query, page, per_page) @@ -48,7 +48,7 @@ async def add_song(song: SongCreate, session: AsyncSession = Depends(get_db_sess @router.post("/city", response_model=CityRead) async def create_city( - *, session: AsyncSession = Depends(get_db_session), city: CityCreate + *, session: AsyncSession = Depends(get_db_session), city: CityCreate ): # new_city = City(**city.dict()) new_city = City(name=city.name) @@ -60,7 +60,7 @@ async def create_city( @router.post("/connect_city_with_song") async def connect_city_with_song( - city_title: str, song_title: str, session: AsyncSession = Depends(get_db_session) + city_title: str, song_title: str, session: AsyncSession = Depends(get_db_session) ): city_in_db = await session.scalars(select(City).where(City.name == city_title)) city = city_in_db.first() @@ -77,7 +77,7 @@ async def connect_city_with_song( @router.post("/tags", response_model=TagRead) async def create_tag( - *, session: AsyncSession = Depends(get_db_session), tag: TagCreate + *, session: AsyncSession = Depends(get_db_session), tag: TagCreate ): input_tag = TagCreate.model_validate(tag) new_tag = Tag(**input_tag.dict()) @@ -89,7 +89,7 @@ async def create_tag( @router.post("/attach-tags") async def attach_tag_to_song( - tag_title: str, song_name: str, session: AsyncSession = Depends(get_db_session) + tag_title: str, song_name: str, session: AsyncSession = Depends(get_db_session) ): tag_in_db = await session.scalars( select(Tag).where(Tag.title.like(f"%{tag_title}%")) diff --git a/app/utils/pagination.py b/app/utils/pagination.py index 898a7e1..71f8a61 100644 --- a/app/utils/pagination.py +++ b/app/utils/pagination.py @@ -4,10 +4,10 @@ async def paginate( - session: AsyncSession, - query, - page: int = 1, - per_page: int = 10, + session: AsyncSession, + query, + page: int = 1, + per_page: int = 10, ): """ Paginate a SQLModel query.