---
name: cursor_paginate
kind: function
lang: py
domain: core
version: "1.0.0"
purity: impure
signature: "def cursor_paginate(fetch_page: Callable[..., list[T]], get_cursor: Callable[[T], str | None], page_size: int = 100, max_items: int = 2000, max_retries: int = 3, retry_delay: float = 2.0, retryable_exceptions: tuple[type[Exception], ...] = (ConnectionError, TimeoutError, OSError)) -> list[T]"
description: "Paginador generico basado en cursor que funciona con cualquier API que use cursor-based pagination. Cada pagina se obtiene con retry automatico con exponential backoff. Se detiene cuando la pagina esta vacia, el batch es menor que page_size, se alcanza max_items, o el cursor del ultimo item es None."
tags: [pagination, cursor, retry, generic, api, backoff, pendiente-usar]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: ["time", "typing.Callable", "typing.TypeVar"]
params:
  - name: fetch_page
    desc: "callable que recibe limit y cursor como kwargs, retorna lista de items"
  - name: get_cursor
    desc: "callable que recibe un item y retorna su cursor (o None si es el ultimo)"
  - name: page_size
    desc: "numero de items por pagina a solicitar"
  - name: max_items
    desc: "limite maximo total de items a acumular"
  - name: max_retries
    desc: "numero maximo de reintentos por pagina"
  - name: retry_delay
    desc: "delay inicial en segundos para exponential backoff entre reintentos"
  - name: retryable_exceptions
    desc: "tupla de tipos de excepcion que desencadenan reintento"
output: "lista de items recopilados de todas las paginas"
tested: true
tests:
  - "API que retorna 3 paginas de 10 items"
  - "API que falla 1 vez por pagina (retry funciona)"
  - "max_items limita correctamente"
  - "API que retorna pagina parcial (ultima pagina)"
  - "Cursor None en ultimo item (se detiene)"
test_file_path: "python/functions/core/cursor_paginate_test.py"
file_path: "python/functions/core/cursor_paginate.py"
---

## Ejemplo

```python
from cursor_paginate import cursor_paginate

def fetch_users(limit: int, cursor: str | None) -> list[dict]:
    params = {"limit": limit}
    if cursor:
        params["cursor"] = cursor
    return requests.get("https://api.example.com/users", params=params).json()["items"]

def get_cursor(user: dict) -> str | None:
    return user.get("next_cursor")

users = cursor_paginate(
    fetch_page=fetch_users,
    get_cursor=get_cursor,
    page_size=100,
    max_items=5000,
    max_retries=3,
    retry_delay=2.0,
)
```

## Notas

El caller solo necesita proveer dos callables:
- `fetch_page(limit, cursor)`: recibe `limit` y `cursor` como kwargs, retorna lista de items.
- `get_cursor(item)`: extrae el cursor del ultimo item de la pagina; retornar None indica fin de datos.

El exponential backoff interno aplica `retry_delay * 2^attempt` sin jitter. Solo se reintentan las excepciones en `retryable_exceptions`; cualquier otra excepcion propaga inmediatamente.

Condiciones de parada (cualquiera de ellas):
1. La pagina retornada esta vacia.
2. La pagina retornada tiene menos items que `page_size` (pagina parcial = ultima pagina).
3. El total acumulado alcanza o supera `max_items` (se trunca y se para).
4. `get_cursor(batch[-1])` retorna `None`.

Funcion impura: llama a `fetch_page` que tipicamente hace I/O de red y usa `time.sleep` en los reintentos.
