Skip to content

Commit

Permalink
add current_page and current_page_backwards parameters to cursor base…
Browse files Browse the repository at this point in the history
…d pagination (#745)

* add current_page and current_page_backwards parameters to cursor based pagination

* improve field descriptions

* Apply suggestions from code review

Co-authored-by: Yurii Karabas <[email protected]>

* make fields optional

* Update fastapi_pagination/cursor.py

* Fix lint errors

---------

Co-authored-by: Yurii Karabas <[email protected]>
  • Loading branch information
cranium and uriyyo authored Jul 17, 2023
1 parent 6b50ad5 commit 99182a4
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 0 deletions.
9 changes: 9 additions & 0 deletions fastapi_pagination/cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ def to_raw_params(self) -> CursorRawParams:
class CursorPage(AbstractPage[T], Generic[T]):
items: Sequence[T]

current_page: Optional[str] = Field(None, description="Cursor to refetch the current page")
current_page_backwards: Optional[str] = Field(
None,
description="Cursor to refetch the current page starting from the last item",
)
previous_page: Optional[str] = Field(None, description="Cursor for the previous page")
next_page: Optional[str] = Field(None, description="Cursor for the next page")

Expand All @@ -86,13 +91,17 @@ def create(
items: Sequence[T],
params: AbstractParams,
*,
current: Optional[Cursor] = None,
current_backwards: Optional[Cursor] = None,
next_: Optional[Cursor] = None,
previous: Optional[Cursor] = None,
**kwargs: Any,
) -> CursorPage[T]:
return create_pydantic_model(
cls,
items=items,
current_page=encode_cursor(current),
current_page_backwards=encode_cursor(current_backwards),
next_page=encode_cursor(next_),
previous_page=encode_cursor(previous),
**kwargs,
Expand Down
2 changes: 2 additions & 0 deletions fastapi_pagination/ext/sqlalchemy.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ def _apply_items_transformer(*args: Any, **kwargs: Any) -> Any:
return create_page(
items,
params=params,
current=page.paging.bookmark_current,
current_backwards=page.paging.bookmark_current_backwards,
previous=page.paging.bookmark_previous if page.paging.has_previous else None,
next_=page.paging.bookmark_next if page.paging.has_next else None,
**(additional_data or {}),
Expand Down
98 changes: 98 additions & 0 deletions tests/ext/test_sqlalchemy_cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ def get_db() -> Iterator[Session]:
def route(db: Session = Depends(get_db)):
return paginate(db, select(sa_user).order_by(sa_user.id, sa_user.name))

@app.get("/first-85", response_model=CursorPage[UserOut])
def route_first(db: Session = Depends(get_db)):
return paginate(db, select(sa_user).where(sa_user.id <= 85).order_by(sa_user.id, sa_user.name))

@app.get("/last-85", response_model=CursorPage[UserOut])
def route_last(db: Session = Depends(get_db)):
return paginate(db, select(sa_user).where(sa_user.id > 15).order_by(sa_user.id, sa_user.name))

@app.get("/no-order", response_model=CursorPage[UserOut])
def route_on_order(db: Session = Depends(get_db)):
return paginate(db, select(sa_user))
Expand Down Expand Up @@ -81,6 +89,96 @@ async def test_cursor(app, client, entities):
assert items == entities


@sqlalchemy20
@mark.asyncio
async def test_cursor_refetch(app, client, entities, postgres_url):
entities = sorted(parse_obj_as(List[UserOut], entities), key=(lambda it: (it.id, it.name)))
first_85_entities = entities[:85]
last_85_entities = entities[15:]

items = []
page_size = 10
cursor = None

while True:
params = {"cursor": cursor} if cursor else {}

resp = await client.get("/first-85", params={**params, "size": page_size})
assert resp.status_code == status.HTTP_200_OK
data = resp.json()

items.extend(parse_obj_as(List[UserOut], data["items"]))

current = data["current_page"]

if data["next_page"] is None:
break

cursor = data["next_page"]

assert items == first_85_entities

items = items[:80]

cursor = current

while True:
params = {"cursor": cursor} if cursor else {}

resp = await client.get("/", params={**params, "size": page_size})
assert resp.status_code == status.HTTP_200_OK
data = resp.json()

items.extend(parse_obj_as(List[UserOut], data["items"]))

if data["next_page"] is None:
break

cursor = data["next_page"]

assert items == entities

items = []

while True:
params = {"cursor": cursor} if cursor else {}

resp = await client.get("/last-85", params={**params, "size": page_size})
assert resp.status_code == status.HTTP_200_OK
data = resp.json()

items = parse_obj_as(List[UserOut], data["items"]) + items

current = data["current_page_backwards"]

if data["previous_page"] is None:
break

cursor = data["previous_page"]

assert items == last_85_entities

items = items[5:]

cursor = current

while True:
params = {"cursor": cursor} if cursor else {}

resp = await client.get("/", params={**params, "size": page_size})
assert resp.status_code == status.HTTP_200_OK
data = resp.json()

items = parse_obj_as(List[UserOut], data["items"]) + items

if data["previous_page"] is None:
break

cursor = data["previous_page"]

assert items == entities


@sqlalchemy20
@mark.asyncio
async def test_no_order(app, client, entities):
Expand Down

0 comments on commit 99182a4

Please sign in to comment.