""" Pagination utilities for SneakyScanner web application. Provides helper functions for paginating SQLAlchemy queries. """ from typing import Any, Callable, Dict, List, Optional 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 @property def prev_num(self) -> int: """Alias for prev_page (Flask-SQLAlchemy compatibility).""" return self.prev_page @property def next_num(self) -> int: """Alias for next_num (Flask-SQLAlchemy compatibility).""" return self.next_page def iter_pages(self, left_edge=1, left_current=1, right_current=2, right_edge=1): """ Generate page numbers for pagination display. Yields page numbers and None for gaps, compatible with Flask-SQLAlchemy's pagination.iter_pages() method. Args: left_edge: Number of pages to show on the left edge left_current: Number of pages to show left of current page right_current: Number of pages to show right of current page right_edge: Number of pages to show on the right edge Yields: int or None: Page number or None for gaps Example: For 100 pages, current page 50: Yields: 1, None, 48, 49, 50, 51, 52, None, 100 """ last = 0 for num in range(1, self.pages + 1): if num <= left_edge or \ (num > self.page - left_current - 1 and num < self.page + right_current) or \ num > self.pages - right_edge: if last + 1 != num: yield None yield num last = num 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, transform: Optional[Callable[[Any], Dict[str, Any]]] = None, 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) transform: Optional function to transform each item (default: None) 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 >>> # With transform function >>> def scan_to_dict(scan): ... return {'id': scan.id, 'name': scan.name} >>> result = paginate(query, page=1, per_page=20, transform=scan_to_dict) """ # 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() # Apply transform if provided if transform is not None: items = [transform(item) for item in items] 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