Skip to content

Commit 99182a4

Browse files
craniumuriyyo
andauthored
add current_page and current_page_backwards parameters to cursor based 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]>
1 parent 6b50ad5 commit 99182a4

File tree

3 files changed

+109
-0
lines changed

3 files changed

+109
-0
lines changed

fastapi_pagination/cursor.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ def to_raw_params(self) -> CursorRawParams:
7575
class CursorPage(AbstractPage[T], Generic[T]):
7676
items: Sequence[T]
7777

78+
current_page: Optional[str] = Field(None, description="Cursor to refetch the current page")
79+
current_page_backwards: Optional[str] = Field(
80+
None,
81+
description="Cursor to refetch the current page starting from the last item",
82+
)
7883
previous_page: Optional[str] = Field(None, description="Cursor for the previous page")
7984
next_page: Optional[str] = Field(None, description="Cursor for the next page")
8085

@@ -86,13 +91,17 @@ def create(
8691
items: Sequence[T],
8792
params: AbstractParams,
8893
*,
94+
current: Optional[Cursor] = None,
95+
current_backwards: Optional[Cursor] = None,
8996
next_: Optional[Cursor] = None,
9097
previous: Optional[Cursor] = None,
9198
**kwargs: Any,
9299
) -> CursorPage[T]:
93100
return create_pydantic_model(
94101
cls,
95102
items=items,
103+
current_page=encode_cursor(current),
104+
current_page_backwards=encode_cursor(current_backwards),
96105
next_page=encode_cursor(next_),
97106
previous_page=encode_cursor(previous),
98107
**kwargs,

fastapi_pagination/ext/sqlalchemy.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ def _apply_items_transformer(*args: Any, **kwargs: Any) -> Any:
105105
return create_page(
106106
items,
107107
params=params,
108+
current=page.paging.bookmark_current,
109+
current_backwards=page.paging.bookmark_current_backwards,
108110
previous=page.paging.bookmark_previous if page.paging.has_previous else None,
109111
next_=page.paging.bookmark_next if page.paging.has_next else None,
110112
**(additional_data or {}),

tests/ext/test_sqlalchemy_cursor.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@ def get_db() -> Iterator[Session]:
3131
def route(db: Session = Depends(get_db)):
3232
return paginate(db, select(sa_user).order_by(sa_user.id, sa_user.name))
3333

34+
@app.get("/first-85", response_model=CursorPage[UserOut])
35+
def route_first(db: Session = Depends(get_db)):
36+
return paginate(db, select(sa_user).where(sa_user.id <= 85).order_by(sa_user.id, sa_user.name))
37+
38+
@app.get("/last-85", response_model=CursorPage[UserOut])
39+
def route_last(db: Session = Depends(get_db)):
40+
return paginate(db, select(sa_user).where(sa_user.id > 15).order_by(sa_user.id, sa_user.name))
41+
3442
@app.get("/no-order", response_model=CursorPage[UserOut])
3543
def route_on_order(db: Session = Depends(get_db)):
3644
return paginate(db, select(sa_user))
@@ -81,6 +89,96 @@ async def test_cursor(app, client, entities):
8189
assert items == entities
8290

8391

92+
@sqlalchemy20
93+
@mark.asyncio
94+
async def test_cursor_refetch(app, client, entities, postgres_url):
95+
entities = sorted(parse_obj_as(List[UserOut], entities), key=(lambda it: (it.id, it.name)))
96+
first_85_entities = entities[:85]
97+
last_85_entities = entities[15:]
98+
99+
items = []
100+
page_size = 10
101+
cursor = None
102+
103+
while True:
104+
params = {"cursor": cursor} if cursor else {}
105+
106+
resp = await client.get("/first-85", params={**params, "size": page_size})
107+
assert resp.status_code == status.HTTP_200_OK
108+
data = resp.json()
109+
110+
items.extend(parse_obj_as(List[UserOut], data["items"]))
111+
112+
current = data["current_page"]
113+
114+
if data["next_page"] is None:
115+
break
116+
117+
cursor = data["next_page"]
118+
119+
assert items == first_85_entities
120+
121+
items = items[:80]
122+
123+
cursor = current
124+
125+
while True:
126+
params = {"cursor": cursor} if cursor else {}
127+
128+
resp = await client.get("/", params={**params, "size": page_size})
129+
assert resp.status_code == status.HTTP_200_OK
130+
data = resp.json()
131+
132+
items.extend(parse_obj_as(List[UserOut], data["items"]))
133+
134+
if data["next_page"] is None:
135+
break
136+
137+
cursor = data["next_page"]
138+
139+
assert items == entities
140+
141+
items = []
142+
143+
while True:
144+
params = {"cursor": cursor} if cursor else {}
145+
146+
resp = await client.get("/last-85", params={**params, "size": page_size})
147+
assert resp.status_code == status.HTTP_200_OK
148+
data = resp.json()
149+
150+
items = parse_obj_as(List[UserOut], data["items"]) + items
151+
152+
current = data["current_page_backwards"]
153+
154+
if data["previous_page"] is None:
155+
break
156+
157+
cursor = data["previous_page"]
158+
159+
assert items == last_85_entities
160+
161+
items = items[5:]
162+
163+
cursor = current
164+
165+
while True:
166+
params = {"cursor": cursor} if cursor else {}
167+
168+
resp = await client.get("/", params={**params, "size": page_size})
169+
assert resp.status_code == status.HTTP_200_OK
170+
data = resp.json()
171+
172+
items = parse_obj_as(List[UserOut], data["items"]) + items
173+
174+
if data["previous_page"] is None:
175+
break
176+
177+
cursor = data["previous_page"]
178+
179+
assert items == entities
180+
181+
84182
@sqlalchemy20
85183
@mark.asyncio
86184
async def test_no_order(app, client, entities):

0 commit comments

Comments
 (0)