Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support async queryset evaluation #49

Merged
merged 11 commits into from
Jul 1, 2024
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,19 @@ def posts_api(request, after=None):
'last_cursor': paginator.cursor(page[-1])
}
return data


async def posts_api_async(request, after=None):
qs = Post.objects.all()
page_size = 10
paginator = CursorPaginator(qs, ordering=('-created', '-id'))
page = await paginator.apage(first=page_size, after=after)
data = {
'objects': [serialize_page(p) for p in page],
'has_next_page': page.has_next,
'last_cursor': paginator.cursor(page[-1])
}
return data
```

Reverse pagination can be achieved by using the `last` and `before` arguments
Expand Down
43 changes: 33 additions & 10 deletions cursor_pagination.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from asgiref.sync import sync_to_async
from base64 import b64decode, b64encode
from collections.abc import Sequence

Expand Down Expand Up @@ -68,17 +69,17 @@ def _nulls_ordering(self, ordering, from_last=False):

return nulls_ordering



def page(self, first=None, last=None, after=None, before=None):
qs = self.queryset
def _apply_paginator_arguments(self, qs, first=None, last=None, after=None, before=None):
"""
Apply first/after, last/before filtering to the queryset
"""
page_size = first or last
if page_size is None:
return CursorPage(qs, self)

from_last = last is not None
if from_last and first is not None:
raise ValueError('Cannot process first and last')
raise ValueError('Cannot process first and last')

if after is not None:
qs = self.apply_cursor(after, qs, from_last=from_last)
Expand All @@ -89,11 +90,10 @@ def page(self, first=None, last=None, after=None, before=None):
if last is not None:
qs = qs.order_by(*self._nulls_ordering(reverse_ordering(self.ordering), from_last=True))[:last + 1]

qs = list(qs)
items = qs[:page_size]
if last is not None:
items.reverse()
has_additional = len(qs) > len(items)
def _get_cursor_page(self, items, has_additional, first, last, after, before):
"""
Create and return the cursor page for the given items
"""
additional_kwargs = {}
if first is not None:
additional_kwargs['has_next'] = has_additional
Expand All @@ -103,6 +103,29 @@ def page(self, first=None, last=None, after=None, before=None):
additional_kwargs['has_next'] = bool(before)
return CursorPage(items, self, **additional_kwargs)

def page(self, first=None, last=None, after=None, before=None):
qs = self.queryset
self._apply_paginator_arguments(qs, first, last, after, before)

qs = list(qs)
items = qs[:page_size]
if last is not None:
items.reverse()
has_additional = len(qs) > len(items)

return self._get_cursor_page(items, has_additional, first, last, after, before)

async def apage(self, first=None, last=None, after=None, before=None):
qs = self.queryset
self._apply_paginator_arguments(qs, first, last, after, before)

items = await sync_to_async(list)(qs[:page_size])
if last is not None:
items.reverse()
bradleyoesch marked this conversation as resolved.
Show resolved Hide resolved
has_additional = (await qs.acount()) > len(items)

return self._get_cursor_page(items, has_additional, first, last, after, before)

def apply_cursor(self, cursor, queryset, from_last, reverse=False):
position = self.decode_cursor(cursor)

Expand Down
22 changes: 22 additions & 0 deletions tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ def test_empty(self):
self.assertFalse(page.has_next)
self.assertFalse(page.has_previous)

async def test_async_empty(self):
paginator = CursorPaginator(Post.objects.all(), ('id',))
page = await paginator.apage()
self.assertEqual(len(page), 0)
self.assertFalse(page.has_next)
self.assertFalse(page.has_previous)

def test_with_items(self):
for i in range(20):
Post.objects.create(name='Name %s' % i)
Expand All @@ -27,6 +34,15 @@ def test_with_items(self):
self.assertFalse(page.has_next)
self.assertFalse(page.has_previous)

async def test_async_with_items(self):
for i in range(20):
await Post.objects.acreate(name='Name %s' % i)
paginator = CursorPaginator(Post.objects.all(), ('id',))
page = await paginator.apage()
self.assertEqual(len(page), 20)
self.assertFalse(page.has_next)
self.assertFalse(page.has_previous)


class TestForwardPagination(TestCase):

Expand All @@ -45,6 +61,12 @@ def test_first_page(self):
self.assertTrue(page.has_next)
self.assertFalse(page.has_previous)

async def test_async_first_page(self):
page = await self.paginator.apage(first=2)
self.assertSequenceEqual(page, [self.items[0], self.items[1]])
self.assertTrue(page.has_next)
self.assertFalse(page.has_previous)
bradleyoesch marked this conversation as resolved.
Show resolved Hide resolved

def test_second_page(self):
previous_page = self.paginator.page(first=2)
cursor = self.paginator.cursor(previous_page[-1])
Expand Down
Loading