Skip to content

Commit fd540e2

Browse files
committed
Split CursorPaginatedResultPresenter logic in multiple methods
1 parent 4dcb05e commit fd540e2

File tree

8 files changed

+157
-75
lines changed

8 files changed

+157
-75
lines changed

sqlalchemy_bind_manager/_repository/async_.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ async def cursor_paginated_find(
188188
self,
189189
items_per_page: int,
190190
cursor_reference: Union[CursorReference, None] = None,
191-
is_end_cursor: bool = False,
191+
is_before_cursor: bool = False,
192192
search_params: Union[None, Mapping[str, Any]] = None,
193193
) -> CursorPaginatedResult[MODEL]:
194194
"""Find models using filters and cursor based pagination
@@ -212,7 +212,7 @@ async def cursor_paginated_find(
212212
paginated_stmt = self._cursor_paginated_query(
213213
find_stmt,
214214
cursor_reference=cursor_reference,
215-
is_end_cursor=is_end_cursor,
215+
is_before_cursor=is_before_cursor,
216216
per_page=items_per_page,
217217
)
218218

@@ -229,5 +229,5 @@ async def cursor_paginated_find(
229229
total_items_count=total_items_count,
230230
items_per_page=self._sanitised_query_limit(items_per_page),
231231
cursor_reference=cursor_reference,
232-
is_end_cursor=is_end_cursor,
232+
is_before_cursor=is_before_cursor,
233233
)

sqlalchemy_bind_manager/_repository/base_repository.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ def _cursor_paginated_query(
174174
self,
175175
stmt: Select,
176176
cursor_reference: Union[CursorReference, None],
177-
is_end_cursor: bool = False,
177+
is_before_cursor: bool = False,
178178
per_page: int = _max_query_limit,
179179
) -> Select:
180180
"""Build the query offset and limit clauses from submitted parameters.
@@ -198,7 +198,7 @@ def _cursor_paginated_query(
198198
)
199199

200200
# TODO: Use window functions
201-
if not is_end_cursor:
201+
if not is_before_cursor:
202202
previous_query = stmt.where(
203203
getattr(self._model, cursor_reference.column) <= cursor_reference.value
204204
)

sqlalchemy_bind_manager/_repository/result_presenters.py

Lines changed: 138 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,14 @@
1414

1515

1616
class CursorPaginatedResultPresenter:
17-
@staticmethod
17+
@classmethod
1818
def build_result(
19+
cls,
1920
result_items: List[MODEL],
2021
total_items_count: int,
2122
items_per_page: int,
2223
cursor_reference: Union[CursorReference, None],
23-
is_end_cursor: bool,
24+
is_before_cursor: bool,
2425
) -> CursorPaginatedResult:
2526
"""
2627
Produces a structured paginated result identifying previous/next pages
@@ -30,72 +31,153 @@ def build_result(
3031
:param total_items_count:
3132
:param items_per_page:
3233
:param cursor_reference:
33-
:param is_end_cursor:
34+
:param is_before_cursor:
3435
:return:
3536
"""
36-
result_structure: CursorPaginatedResult = CursorPaginatedResult(
37+
if not result_items:
38+
return cls._build_empty_items_result(total_items_count, items_per_page)
39+
40+
if not cursor_reference:
41+
return cls._build_no_cursor_result(
42+
result_items, total_items_count, items_per_page
43+
)
44+
45+
if is_before_cursor:
46+
return cls._build_before_cursor_result(
47+
result_items, total_items_count, items_per_page, cursor_reference
48+
)
49+
50+
return cls._build_after_cursor_result(
51+
result_items, total_items_count, items_per_page, cursor_reference
52+
)
53+
54+
@staticmethod
55+
def _build_empty_items_result(
56+
total_items_count: int,
57+
items_per_page: int,
58+
) -> CursorPaginatedResult:
59+
return CursorPaginatedResult(
60+
items=[],
61+
page_info=CursorPageInfo(
62+
items_per_page=items_per_page,
63+
total_items=total_items_count,
64+
),
65+
)
66+
67+
@staticmethod
68+
def _build_no_cursor_result(
69+
result_items: List[MODEL],
70+
total_items_count: int,
71+
items_per_page: int,
72+
) -> CursorPaginatedResult:
73+
has_next_page = len(result_items) > items_per_page
74+
if has_next_page:
75+
result_items = result_items[0:items_per_page]
76+
reference_column = _pk_from_result_object(result_items[0])
77+
78+
return CursorPaginatedResult(
3779
items=result_items,
3880
page_info=CursorPageInfo(
3981
items_per_page=items_per_page,
4082
total_items=total_items_count,
83+
has_previous_page=False,
84+
has_next_page=has_next_page,
85+
start_cursor=CursorReference(
86+
column=reference_column,
87+
value=getattr(result_items[0], reference_column),
88+
),
89+
end_cursor=CursorReference(
90+
column=reference_column,
91+
value=getattr(result_items[-1], reference_column),
92+
),
4193
),
4294
)
43-
if not result_items:
44-
return result_structure
4595

46-
if not cursor_reference:
47-
has_previous_page = False
48-
has_next_page = len(result_items) > items_per_page
49-
if has_next_page:
50-
result_items = result_items[0:items_per_page]
51-
result_structure.page_info.has_next_page = has_next_page
52-
result_structure.page_info.has_previous_page = has_previous_page
53-
reference_column = _pk_from_result_object(result_items[0])
54-
55-
elif is_end_cursor:
56-
index = -1
57-
reference_column = cursor_reference.column
58-
last_found_cursor_value = getattr(result_items[index], reference_column)
59-
if not isinstance(last_found_cursor_value, type(cursor_reference.value)):
60-
raise TypeError(
61-
"Values from CursorReference and results must be of the same type"
96+
@staticmethod
97+
def _build_before_cursor_result(
98+
result_items: List[MODEL],
99+
total_items_count: int,
100+
items_per_page: int,
101+
cursor_reference: CursorReference,
102+
) -> CursorPaginatedResult:
103+
index = -1
104+
reference_column = cursor_reference.column
105+
last_found_cursor_value = getattr(result_items[index], reference_column)
106+
if not isinstance(last_found_cursor_value, type(cursor_reference.value)):
107+
raise TypeError(
108+
"Values from CursorReference and results must be of the same type"
109+
)
110+
has_next_page = last_found_cursor_value >= cursor_reference.value
111+
if has_next_page:
112+
result_items.pop(index)
113+
has_previous_page = len(result_items) > items_per_page
114+
if has_previous_page:
115+
result_items = result_items[-items_per_page:]
116+
117+
return CursorPaginatedResult(
118+
items=result_items,
119+
page_info=CursorPageInfo(
120+
items_per_page=items_per_page,
121+
total_items=total_items_count,
122+
has_previous_page=has_previous_page,
123+
has_next_page=has_next_page,
124+
start_cursor=CursorReference(
125+
column=reference_column,
126+
value=getattr(result_items[0], reference_column),
62127
)
63-
has_next_page = last_found_cursor_value >= cursor_reference.value
64-
if has_next_page:
65-
result_items.pop(index)
66-
has_previous_page = len(result_items) > items_per_page
67-
if has_previous_page:
68-
result_items = result_items[-items_per_page:]
69-
else:
70-
index = 0
71-
reference_column = cursor_reference.column
72-
first_found_cursor_value = getattr(result_items[index], reference_column)
73-
if not isinstance(first_found_cursor_value, type(cursor_reference.value)):
74-
raise TypeError(
75-
"Values from CursorReference and results must be of the same type"
128+
if result_items
129+
else None,
130+
end_cursor=CursorReference(
131+
column=reference_column,
132+
value=getattr(result_items[-1], reference_column),
76133
)
77-
has_previous_page = first_found_cursor_value <= cursor_reference.value
78-
if has_previous_page:
79-
result_items.pop(index)
80-
has_next_page = len(result_items) > items_per_page
81-
if has_next_page:
82-
result_items = result_items[0:items_per_page]
83-
84-
result_structure.items = result_items
85-
result_structure.page_info.has_next_page = has_next_page
86-
result_structure.page_info.has_previous_page = has_previous_page
87-
88-
if result_items:
89-
result_structure.page_info.start_cursor = CursorReference(
90-
column=reference_column,
91-
value=getattr(result_items[0], reference_column),
92-
)
93-
result_structure.page_info.end_cursor = CursorReference(
94-
column=reference_column,
95-
value=getattr(result_items[-1], reference_column),
96-
)
134+
if result_items
135+
else None,
136+
),
137+
)
97138

98-
return result_structure
139+
@staticmethod
140+
def _build_after_cursor_result(
141+
result_items: List[MODEL],
142+
total_items_count: int,
143+
items_per_page: int,
144+
cursor_reference: CursorReference,
145+
) -> CursorPaginatedResult:
146+
index = 0
147+
reference_column = cursor_reference.column
148+
first_found_cursor_value = getattr(result_items[index], reference_column)
149+
if not isinstance(first_found_cursor_value, type(cursor_reference.value)):
150+
raise TypeError(
151+
"Values from CursorReference and results must be of the same type"
152+
)
153+
has_previous_page = first_found_cursor_value <= cursor_reference.value
154+
if has_previous_page:
155+
result_items.pop(index)
156+
has_next_page = len(result_items) > items_per_page
157+
if has_next_page:
158+
result_items = result_items[0:items_per_page]
159+
160+
return CursorPaginatedResult(
161+
items=result_items,
162+
page_info=CursorPageInfo(
163+
items_per_page=items_per_page,
164+
total_items=total_items_count,
165+
has_previous_page=has_previous_page,
166+
has_next_page=has_next_page,
167+
start_cursor=CursorReference(
168+
column=reference_column,
169+
value=getattr(result_items[0], reference_column),
170+
)
171+
if result_items
172+
else None,
173+
end_cursor=CursorReference(
174+
column=reference_column,
175+
value=getattr(result_items[-1], reference_column),
176+
)
177+
if result_items
178+
else None,
179+
),
180+
)
99181

100182

101183
class PaginatedResultPresenter:

sqlalchemy_bind_manager/_repository/sync.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ def cursor_paginated_find(
176176
self,
177177
items_per_page: int,
178178
cursor_reference: Union[CursorReference, None] = None,
179-
is_end_cursor: bool = False,
179+
is_before_cursor: bool = False,
180180
search_params: Union[None, Mapping[str, Any]] = None,
181181
) -> CursorPaginatedResult[MODEL]:
182182
"""Find models using filters and cursor based pagination
@@ -201,7 +201,7 @@ def cursor_paginated_find(
201201
paginated_stmt = self._cursor_paginated_query(
202202
find_stmt,
203203
cursor_reference=cursor_reference,
204-
is_end_cursor=is_end_cursor,
204+
is_before_cursor=is_before_cursor,
205205
per_page=items_per_page,
206206
)
207207

@@ -216,5 +216,5 @@ def cursor_paginated_find(
216216
total_items_count=total_items_count,
217217
items_per_page=self._sanitised_query_limit(items_per_page),
218218
cursor_reference=cursor_reference,
219-
is_end_cursor=is_end_cursor,
219+
is_before_cursor=is_before_cursor,
220220
)

sqlalchemy_bind_manager/protocols.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ async def cursor_paginated_find(
5555
self,
5656
items_per_page: int,
5757
cursor_reference: Union[CursorReference, None] = None,
58-
is_end_cursor: bool = False,
58+
is_before_cursor: bool = False,
5959
search_params: Union[None, Mapping[str, Any]] = None,
6060
) -> CursorPaginatedResult[MODEL]:
6161
...
@@ -95,7 +95,7 @@ def cursor_paginated_find(
9595
self,
9696
items_per_page: int,
9797
cursor_reference: Union[CursorReference, None] = None,
98-
is_end_cursor: bool = False,
98+
is_before_cursor: bool = False,
9999
search_params: Union[None, Mapping[str, Any]] = None,
100100
) -> CursorPaginatedResult[MODEL]:
101101
...

tests/repository/async_/test_cursor_pagination.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ async def test_paginated_find_page_length_before(
104104
column="model_id",
105105
value=110,
106106
),
107-
is_end_cursor=True,
107+
is_before_cursor=True,
108108
items_per_page=2,
109109
)
110110
assert len(results.items) == 2
@@ -192,7 +192,7 @@ async def test_paginated_find_previous_next_page(
192192
column="model_id",
193193
value=after or before,
194194
),
195-
is_end_cursor=bool(before),
195+
is_before_cursor=bool(before),
196196
items_per_page=2,
197197
)
198198

@@ -254,7 +254,7 @@ async def test_paginated_find_string_pk(
254254
column="model_id",
255255
value=after or before,
256256
),
257-
is_end_cursor=bool(before),
257+
is_before_cursor=bool(before),
258258
items_per_page=2,
259259
)
260260

tests/repository/result_presenters/test_cursor_paginated_result_presenter.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@ class MyModel:
1414
name: str
1515

1616

17-
@pytest.mark.parametrize(["is_end_cursor"], [(True,), (False,)])
18-
def test_fails_if_reference_cursor_wrong_type(is_end_cursor):
17+
@pytest.mark.parametrize(["is_before_cursor"], [(True,), (False,)])
18+
def test_fails_if_reference_cursor_wrong_type(is_before_cursor):
1919
with pytest.raises(TypeError):
2020
CursorPaginatedResultPresenter.build_result(
2121
result_items=[MyModel(model_id=1, name="test")],
2222
total_items_count=10,
2323
items_per_page=1,
2424
cursor_reference=CursorReference(column="model_id", value="1"),
25-
is_end_cursor=is_end_cursor,
25+
is_before_cursor=is_before_cursor,
2626
)

tests/repository/sync/test_cursor_pagination.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ def test_paginated_find_page_length_before(repository_class, model_class, sa_man
9494
column="model_id",
9595
value=110,
9696
),
97-
is_end_cursor=True,
97+
is_before_cursor=True,
9898
items_per_page=2,
9999
)
100100
assert len(results.items) == 2
@@ -184,7 +184,7 @@ def test_paginated_find_previous_next_page(
184184
column="model_id",
185185
value=after or before,
186186
),
187-
is_end_cursor=bool(before),
187+
is_before_cursor=bool(before),
188188
items_per_page=2,
189189
)
190190

@@ -246,7 +246,7 @@ def test_paginated_find_string_pk(
246246
column="model_id",
247247
value=after or before,
248248
),
249-
is_end_cursor=bool(before),
249+
is_before_cursor=bool(before),
250250
items_per_page=2,
251251
)
252252

0 commit comments

Comments
 (0)