Skip to content

Commit d83ef25

Browse files
committed
Move paginated presentation logic in separate classes
1 parent eafa015 commit d83ef25

File tree

4 files changed

+164
-149
lines changed

4 files changed

+164
-149
lines changed

sqlalchemy_bind_manager/_repository/async_.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
CursorReference,
2929
PaginatedResult,
3030
)
31+
from .result_presenters import CursorPaginatedResultPresenter, PaginatedResultPresenter
3132

3233

3334
class SQLAlchemyAsyncRepository(Generic[MODEL], BaseRepository[MODEL], ABC):
@@ -176,11 +177,11 @@ async def paginated_find(
176177
x for x in (await session.execute(paginated_stmt)).scalars()
177178
]
178179

179-
return self._build_page_paginated_result(
180+
return PaginatedResultPresenter.build_result(
180181
result_items=result_items,
181182
total_items_count=total_items_count,
182183
page=page,
183-
items_per_page=items_per_page,
184+
items_per_page=self._sanitised_query_limit(items_per_page),
184185
)
185186

186187
async def cursor_paginated_find(
@@ -223,10 +224,10 @@ async def cursor_paginated_find(
223224
x for x in (await session.execute(paginated_stmt)).scalars()
224225
] or []
225226

226-
return self._build_cursor_paginated_result(
227+
return CursorPaginatedResultPresenter.build_result(
227228
result_items=result_items,
228229
total_items_count=total_items_count,
229-
items_per_page=items_per_page,
230+
items_per_page=self._sanitised_query_limit(items_per_page),
230231
cursor_reference=cursor_reference,
231232
is_end_cursor=is_end_cursor,
232233
)

sqlalchemy_bind_manager/_repository/base_repository.py

Lines changed: 0 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
from abc import ABC
22
from enum import Enum
33
from functools import partial
4-
from math import ceil
54
from typing import (
65
Any,
7-
Collection,
86
Generic,
97
Iterable,
10-
List,
118
Mapping,
129
Tuple,
1310
Type,
@@ -23,11 +20,7 @@
2320

2421
from .common import (
2522
MODEL,
26-
CursorPageInfo,
27-
CursorPaginatedResult,
2823
CursorReference,
29-
PageInfo,
30-
PaginatedResult,
3124
)
3225

3326

@@ -265,140 +258,6 @@ def _cursor_paginated_query(
265258
def _sanitised_query_limit(self, limit):
266259
return max(min(limit, self._max_query_limit), 0)
267260

268-
def _build_page_paginated_result(
269-
self,
270-
result_items: Collection[MODEL],
271-
total_items_count: int,
272-
page: int,
273-
items_per_page: int,
274-
) -> PaginatedResult:
275-
276-
_per_page = self._sanitised_query_limit(items_per_page)
277-
total_pages = (
278-
0
279-
if total_items_count == 0 or total_items_count is None
280-
else ceil(total_items_count / _per_page)
281-
)
282-
283-
_page = 0 if len(result_items) == 0 else min(page, total_pages)
284-
has_next_page = _page and _page < total_pages
285-
has_previous_page = _page and _page > 1
286-
287-
return PaginatedResult(
288-
items=result_items,
289-
page_info=PageInfo(
290-
page=_page,
291-
items_per_page=_per_page,
292-
total_items=total_items_count,
293-
total_pages=total_pages,
294-
has_next_page=has_next_page,
295-
has_previous_page=has_previous_page,
296-
),
297-
)
298-
299-
def _build_cursor_paginated_result(
300-
self,
301-
result_items: List[MODEL],
302-
total_items_count: int,
303-
items_per_page: int,
304-
cursor_reference: Union[CursorReference, None],
305-
is_end_cursor: bool,
306-
) -> CursorPaginatedResult:
307-
"""
308-
Produces a structured paginated result identifying previous/next pages
309-
and slicing results accordingly.
310-
311-
:param result_items:
312-
:param total_items_count:
313-
:param items_per_page:
314-
:param cursor_reference:
315-
:param is_end_cursor:
316-
:return:
317-
"""
318-
319-
sanitised_query_limit = self._sanitised_query_limit(items_per_page)
320-
321-
result_structure: CursorPaginatedResult = CursorPaginatedResult(
322-
items=result_items,
323-
page_info=CursorPageInfo(
324-
items_per_page=sanitised_query_limit,
325-
total_items=total_items_count,
326-
),
327-
)
328-
if not result_items:
329-
return result_structure
330-
331-
if not cursor_reference:
332-
has_previous_page = False
333-
has_next_page = len(result_items) > sanitised_query_limit
334-
if has_next_page:
335-
result_items = result_items[0:sanitised_query_limit]
336-
# TODO: Infer default cursor format from model
337-
result_structure.page_info.has_next_page = has_next_page
338-
result_structure.page_info.has_previous_page = has_previous_page
339-
reference_column = self._model_pk()
340-
341-
elif is_end_cursor:
342-
index = -1
343-
reference_column = cursor_reference.column
344-
last_found_cursor_value = getattr(result_items[index], reference_column)
345-
"""
346-
Currently we support only numeric or string model values for cursors,
347-
but pydantic models (cursor) coerce always the value as string.
348-
This mean if the value is not actually string we need to cast to
349-
ensure correct ordering is evaluated.
350-
e.g.
351-
9 < 10 but '9' > '10'
352-
"""
353-
if isinstance(last_found_cursor_value, str):
354-
has_next_page = last_found_cursor_value >= cursor_reference.value
355-
else:
356-
has_next_page = last_found_cursor_value >= float(cursor_reference.value)
357-
if has_next_page:
358-
result_items.pop(index)
359-
has_previous_page = len(result_items) > sanitised_query_limit
360-
if has_previous_page:
361-
result_items = result_items[-sanitised_query_limit:]
362-
else:
363-
index = 0
364-
reference_column = cursor_reference.column
365-
first_found_cursor_value = getattr(result_items[index], reference_column)
366-
"""
367-
Currently we support only numeric or string model values for cursors,
368-
but pydantic models (cursor) coerce always the value as string.
369-
This mean if the value is not actually string we need to cast to
370-
ensure correct ordering is evaluated.
371-
e.g.
372-
9 < 10 but '9' > '10'
373-
"""
374-
if isinstance(first_found_cursor_value, str):
375-
has_previous_page = first_found_cursor_value <= cursor_reference.value
376-
else:
377-
has_previous_page = first_found_cursor_value <= float(
378-
cursor_reference.value
379-
)
380-
if has_previous_page:
381-
result_items.pop(index)
382-
has_next_page = len(result_items) > sanitised_query_limit
383-
if has_next_page:
384-
result_items = result_items[0:sanitised_query_limit]
385-
386-
result_structure.items = result_items
387-
result_structure.page_info.has_next_page = has_next_page
388-
result_structure.page_info.has_previous_page = has_previous_page
389-
390-
if result_items:
391-
result_structure.page_info.start_cursor = CursorReference(
392-
column=reference_column,
393-
value=getattr(result_items[0], reference_column),
394-
)
395-
result_structure.page_info.end_cursor = CursorReference(
396-
column=reference_column,
397-
value=getattr(result_items[-1], reference_column),
398-
)
399-
400-
return result_structure
401-
402261
def _model_pk(self) -> str:
403262
primary_keys = inspect(self._model).primary_key # type: ignore
404263
if len(primary_keys) > 1:
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
from math import ceil
2+
from typing import Collection, List, Union
3+
4+
from sqlalchemy import inspect
5+
6+
from .common import (
7+
MODEL,
8+
CursorPageInfo,
9+
CursorPaginatedResult,
10+
CursorReference,
11+
PageInfo,
12+
PaginatedResult,
13+
)
14+
15+
16+
class CursorPaginatedResultPresenter:
17+
@staticmethod
18+
def build_result(
19+
result_items: List[MODEL],
20+
total_items_count: int,
21+
items_per_page: int,
22+
cursor_reference: Union[CursorReference, None],
23+
is_end_cursor: bool,
24+
) -> CursorPaginatedResult:
25+
"""
26+
Produces a structured paginated result identifying previous/next pages
27+
and slicing results accordingly.
28+
29+
:param result_items:
30+
:param total_items_count:
31+
:param items_per_page:
32+
:param cursor_reference:
33+
:param is_end_cursor:
34+
:return:
35+
"""
36+
result_structure: CursorPaginatedResult = CursorPaginatedResult(
37+
items=result_items,
38+
page_info=CursorPageInfo(
39+
items_per_page=items_per_page,
40+
total_items=total_items_count,
41+
),
42+
)
43+
if not result_items:
44+
return result_structure
45+
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+
"""
60+
Currently we support only numeric or string model values for cursors,
61+
but pydantic models (cursor) coerce always the value as string.
62+
This mean if the value is not actually string we need to cast to
63+
ensure correct ordering is evaluated.
64+
e.g.
65+
9 < 10 but '9' > '10'
66+
"""
67+
if isinstance(last_found_cursor_value, str):
68+
has_next_page = last_found_cursor_value >= cursor_reference.value
69+
else:
70+
has_next_page = last_found_cursor_value >= float(cursor_reference.value)
71+
if has_next_page:
72+
result_items.pop(index)
73+
has_previous_page = len(result_items) > items_per_page
74+
if has_previous_page:
75+
result_items = result_items[-items_per_page:]
76+
else:
77+
index = 0
78+
reference_column = cursor_reference.column
79+
first_found_cursor_value = getattr(result_items[index], reference_column)
80+
"""
81+
Currently we support only numeric or string model values for cursors,
82+
but pydantic models (cursor) coerce always the value as string.
83+
This mean if the value is not actually string we need to cast to
84+
ensure correct ordering is evaluated.
85+
e.g.
86+
9 < 10 but '9' > '10'
87+
"""
88+
if isinstance(first_found_cursor_value, str):
89+
has_previous_page = first_found_cursor_value <= cursor_reference.value
90+
else:
91+
has_previous_page = first_found_cursor_value <= float(
92+
cursor_reference.value
93+
)
94+
if has_previous_page:
95+
result_items.pop(index)
96+
has_next_page = len(result_items) > items_per_page
97+
if has_next_page:
98+
result_items = result_items[0:items_per_page]
99+
100+
result_structure.items = result_items
101+
result_structure.page_info.has_next_page = has_next_page
102+
result_structure.page_info.has_previous_page = has_previous_page
103+
104+
if result_items:
105+
result_structure.page_info.start_cursor = CursorReference(
106+
column=reference_column,
107+
value=getattr(result_items[0], reference_column),
108+
)
109+
result_structure.page_info.end_cursor = CursorReference(
110+
column=reference_column,
111+
value=getattr(result_items[-1], reference_column),
112+
)
113+
114+
return result_structure
115+
116+
117+
class PaginatedResultPresenter:
118+
@staticmethod
119+
def build_result(
120+
result_items: Collection[MODEL],
121+
total_items_count: int,
122+
page: int,
123+
items_per_page: int,
124+
) -> PaginatedResult:
125+
126+
total_pages = (
127+
0
128+
if total_items_count == 0 or total_items_count is None
129+
else ceil(total_items_count / items_per_page)
130+
)
131+
132+
_page = 0 if len(result_items) == 0 else min(page, total_pages)
133+
has_next_page = _page and _page < total_pages
134+
has_previous_page = _page and _page > 1
135+
136+
return PaginatedResult(
137+
items=result_items,
138+
page_info=PageInfo(
139+
page=_page,
140+
items_per_page=items_per_page,
141+
total_items=total_items_count,
142+
total_pages=total_pages,
143+
has_next_page=has_next_page,
144+
has_previous_page=has_previous_page,
145+
),
146+
)
147+
148+
149+
def _pk_from_result_object(model) -> str:
150+
primary_keys = inspect(type(model)).primary_key # type: ignore
151+
if len(primary_keys) > 1:
152+
raise NotImplementedError("Composite primary keys are not supported.")
153+
154+
return primary_keys[0].name

sqlalchemy_bind_manager/_repository/sync.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
CursorReference,
2929
PaginatedResult,
3030
)
31+
from .result_presenters import CursorPaginatedResultPresenter, PaginatedResultPresenter
3132

3233

3334
class SQLAlchemyRepository(Generic[MODEL], BaseRepository[MODEL], ABC):
@@ -164,11 +165,11 @@ def paginated_find(
164165
)
165166
result_items = [x for x in session.execute(paginated_stmt).scalars()]
166167

167-
return self._build_page_paginated_result(
168+
return PaginatedResultPresenter.build_result(
168169
result_items=result_items,
169170
total_items_count=total_items_count,
170171
page=page,
171-
items_per_page=items_per_page,
172+
items_per_page=self._sanitised_query_limit(items_per_page),
172173
)
173174

174175
def cursor_paginated_find(
@@ -210,10 +211,10 @@ def cursor_paginated_find(
210211
)
211212
result_items = [x for x in session.execute(paginated_stmt).scalars()]
212213

213-
return self._build_cursor_paginated_result(
214+
return CursorPaginatedResultPresenter.build_result(
214215
result_items=result_items,
215216
total_items_count=total_items_count,
216-
items_per_page=items_per_page,
217+
items_per_page=self._sanitised_query_limit(items_per_page),
217218
cursor_reference=cursor_reference,
218219
is_end_cursor=is_end_cursor,
219220
)

0 commit comments

Comments
 (0)