Skip to content

Commit

Permalink
Merge pull request #5 from Overseas-Student-Living/query-pagination
Browse files Browse the repository at this point in the history
Add apply_pagination function to allow query pagination
  • Loading branch information
juliotrigo authored Jan 5, 2017
2 parents f204f10 + 0d1803d commit 9d8fa39
Show file tree
Hide file tree
Showing 6 changed files with 536 additions and 13 deletions.
23 changes: 21 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ SQLAlchemy-filters
Filter, sort and paginate SQLAlchemy query objects.
Ideal for exposing these actions over a REST API.

Usage
-----
Filtering
---------

Assuming that we have a SQLAlchemy `query` that only contains a single
model:
Expand Down Expand Up @@ -51,6 +51,25 @@ Then we can apply filters to that ``query`` object (multiple times):
result = filtered_query.all()
Pagination
----------

.. code-block:: python
from sqlalchemy_filters import apply_pagination
# `query` should be a SQLAlchemy query object
query, pagination = apply_pagination(query, page_number=1, page_size=10)
page_size, page_number, num_pages, total_results = pagination
assert 10 == len(query)
assert 10 == page_size == pagination.page_size
assert 1 == page_number == pagination.page_number
assert 3 == num_pages == pagination.num_pages
assert 22 == total_results == pagination.total_results
Filters format
--------------

Expand Down
4 changes: 4 additions & 0 deletions sqlalchemy_filters/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@ class BadFilterFormat(Exception):

class BadQuery(Exception):
pass


class InvalidPage(Exception):
pass
22 changes: 22 additions & 0 deletions sqlalchemy_filters/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,20 @@ def format_for_sqlalchemy(self):


def apply_filters(query, filters):
"""Apply filters to a SQLAlchemy query.
:param query:
A :class:`sqlalchemy.orm.Query` instance.
:param filters:
A list of dictionaries, where each one of them includes
the necesary information to create a filter to be applied to the
query.
:returns:
The :class:`sqlalchemy.orm.Query` instance after all the filters
have been applied.
"""
models = get_query_models(query)
if not models:
raise BadQuery('The query does not contain any models.')
Expand All @@ -112,6 +126,14 @@ def apply_filters(query, filters):


def get_query_models(query):
"""Get models from query.
:param query:
A :class:`sqlalchemy.orm.Query` instance.
:returns:
A dictionary with all the models included in the query.
"""
return {
entity['type'].__name__: entity['type']
for entity in query.column_descriptions
Expand Down
92 changes: 89 additions & 3 deletions sqlalchemy_filters/pagination.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,92 @@
# -*- coding: utf-8 -*-
import math
from collections import namedtuple

from sqlalchemy_filters.exceptions import InvalidPage

def apply_pagination(query, offset, limit):
# TODO
raise NotImplemented()

def apply_pagination(query, page_number=None, page_size=None):
"""Apply pagination to a SQLAlchemy query object.
:param page_number:
Page to be returned (starts and defaults to 1).
:param page_size:
Maximum number of results to be returned in the page (defaults
to the total results).
:returns:
A 2-tuple with the paginated SQLAlchemy query object and
a pagination namedtuple.
The pagination object contains information about the results
and pages: ``page_size`` (defaults to ``total_results``),
``page_number`` (defaults to 1), ``num_pages`` and
``total_results``.
Basic usage::
query, pagination = apply_pagination(query, 1, 10)
>>> len(query)
10
>>> pagination.page_size
10
>>> pagination.page_number
1
>>> pagination.num_pages
3
>>> pagination.total_results
22
>>> page_size, page_number, num_pages, total_results = pagination
"""
total_results = query.count()
query = _limit(query, page_size)

# Page size defaults to total results
if page_size is None or (page_size > total_results and total_results > 0):
page_size = total_results

query = _offset(query, page_number, page_size)

# Page number defaults to 1
if page_number is None:
page_number = 1

num_pages = _calculate_num_pages(page_number, page_size, total_results)

Pagination = namedtuple(
'Pagination',
['page_number', 'page_size', 'num_pages', 'total_results']
)
return query, Pagination(page_number, page_size, num_pages, total_results)


def _limit(query, page_size):
if page_size is not None:
if page_size < 0:
raise InvalidPage(
'Page size should not be negative: {}'.format(page_size)
)

query = query.limit(page_size)

return query


def _offset(query, page_number, page_size):
if page_number is not None:
if page_number < 1:
raise InvalidPage(
'Page number should be positive: {}'.format(page_number)
)

query = query.offset((page_number - 1) * page_size)

return query


def _calculate_num_pages(page_number, page_size, total_results):
if page_size == 0:
return 0

return math.ceil(total_results / page_size)
10 changes: 2 additions & 8 deletions test/interface/test_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,10 +166,7 @@ def multiple_bars_inserted(self, session):
bar_2 = Bar(id=2, name='name_2', count=10)
bar_3 = Bar(id=3, name='name_1', count=None)
bar_4 = Bar(id=4, name='name_4', count=15)
session.add(bar_1)
session.add(bar_2)
session.add(bar_3)
session.add(bar_4)
session.add_all([bar_1, bar_2, bar_3, bar_4])
session.commit()


Expand Down Expand Up @@ -549,10 +546,7 @@ def multiple_quxs_inserted(self, session):
created_at=datetime.date(2016, 7, 14),
execution_time=datetime.datetime(2016, 7, 14, 3, 5, 9)
)
session.add(qux_1)
session.add(qux_2)
session.add(qux_3)
session.add(qux_4)
session.add_all([qux_1, qux_2, qux_3, qux_4])
session.commit()


Expand Down
Loading

0 comments on commit 9d8fa39

Please sign in to comment.