Source code for flask_rest_api.pagination

"""Pagination feature

Two pagination modes are supported:

- Pagination inside the resource: the resource function is responsible for
  selecting requested range of items and setting total number of items.

- Post-pagination: the resource returns an iterator (typically a DB cursor) and
  a pager is provided to paginate the data and get the total number of items.
"""

from collections import OrderedDict
from functools import wraps
import json

from flask import request, current_app

import marshmallow as ma
from webargs.flaskparser import FlaskParser

from .utils import unpack_tuple_response
from .compat import MARSHMALLOW_VERSION_MAJOR


parser = FlaskParser()


[docs]class PaginationParameters: """Holds pagination arguments :param int page: Page number :param int page_size: Page size """ def __init__(self, page, page_size): self.page = page self.page_size = page_size self.item_count = None @property def first_item(self): """Return first item number""" return (self.page - 1) * self.page_size @property def last_item(self): """Return last item number""" return self.first_item + self.page_size - 1 def __repr__(self): return ("{}(page={!r},page_size={!r})" .format(self.__class__.__name__, self.page, self.page_size))
def _pagination_parameters_schema_factory( def_page, def_page_size, def_max_page_size): """Generate a PaginationParametersSchema""" class PaginationParametersSchema(ma.Schema): """Deserializes pagination params into PaginationParameters""" class Meta: ordered = True if MARSHMALLOW_VERSION_MAJOR < 3: strict = True page = ma.fields.Integer( missing=def_page, validate=ma.validate.Range(min=1) ) page_size = ma.fields.Integer( missing=def_page_size, validate=ma.validate.Range(min=1, max=def_max_page_size) ) @ma.post_load def make_paginator(self, data, **kwargs): return PaginationParameters(**data) return PaginationParametersSchema
[docs]class Page: """Pager for simple types such as lists. Can be subclassed to provide a pager for a specific data object. """ def __init__(self, collection, page_params): """Create a Page instance :param sequence collection: Collection of items to page through :page PaginationParameters page_params: Pagination parameters """ self.collection = collection self.page_params = page_params self.page_params.item_count = self.item_count @property def items(self): return list(self.collection[ self.page_params.first_item: self.page_params.last_item + 1]) @property def item_count(self): return len(self.collection) def __repr__(self): return ("{}(collection={!r},page_params={!r})" .format(self.__class__.__name__, self.collection, self.page_params))
class PaginationMixin: """Extend Blueprint to add Pagination feature""" # Name of field to use for pagination metadata response header # Can be overridden. If None, no pagination header is returned. PAGINATION_HEADER_FIELD_NAME = 'X-Pagination' # Global default pagination parameters # Can be overridden to provide custom defaults DEFAULT_PAGINATION_PARAMETERS = { 'page': 1, 'page_size': 10, 'max_page_size': 100} def paginate(self, pager=None, *, page=None, page_size=None, max_page_size=None): """Decorator adding pagination to the endpoint :param Page pager: Page class used to paginate response data :param int page: Default requested page number (default: 1) :param int page_size: Default requested page size (default: 10) :param int max_page_size: Maximum page size (default: 100) If a :class:`Page <Page>` class is provided, it is used to paginate the data returned by the view function, typically a lazy database cursor. Otherwise, pagination is handled in the view function. The decorated function may return a tuple including status and/or headers, like a typical flask view function. It may not return a ``Response`` object. See :doc:`Pagination <pagination>`. """ if page is None: page = self.DEFAULT_PAGINATION_PARAMETERS['page'] if page_size is None: page_size = self.DEFAULT_PAGINATION_PARAMETERS['page_size'] if max_page_size is None: max_page_size = self.DEFAULT_PAGINATION_PARAMETERS['max_page_size'] page_params_schema = _pagination_parameters_schema_factory( page, page_size, max_page_size) parameters = { 'in': 'query', 'schema': page_params_schema, } def decorator(func): # Add pagination params to doc info in function object func._apidoc = getattr(func, '_apidoc', {}) func._apidoc.setdefault('parameters', []).append(parameters) @wraps(func) def wrapper(*args, **kwargs): page_params = parser.parse(page_params_schema, request) # Pagination in resource code: inject page_params as kwargs if pager is None: kwargs['pagination_parameters'] = page_params # Execute decorated function result, status, headers = unpack_tuple_response( func(*args, **kwargs)) # Post pagination: use pager class to paginate the result if pager is not None: result = pager(result, page_params=page_params).items # Add pagination metadata to headers if self.PAGINATION_HEADER_FIELD_NAME is not None: if page_params.item_count is None: current_app.logger.warning( 'item_count not set in endpoint {}' .format(request.endpoint)) else: page_header = self._make_pagination_header( page_params.page, page_params.page_size, page_params.item_count) if headers is None: headers = {} headers[ self.PAGINATION_HEADER_FIELD_NAME] = page_header return result, status, headers return wrapper return decorator @staticmethod def _make_pagination_header(page, page_size, item_count): """Build pagination header from page, page size and item count This method returns a json representation of a default pagination metadata structure. It can be overridden to use another structure. """ page_header = OrderedDict() page_header['total'] = item_count if item_count == 0: page_header['total_pages'] = 0 else: # First / last page, page count page_count = ((item_count - 1) // page_size) + 1 first_page = 1 last_page = page_count page_header['total_pages'] = page_count page_header['first_page'] = first_page page_header['last_page'] = last_page # Page, previous / next page if page <= last_page: page_header['page'] = page if page > first_page: page_header['previous_page'] = page - 1 if page < last_page: page_header['next_page'] = page + 1 return json.dumps(page_header)