from aiohttp.web import Request from abc import ABC from inspect import signature from typing import Any, Callable, Iterable, Union from urllib.parse import urlencode def _slice_data(offset: int, limit: int, count: int, data: Iterable) -> list: if count == 0 or offset > count: return [] return data[offset: offset + limit] # TODO: Create inherited base CachedPagination with cache support class Pagination(ABC): api_real_prefix: str api_prefix: str = "/api" def _get_url_path(self, request: Request): return request.url.path.replace(self.api_prefix, self.api_real_prefix) def _replace_query_param(self, request: Request, **kwargs): """Create url with offset and limit query params""" url = self._get_url_path(request) params = [(key, value) for key, value in request.url.query.items() if key not in ("limit", "offset")] params = params + [(k, v) for k, v in kwargs.items()] return f"{url}?{urlencode(params)}" if params else url def _get_previous_link(self, request: Request, offset: int, limit: int) -> str: """Create previous page URL.""" return ( self._replace_query_param(request) if offset - limit < 0 else self._replace_query_param(request, limit=limit, offset=offset - limit) ) def _get_next_link(self, request: Request, offset: int, limit: int) -> str: """Create next page URL.""" return self._replace_query_param(request, limit=limit, offset=offset + limit) def get_links(self, request: Request, count: int, offset: int, limit: int): return { "previous": self._get_previous_link(request, offset, limit) if offset > 0 else None, "next": self._get_next_link(request, offset, limit) if offset + limit < count else None, } def __call__( self, items_key: str = "items", count_key: str = "count", request_index: int = 0, params_key: str = "querystring", extend_original_dict: bool = False, response_kwargs: bool = False, ) -> Callable: """Paginate function results. Args: For query get all, for iterable it is not a one page but all of them at once. items_key: Response items node field name. request_index: Request obj index in args. params_key: Params dict key in request. extend_original_dict: Response items within dict already, need to extend it with pagination fields. response_kwargs: Result contains extra data that should be a part of final response. Returns: Callable: Decorated function. """ def decorator(f: Callable[..., Union[list, Any]]) -> Callable: async def wrapped(*args, **kwargs) -> Any: request = args[request_index] params = request[params_key] limit, offset = params.get("limit"), params.get("offset") or 0 response = await f(*args, **kwargs) response_extra = None if response_kwargs: response, response_extra = response if extend_original_dict: items, count = response[items_key], response[count_key] result = response else: items, count = response result = {items_key: response} if limit is None: offset = 0 limit = count result.update( { count_key: count, items_key: items, **(self.get_links(request, count, offset, limit)), **(response_extra if response_kwargs else {}), } ) return result wrapped.__signature__ = signature(f) return wrapped return decorator