Source code for clash_royale.aio.pagination
from __future__ import annotations
from collections.abc import AsyncGenerator
from typing import TYPE_CHECKING, Generic, TypeVar
from ..models.base import CRBaseModel
from ..types import ClanSearchParams, PaginationParams
if TYPE_CHECKING:
from .client import Client
ResourceType = TypeVar("ResourceType", bound="CRBaseModel")
[docs]
class PaginatedList(Generic[ResourceType]):
"""Async lazy-loading paginated list that fetches pages on demand.
Supports async iteration and explicit async methods for indexed access.
Example usage::
# Async iteration (lazy loading)
async for clan in client.clans.search("royal"):
print(clan.name)
# Explicit index access
clan = await results.get(5)
# Explicit slice access
clans = await results.slice(0, 10)
# Fetch all results
all_clans = await results.all()
"""
[docs]
def __init__(
self,
client: Client,
endpoint: str,
model: type[ResourceType],
params: PaginationParams | ClanSearchParams | None = None,
):
self._client = client
self._endpoint = endpoint
self._model = model
# Extract client-side pagination control params
_params = params.copy() if params else {}
self._limit: int | None = _params.pop("limit", None)
self._page_size: int | None = _params.pop("page_size", None)
# Remaining params are for the API (after, before, etc.)
self._params: dict[str, str | int] = _params
self._elements: list[ResourceType] = []
self._after_cursor: str | None = None
self._has_more: bool = True
def __repr__(self) -> str:
loaded = len(self._elements)
status = "more available" if self._has_more else "complete"
return f"<{self.__class__.__name__} [{loaded} loaded, {status}]>"
async def __aiter__(self) -> AsyncGenerator[ResourceType, None]:
"""Async iterate over all items, fetching pages as needed."""
for item in self._elements:
yield item
while self._has_more and (
self._limit is None or len(self._elements) < self._limit
):
new_items = await self._grow()
for item in new_items:
yield item
[docs]
async def get(self, index: int) -> ResourceType:
"""Get item at index, fetching pages as needed.
:param index: The index of the item to retrieve.
:returns: The item at the specified index.
:raises IndexError: If index is out of range.
:raises ValueError: If index is negative.
"""
if index < 0:
raise ValueError(
"Negative indexing is not supported because it requires "
"fetching all pages. Use `await results.all()` and index "
"the resulting list instead."
)
await self._fetch_to_index(index)
return self._elements[index]
[docs]
async def slice(self, start: int, stop: int) -> list[ResourceType]:
"""Get a slice of items, fetching pages as needed.
:param start: Start index (inclusive).
:param stop: Stop index (exclusive).
:returns: List of items in the specified range.
"""
await self._fetch_to_index(stop - 1)
return self._elements[start:stop]
[docs]
async def all(self) -> list[ResourceType]:
"""Fetch and return all items.
.. warning:: This method fetches all pages from the API, which may
result in many requests and large memory usage for endpoints
with many results. Consider using ``limit`` or async iteration
for large datasets.
:returns: List of all items.
"""
await self._fetch_all()
return list(self._elements)
[docs]
async def first(self) -> ResourceType | None:
"""Get the first item, or None if empty.
:returns: The first item or None.
"""
try:
return await self.get(0)
except IndexError:
return None
def _could_grow(self) -> bool:
"""Check if more items can be fetched."""
if self._limit is not None and len(self._elements) >= self._limit:
return False
return self._has_more
async def _grow(self) -> list[ResourceType]:
"""Fetch the next page and add items to the list."""
new_elements = await self._fetch_next_page()
# If limit is set, only add elements up to the limit
if self._limit is not None:
remaining = self._limit - len(self._elements)
if remaining <= 0:
return []
new_elements = new_elements[:remaining]
self._elements.extend(new_elements)
return new_elements
async def _fetch_next_page(self) -> list[ResourceType]:
"""Fetch the next page from the API."""
params = self._params.copy()
if self._after_cursor:
params["after"] = self._after_cursor
if self._page_size:
# Send page_size as "limit" to the API
params["limit"] = self._page_size
response = await self._client._request("GET", self._endpoint, params=params)
# Update cursor for next page
paging = response.get("paging", {})
cursors = paging.get("cursors", {})
self._after_cursor = cursors.get("after")
self._has_more = self._after_cursor is not None
# Parse items with model
items = response.get("items", [])
return [self._model.model_validate(item) for item in items]
async def _fetch_to_index(self, index: int) -> None:
"""Fetch pages until the specified index is available."""
while len(self._elements) <= index and self._could_grow():
await self._grow()
async def _fetch_all(self) -> None:
"""Fetch all remaining pages."""
while self._could_grow():
await self._grow()