Skip to main content
Glama
pagination_service.py7.11 kB
"""Pagination service for cursor-based navigation. Provides cursor encoding, decoding, and paginated response building. Integrates cursor_codec and cursor_storage for complete pagination workflow. """ from typing import Any, TypeVar from src.models.pagination import PageMetadata, PaginatedResponse, PaginationParams from src.services.cursor_storage import get_cursor_storage from src.utils.cursor_codec import decode_cursor, encode_cursor T = TypeVar("T") class PaginationService: """Service for cursor-based pagination. Handles cursor creation, validation, and paginated response building. Attributes: secret: HMAC secret for cursor signing default_page_size: Default items per page max_page_size: Maximum allowed items per page """ def __init__( self, secret: str, default_page_size: int = 50, max_page_size: int = 200, ) -> None: """Initialize pagination service. Args: secret: HMAC secret for cursor signing default_page_size: Default page size (default: 50) max_page_size: Maximum page size (default: 200) """ self.secret = secret self.default_page_size = default_page_size self.max_page_size = max_page_size self.cursor_storage = get_cursor_storage() def validate_page_size(self, requested: int | None) -> int: """Validate and normalize page size. Args: requested: Requested page size (None uses default) Returns: Validated page size (clamped to max_page_size) """ if requested is None: return self.default_page_size if requested < 1: return self.default_page_size return min(requested, self.max_page_size) def create_cursor( self, offset: int, order_by: str | None = None, filters: dict[str, Any] | None = None, ) -> str: """Create pagination cursor. Args: offset: Current position in result set order_by: Sort order (e.g., "created_desc") filters: Query filters Returns: Encoded cursor string """ return encode_cursor( offset=offset, secret=self.secret, order_by=order_by, filters=filters, ) def parse_cursor(self, cursor: str) -> dict[str, Any]: """Parse and validate cursor. Args: cursor: Encoded cursor string Returns: Decoded cursor data Raises: CursorCodecError: If cursor is invalid or expired """ return decode_cursor(cursor, secret=self.secret) def build_response( self, items: list[T], total_count: int, params: PaginationParams, order_by: str | None = None, filters: dict[str, Any] | None = None, ) -> PaginatedResponse[T]: """Build paginated response with metadata and next cursor. Args: items: Current page of items total_count: Total items across all pages params: Pagination parameters from request order_by: Sort order for cursor filters: Query filters for cursor Returns: PaginatedResponse with items, metadata, and optional next cursor """ # Determine current offset if params.cursor: cursor_data = self.parse_cursor(params.cursor) current_offset = cursor_data["offset"] else: current_offset = 0 # Calculate next offset next_offset = current_offset + len(items) has_more = next_offset < total_count # Create next cursor if more pages available next_cursor = None if has_more: next_cursor = self.create_cursor( offset=next_offset, order_by=order_by, filters=filters, ) # Build metadata meta = PageMetadata( totalCount=total_count, pageSize=len(items), hasMore=has_more, ) return PaginatedResponse[T]( items=items, nextCursor=next_cursor, meta=meta, ) async def paginate_query( self, query_func: Any, # Callable that accepts offset and limit params: PaginationParams, total_count: int, order_by: str | None = None, filters: dict[str, Any] | None = None, ) -> PaginatedResponse[T]: """Execute paginated query and build response. Helper method that executes query function with pagination parameters and builds the response. Args: query_func: Async function that accepts (offset, limit) and returns items params: Pagination parameters total_count: Total items available order_by: Sort order filters: Query filters Returns: PaginatedResponse with results Example: >>> async def fetch_listings(offset: int, limit: int) -> list[Listing]: ... # Fetch from database ... return await db.query("SELECT * FROM listings LIMIT ? OFFSET ?", limit, offset) >>> params = PaginationParams(cursor=None, limit=50) >>> response = await service.paginate_query( ... query_func=fetch_listings, ... params=params, ... total_count=500 ... ) """ page_size = self.validate_page_size(params.limit) # Determine offset from cursor if params.cursor: cursor_data = self.parse_cursor(params.cursor) offset = cursor_data["offset"] else: offset = 0 # Execute query items = await query_func(offset=offset, limit=page_size) # Build response return self.build_response( items=items, total_count=total_count, params=params, order_by=order_by, filters=filters, ) # Global singleton instance _pagination_service: PaginationService | None = None def get_pagination_service( secret: str | None = None, default_page_size: int = 50, max_page_size: int = 200, ) -> PaginationService: """Get global pagination service instance. Args: secret: HMAC secret (required for first call) default_page_size: Default page size max_page_size: Maximum page size Returns: Singleton PaginationService instance Raises: ValueError: If secret not provided on first call """ global _pagination_service if _pagination_service is None: if secret is None: raise ValueError("secret required for first initialization") _pagination_service = PaginationService( secret=secret, default_page_size=default_page_size, max_page_size=max_page_size, ) return _pagination_service

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/darrentmorgan/hostaway-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server