Skip to content

Commit

Permalink
Merge pull request #49 from bradleyoesch/feat/async-support
Browse files Browse the repository at this point in the history
Support async queryset evaluation
  • Loading branch information
patrick91 authored Jul 1, 2024
2 parents 50e88f2 + aa9c7d8 commit 8ea96d4
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 16 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ jobs:

strategy:
matrix:
python-version: [3.8, 3.9, "3.10", "3.11"]
django-version: [4.0, 3.2]
python-version: ["3.10", "3.11"]
django-version: [5.0, 4.2]

services:
postgres:
Expand Down
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
52 changes: 38 additions & 14 deletions cursor_pagination.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,17 +68,13 @@ 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
page_size = first or last
if page_size is None:
return CursorPage(qs, self)

def _apply_paginator_arguments(self, qs, first=None, last=None, after=None, before=None):
"""
Apply first/after, last/before filtering to the queryset
"""
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 +85,12 @@ 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)
return qs

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 +100,33 @@ 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
qs = self._apply_paginator_arguments(qs, first, last, after, before)

qs = list(qs)
page_size = first if first is not None else last
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
qs = self._apply_paginator_arguments(qs, first, last, after, before)

page_size = first if first is not None else last
items = []
async for item in qs[:page_size].aiterator():
items.append(item)
if last is not None:
items.reverse()
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
100 changes: 100 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 @@ -39,12 +55,30 @@ def setUpTestData(cls):
cls.items.append(post)
cls.paginator = CursorPaginator(Post.objects.all(), ('-created',))

def test_first_page_zero(self):
page = self.paginator.page(first=0)
self.assertSequenceEqual(page, [])
self.assertTrue(page.has_next)
self.assertFalse(page.has_previous)

async def test_async_first_page_zero(self):
page = await self.paginator.apage(first=0)
self.assertSequenceEqual(page, [])
self.assertTrue(page.has_next)
self.assertFalse(page.has_previous)

def test_first_page(self):
page = self.paginator.page(first=2)
self.assertSequenceEqual(page, [self.items[0], self.items[1]])
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)

def test_second_page(self):
previous_page = self.paginator.page(first=2)
cursor = self.paginator.cursor(previous_page[-1])
Expand All @@ -53,6 +87,14 @@ def test_second_page(self):
self.assertTrue(page.has_next)
self.assertTrue(page.has_previous)

async def test_async_second_page(self):
previous_page = await self.paginator.apage(first=2)
cursor = self.paginator.cursor(previous_page[-1])
page = await self.paginator.apage(first=2, after=cursor)
self.assertSequenceEqual(page, [self.items[2], self.items[3]])
self.assertTrue(page.has_next)
self.assertTrue(page.has_previous)

def test_last_page(self):
previous_page = self.paginator.page(first=18)
cursor = self.paginator.cursor(previous_page[-1])
Expand All @@ -61,6 +103,14 @@ def test_last_page(self):
self.assertFalse(page.has_next)
self.assertTrue(page.has_previous)

async def test_async_last_page(self):
previous_page = await self.paginator.apage(first=18)
cursor = self.paginator.cursor(previous_page[-1])
page = await self.paginator.apage(first=2, after=cursor)
self.assertSequenceEqual(page, [self.items[18], self.items[19]])
self.assertFalse(page.has_next)
self.assertTrue(page.has_previous)

def test_incomplete_last_page(self):
previous_page = self.paginator.page(first=18)
cursor = self.paginator.cursor(previous_page[-1])
Expand All @@ -69,6 +119,14 @@ def test_incomplete_last_page(self):
self.assertFalse(page.has_next)
self.assertTrue(page.has_previous)

async def test_async_incomplete_last_page(self):
previous_page = await self.paginator.apage(first=18)
cursor = self.paginator.cursor(previous_page[-1])
page = await self.paginator.apage(first=100, after=cursor)
self.assertSequenceEqual(page, [self.items[18], self.items[19]])
self.assertFalse(page.has_next)
self.assertTrue(page.has_previous)


class TestBackwardsPagination(TestCase):

Expand All @@ -81,12 +139,30 @@ def setUpTestData(cls):
cls.items.append(post)
cls.paginator = CursorPaginator(Post.objects.all(), ('-created',))

def test_first_page_zero(self):
page = self.paginator.page(last=0)
self.assertSequenceEqual(page, [])
self.assertTrue(page.has_previous)
self.assertFalse(page.has_next)

async def test_async_first_page_zero(self):
page = await self.paginator.apage(last=0)
self.assertSequenceEqual(page, [])
self.assertTrue(page.has_previous)
self.assertFalse(page.has_next)

def test_first_page(self):
page = self.paginator.page(last=2)
self.assertSequenceEqual(page, [self.items[18], self.items[19]])
self.assertTrue(page.has_previous)
self.assertFalse(page.has_next)

async def test_async_first_page(self):
page = await self.paginator.apage(last=2)
self.assertSequenceEqual(page, [self.items[18], self.items[19]])
self.assertTrue(page.has_previous)
self.assertFalse(page.has_next)

def test_second_page(self):
previous_page = self.paginator.page(last=2)
cursor = self.paginator.cursor(previous_page[0])
Expand All @@ -95,6 +171,14 @@ def test_second_page(self):
self.assertTrue(page.has_previous)
self.assertTrue(page.has_next)

async def test_async_second_page(self):
previous_page = await self.paginator.apage(last=2)
cursor = self.paginator.cursor(previous_page[0])
page = await self.paginator.apage(last=2, before=cursor)
self.assertSequenceEqual(page, [self.items[16], self.items[17]])
self.assertTrue(page.has_previous)
self.assertTrue(page.has_next)

def test_last_page(self):
previous_page = self.paginator.page(last=18)
cursor = self.paginator.cursor(previous_page[0])
Expand All @@ -103,6 +187,14 @@ def test_last_page(self):
self.assertFalse(page.has_previous)
self.assertTrue(page.has_next)

async def test_async_last_page(self):
previous_page = await self.paginator.apage(last=18)
cursor = self.paginator.cursor(previous_page[0])
page = await self.paginator.apage(last=2, before=cursor)
self.assertSequenceEqual(page, [self.items[0], self.items[1]])
self.assertFalse(page.has_previous)
self.assertTrue(page.has_next)

def test_incomplete_last_page(self):
previous_page = self.paginator.page(last=18)
cursor = self.paginator.cursor(previous_page[0])
Expand All @@ -111,6 +203,14 @@ def test_incomplete_last_page(self):
self.assertFalse(page.has_previous)
self.assertTrue(page.has_next)

async def test_async_incomplete_last_page(self):
previous_page = await self.paginator.apage(last=18)
cursor = self.paginator.cursor(previous_page[0])
page = await self.paginator.apage(last=100, before=cursor)
self.assertSequenceEqual(page, [self.items[0], self.items[1]])
self.assertFalse(page.has_previous)
self.assertTrue(page.has_next)


class TestTwoFieldPagination(TestCase):

Expand Down

0 comments on commit 8ea96d4

Please sign in to comment.