""" Pagination utilities for SneakyScanner web application. Provides helper functions for paginating SQLAlchemy queries. """ from typing import Any, Dict, List from sqlalchemy.orm import Query class PaginatedResult: """Container for paginated query results.""" def __init__(self, items: List[Any], total: int, page: int, per_page: int): """ Initialize paginated result. Args: items: List of items for current page total: Total number of items across all pages page: Current page number (1-indexed) per_page: Number of items per page """ self.items = items self.total = total self.page = page self.per_page = per_page @property def pages(self) -> int: """Calculate total number of pages.""" if self.per_page == 0: return 0 return (self.total + self.per_page - 1) // self.per_page @property def has_prev(self) -> bool: """Check if there is a previous page.""" return self.page > 1 @property def has_next(self) -> bool: """Check if there is a next page.""" return self.page < self.pages @property def prev_page(self) -> int: """Get previous page number.""" return self.page - 1 if self.has_prev else None @property def next_page(self) -> int: """Get next page number.""" return self.page + 1 if self.has_next else None def to_dict(self) -> Dict[str, Any]: """ Convert to dictionary for API responses. Returns: Dictionary with pagination metadata and items """ return { 'items': self.items, 'total': self.total, 'page': self.page, 'per_page': self.per_page, 'pages': self.pages, 'has_prev': self.has_prev, 'has_next': self.has_next, 'prev_page': self.prev_page, 'next_page': self.next_page, } def paginate(query: Query, page: int = 1, per_page: int = 20, max_per_page: int = 100) -> PaginatedResult: """ Paginate a SQLAlchemy query. Args: query: SQLAlchemy query to paginate page: Page number (1-indexed, default: 1) per_page: Items per page (default: 20) max_per_page: Maximum items per page (default: 100) Returns: PaginatedResult with items and pagination metadata Examples: >>> from web.models import Scan >>> query = db.query(Scan).order_by(Scan.timestamp.desc()) >>> result = paginate(query, page=1, per_page=20) >>> scans = result.items >>> total_pages = result.pages """ # Validate and sanitize parameters page = max(1, page) # Page must be at least 1 per_page = max(1, min(per_page, max_per_page)) # Clamp per_page # Get total count total = query.count() # Calculate offset offset = (page - 1) * per_page # Execute query with limit and offset items = query.limit(per_page).offset(offset).all() return PaginatedResult( items=items, total=total, page=page, per_page=per_page ) def validate_page_params(page: Any, per_page: Any, max_per_page: int = 100) -> tuple[int, int]: """ Validate and sanitize pagination parameters. Args: page: Page number (any type, will be converted to int) per_page: Items per page (any type, will be converted to int) max_per_page: Maximum items per page (default: 100) Returns: Tuple of (validated_page, validated_per_page) Examples: >>> validate_page_params('2', '50') (2, 50) >>> validate_page_params(-1, 200) (1, 100) >>> validate_page_params(None, None) (1, 20) """ # Default values default_page = 1 default_per_page = 20 # Convert to int, use default if invalid try: page = int(page) if page is not None else default_page except (ValueError, TypeError): page = default_page try: per_page = int(per_page) if per_page is not None else default_per_page except (ValueError, TypeError): per_page = default_per_page # Validate ranges page = max(1, page) per_page = max(1, min(per_page, max_per_page)) return page, per_page