← все задачи

Async Python · задача 9 из 10

Асинхронный итератор по страницам API

Средний 15–20 минут __aiter__генераторыленивость

Условие

Чужой API отдаёт данные страницами с курсором. Напишите асинхронный генератор, который отдаёт записи по одной, сам подгружая следующие страницы, пока они есть.

Что требуется

  • Снаружи используется как async for item in fetch_items(...)
  • Страницы подгружаются лениво, по мере необходимости
  • Можно остановиться в любой момент, не выкачав всё

Пример

async for user in fetch_items(session, "/api/users"):
    if user["id"] == target:
        break          # лишние страницы не грузятся

Сначала уточните

Вопросы до кода — половина оценки. Молча начать печатать хуже, чем задать два вопроса.

  • Как устроена пагинация: курсор, offset или ссылка next в ответе?
  • Есть ли ограничение по частоте запросов к API?
  • Нужен ли повтор при обрыве в середине выкачки и можно ли продолжить с курсора?
Показать решение Скрыть решение

Решение

async def fetch_items(session, url, page_size=100):
    cursor = None

    while True:
        params = {"limit": page_size}
        if cursor:
            params["cursor"] = cursor

        async with session.get(url, params=params) as response:
            response.raise_for_status()
            payload = await response.json()

        for item in payload["items"]:
            yield item                    # отдаём по одной записи

        cursor = payload.get("next_cursor")
        if not cursor:
            return


# Тот же смысл классом, если нужно хранить состояние снаружи:
class Pages:
    def __init__(self, session, url):
        self.session = session
        self.url = url

    def __aiter__(self):
        return fetch_items(self.session, self.url)

Почему так

Почему генератор, а не список страниц

  • Вызывающий получает записи по одной и может остановиться на первой подходящей — остальные страницы не запрашиваются
  • Память не зависит от объёма данных: в ней одна страница, а не весь ответ API
  • Такой интерфейс скрывает пагинацию: снаружи просто поток записей

Почему курсор лучше offset

  • offset при вставках и удалениях между запросами пропускает и дублирует записи — данные едут
  • В базе OFFSET 100000 заставляет сервер пролистать сто тысяч строк на каждой странице
  • Курсор фиксирует позицию по отсортированному ключу — стабильно и дёшево

Что происходит при break

  • Прерывание async for закрывает генератор: внутрь на текущем yield прилетает GeneratorExit
  • Если внутри есть try/finally, он отработает — там и закрывают ресурсы
  • Именно поэтому сессию сюда передают снаружи: генератор не должен закрывать то, чем не владеет

Что спросят дальше

  • Спросят про конкурентную выкачку: страницы с курсором принципиально последовательны, распараллелить можно только диапазоны
  • Спросят про дубликаты между страницами — при курсорной пагинации на равных ключах такое бывает
  • Спросят про 429 в середине выкачки: нужен повтор с паузой, при этом курсор сохраняется и всё продолжается с того же места

Следующая задача

Кэш и одновременные промахи — Сто запросов одновременно не нашли значение в кэше — в базу должен уйти один.