"""Контракт progress_cb у парсеров: отчёт по страницам и остановка по запросу. Сеть не трогаем — подменяем HTTP-клиент парсера заглушкой. Проверяем то, на что опирается заливка: воркер видит рост выборки и может остановить парсер, не дожидаясь конца (отмена из админки, исчерпанный бюджет времени таска). """ from typing import Any import openalex from cyberleninka import CyberLeninkaParser from openalex import _sleep_alive class FakeResponse: def __init__(self, payload: dict[str, Any]) -> None: self._payload = payload def raise_for_status(self) -> None: pass def json(self) -> dict[str, Any]: return self._payload class FakeClient: """Отдаёт бесконечные полные страницы по 10 статей — как большая выдача.""" def __init__(self) -> None: self.calls = 0 def post(self, url: str, json: dict[str, Any]) -> FakeResponse: self.calls += 1 start = json["from"] return FakeResponse({ "articles": [{"id": start + i, "name": f"статья {start + i}"} for i in range(10)] }) def _parser(monkeypatch) -> CyberLeninkaParser: p = CyberLeninkaParser() p.client = FakeClient() # type: ignore[assignment] monkeypatch.setattr("cyberleninka.RATE_LIMIT_DELAY", 0) return p def test_progress_cb_reports_growing_count(monkeypatch): p = _parser(monkeypatch) seen: list[int] = [] docs = p.fetch(query="x", limit=30, progress_cb=lambda n: seen.append(n) or True) assert len(docs) == 30 assert seen == [10, 20, 30] def test_progress_cb_false_stops_fetch_early(monkeypatch): p = _parser(monkeypatch) # Останавливаем после второй страницы — как отмена прогона из админки docs = p.fetch(query="x", limit=1000, progress_cb=lambda n: n < 20) assert len(docs) == 20 assert p.client.calls == 2 # type: ignore[attr-defined] def test_fetch_works_without_callback(monkeypatch): """Скрипты заливки зовут парсеры без progress_cb — поведение прежнее.""" p = _parser(monkeypatch) assert len(p.fetch(query="x", limit=20)) == 20 def test_backoff_sleep_reports_life_and_can_be_interrupted(monkeypatch): """Минуты ожидания в backoff OpenAlex — не молчание: тик и шанс остановиться.""" slept: list[float] = [] monkeypatch.setattr(openalex.time, "sleep", lambda s: slept.append(s)) monkeypatch.setattr(openalex, "BACKOFF_TICK_S", 5.0) ticks: list[int] = [] assert _sleep_alive(20, lambda n: ticks.append(n) or True, fetched=7) is True assert sum(slept) == 20 and ticks == [7, 7, 7, 7] slept.clear() # Останавливаемся на первом же тике — не досыпая оставшиеся 900с assert _sleep_alive(900, lambda n: False, fetched=7) is False assert sum(slept) == 5 def test_backoff_sleep_without_callback_just_sleeps(monkeypatch): slept: list[float] = [] monkeypatch.setattr(openalex.time, "sleep", lambda s: slept.append(s)) assert _sleep_alive(60, None, fetched=0) is True assert slept == [60]