mirror of
https://github.com/hanzoai/python-sdk.git
synced 2026-10-27 04:20:09 +00:00
224 lines
5.9 KiB
Python
224 lines
5.9 KiB
Python
"""Pagination utilities for MCP tools.
|
|
|
|
This module provides utilities for implementing cursor-based pagination
|
|
according to the MCP pagination protocol.
|
|
"""
|
|
|
|
import base64
|
|
import json
|
|
from dataclasses import dataclass
|
|
from typing import Any, Dict, Generic, List, Optional, TypeVar
|
|
|
|
T = TypeVar("T")
|
|
|
|
|
|
@dataclass
|
|
class PaginationParams:
|
|
"""Parameters for pagination."""
|
|
|
|
cursor: Optional[str] = None
|
|
page_size: int = 100 # Default page size
|
|
|
|
|
|
@dataclass
|
|
class PaginatedResponse(Generic[T]):
|
|
"""A paginated response containing items and optional next cursor."""
|
|
|
|
items: List[T]
|
|
next_cursor: Optional[str] = None
|
|
|
|
def to_dict(self, items_key: str = "items") -> Dict[str, Any]:
|
|
"""Convert to dictionary format for MCP response.
|
|
|
|
Args:
|
|
items_key: The key to use for items in the response
|
|
|
|
Returns:
|
|
Dictionary with items and optional nextCursor
|
|
"""
|
|
result = {items_key: self.items}
|
|
if self.next_cursor:
|
|
result["nextCursor"] = self.next_cursor
|
|
return result
|
|
|
|
|
|
class CursorManager:
|
|
"""Manages cursor creation and parsing for pagination."""
|
|
|
|
@staticmethod
|
|
def create_cursor(data: Dict[str, Any]) -> str:
|
|
"""Create an opaque cursor from data.
|
|
|
|
Args:
|
|
data: Data to encode in the cursor
|
|
|
|
Returns:
|
|
Base64-encoded cursor string
|
|
"""
|
|
json_data = json.dumps(data, separators=(",", ":"))
|
|
return base64.b64encode(json_data.encode()).decode()
|
|
|
|
@staticmethod
|
|
def parse_cursor(cursor: str) -> Optional[Dict[str, Any]]:
|
|
"""Parse a cursor string back to data.
|
|
|
|
Args:
|
|
cursor: Base64-encoded cursor string
|
|
|
|
Returns:
|
|
Decoded data or None if invalid
|
|
"""
|
|
try:
|
|
decoded = base64.b64decode(cursor.encode()).decode()
|
|
return json.loads(decoded)
|
|
except (ValueError, json.JSONDecodeError):
|
|
return None
|
|
|
|
@staticmethod
|
|
def create_offset_cursor(offset: int) -> str:
|
|
"""Create a cursor for offset-based pagination.
|
|
|
|
Args:
|
|
offset: The offset for the next page
|
|
|
|
Returns:
|
|
Cursor string
|
|
"""
|
|
return CursorManager.create_cursor({"offset": offset})
|
|
|
|
@staticmethod
|
|
def parse_offset_cursor(cursor: Optional[str]) -> int:
|
|
"""Parse an offset cursor.
|
|
|
|
Args:
|
|
cursor: Cursor string or None
|
|
|
|
Returns:
|
|
Offset value (0 if cursor is None or invalid)
|
|
"""
|
|
if not cursor:
|
|
return 0
|
|
|
|
data = CursorManager.parse_cursor(cursor)
|
|
if not data or "offset" not in data:
|
|
return 0
|
|
|
|
return int(data["offset"])
|
|
|
|
|
|
class Paginator(Generic[T]):
|
|
"""Generic paginator for any list of items."""
|
|
|
|
def __init__(self, items: List[T], page_size: int = 100):
|
|
"""Initialize paginator.
|
|
|
|
Args:
|
|
items: List of items to paginate
|
|
page_size: Number of items per page
|
|
"""
|
|
self.items = items
|
|
self.page_size = page_size
|
|
|
|
def get_page(self, cursor: Optional[str] = None) -> PaginatedResponse[T]:
|
|
"""Get a page of results.
|
|
|
|
Args:
|
|
cursor: Optional cursor for the page
|
|
|
|
Returns:
|
|
Paginated response with items and next cursor
|
|
"""
|
|
offset = CursorManager.parse_offset_cursor(cursor)
|
|
|
|
# Get the page of items
|
|
start = offset
|
|
end = min(start + self.page_size, len(self.items))
|
|
page_items = self.items[start:end]
|
|
|
|
# Create next cursor if there are more items
|
|
next_cursor = None
|
|
if end < len(self.items):
|
|
next_cursor = CursorManager.create_offset_cursor(end)
|
|
|
|
return PaginatedResponse(items=page_items, next_cursor=next_cursor)
|
|
|
|
|
|
class StreamPaginator(Generic[T]):
|
|
"""Paginator for streaming/generator-based results."""
|
|
|
|
def __init__(self, page_size: int = 100):
|
|
"""Initialize stream paginator.
|
|
|
|
Args:
|
|
page_size: Number of items per page
|
|
"""
|
|
self.page_size = page_size
|
|
|
|
def paginate_stream(
|
|
self, stream_generator, cursor: Optional[str] = None
|
|
) -> PaginatedResponse[T]:
|
|
"""Paginate results from a stream/generator.
|
|
|
|
Args:
|
|
stream_generator: Generator function that yields items
|
|
cursor: Optional cursor for resuming
|
|
|
|
Returns:
|
|
Paginated response
|
|
"""
|
|
items = []
|
|
skip_count = 0
|
|
|
|
# Parse cursor to get skip count
|
|
if cursor:
|
|
cursor_data = CursorManager.parse_cursor(cursor)
|
|
if cursor_data and "skip" in cursor_data:
|
|
skip_count = cursor_data["skip"]
|
|
|
|
# Skip items based on cursor
|
|
item_count = 0
|
|
for item in stream_generator():
|
|
if item_count < skip_count:
|
|
item_count += 1
|
|
continue
|
|
|
|
items.append(item)
|
|
if len(items) >= self.page_size:
|
|
# We have a full page, create cursor for next page
|
|
next_cursor = CursorManager.create_cursor(
|
|
{"skip": skip_count + len(items)}
|
|
)
|
|
return PaginatedResponse(items=items, next_cursor=next_cursor)
|
|
|
|
# No more items
|
|
return PaginatedResponse(items=items, next_cursor=None)
|
|
|
|
|
|
def paginate_list(
|
|
items: List[T], cursor: Optional[str] = None, page_size: int = 100
|
|
) -> PaginatedResponse[T]:
|
|
"""Convenience function to paginate a list.
|
|
|
|
Args:
|
|
items: List to paginate
|
|
cursor: Optional cursor
|
|
page_size: Items per page
|
|
|
|
Returns:
|
|
Paginated response
|
|
"""
|
|
paginator = Paginator(items, page_size)
|
|
return paginator.get_page(cursor)
|
|
|
|
|
|
def validate_cursor(cursor: str) -> bool:
|
|
"""Validate that a cursor is properly formatted.
|
|
|
|
Args:
|
|
cursor: Cursor string to validate
|
|
|
|
Returns:
|
|
True if valid, False otherwise
|
|
"""
|
|
return CursorManager.parse_cursor(cursor) is not None
|