Files
anti-plagiarism/scripts/parsers/tests/test_progress_cb.py
jze9 f1a8d07cb3
All checks were successful
Deploy / test (push) Successful in 4m3s
Deploy / deploy (push) Successful in 5s
fix(openalex): пауза между страницами и живой backoff — заливка тонула в 429
Массовый запуск показал: лимит вежливого пула OpenAlex (10 req/s) общий на
mailto, а не на процесс. Четыре воркера с паузой 0.1с получали сплошные 429,
и каждый прогон уходил в 900с бесполезного backoff, не забрав ничего.

- RATE_LIMIT_DELAY 0.1 → 1.0с (≈4 req/s на четырёх воркерах);
- backoff спит кусками по 5с и отчитывается через progress_cb: прогон больше
  не выглядит зависшим в админке и отменяется во время ожидания, а не после.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 17:54:42 +05:00

94 lines
3.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Контракт 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]