"""Pagination helpers for list endpoints (Hardening 3.3).
Standard usage:
from app.utils.pagination import paginate_query
pagination, meta = paginate_query(query)
items = pagination.items
return jsonify({'things': [...], **meta})
Meta dict shape:
{
'page': 1,
'per_page': 50,
'total': 1234,
'total_pages': 25,
'has_next': True,
'has_prev': False,
}
"""
from flask import request
DEFAULT_PER_PAGE = 50
MAX_PER_PAGE = 200
def get_page_args():
"""Read and clamp ?page= and ?per_page= from the query string."""
try:
page = int(request.args.get('page', 1))
except (TypeError, ValueError):
page = 1
try:
per_page = int(request.args.get('per_page', DEFAULT_PER_PAGE))
except (TypeError, ValueError):
per_page = DEFAULT_PER_PAGE
page = max(page, 1)
per_page = min(max(per_page, 1), MAX_PER_PAGE)
return page, per_page
def paginate_query(query, page=None, per_page=None):
"""Paginate a Flask-SQLAlchemy query.
Returns (pagination, meta) where pagination.items holds the rows
and meta is a JSON-serializable dict with pagination info.
"""
if page is None or per_page is None:
_page, _per_page = get_page_args()
page = page if page is not None else _page
per_page = per_page if per_page is not None else _per_page
pagination = query.paginate(page=page, per_page=per_page, error_out=False)
meta = {
'page': pagination.page,
'per_page': pagination.per_page,
'total': pagination.total,
'total_pages': pagination.pages,
'has_next': pagination.has_next,
'has_prev': pagination.has_prev,
}
return pagination, meta