Перейти к содержанию

Crawler

Crawler — основное тестовое задание. Он асинхронно опрашивает два официальных API, нормализует записи, безопасно скачивает файлы, сохраняет metadata и публикует событие индексации.

Вход
TED Search API v3, Contracts Finder OCDS Search или fixture JSON
Выход
sources/tenders/attachments, файлы volume, tender.changed.v1
Параллельность
asyncio tasks + отдельные BoundedSemaphore для API и attachments
Точка запуска
python -m tender_lens.crawler

Карта файлов

Файл Зачем существует Ключевой символ
base.py единый adapter protocol и HTTP safety/retry policy ResilientHttpClient
ted.py TED fields, pagination token и mapping TedAdapter
contracts_finder.py OCDS release, cursor и documents ContractsFinderAdapter
fixture.py offline source для E2E/демо FixtureAdapter
service.py транзакционный orchestration CrawlerService
__main__.py wiring, source isolation и interval loop run()

1. Adapter boundary

SourceAdapter требует только source_code и fetch_page(cursor, limit). Результат SourcePage содержит нормализованные records и следующий opaque cursor.

TED использует POST search с paginationMode=ITERATION, а Contracts Finder — GET с cursor query parameter. Разница не просачивается в CrawlerService.

2. HTTP policy

ResilientHttpClient выполняет один алгоритм для JSON и stream:

  1. проверяет scheme/host/IP;
  2. добавляет politeness delay + jitter;
  3. занимает semaphore только на время активного HTTP I/O;
  4. не следует redirect автоматически;
  5. разрешает максимум пять redirect независимо от retry attempts;
  6. повторяет timeout/network/429/5xx, а Contracts Finder ещё 403 с cooldown;
  7. уважает числовой или HTTP-date Retry-After;
  8. переводит исчерпание попыток в SourceRequestError.

Отдельный instance для source pages и attachments не позволяет крупным файлам занять все слоты JSON polling.

3. Mapping

TedAdapter.map_notice() использует tolerant helpers _first, _string, _datetime, _decimal, потому что Search API может возвращать локализованные/списочные формы. Он создаёт PDF/XML attachments только из официальных links.

ContractsFinderAdapter.map_release() читает OCDS tender, buyer, value, tenderPeriod, documents. Missing optional field становится None; отсутствие id/title отклоняет только конкретную release и пишет warning.

4. Persistence

persist_record():

  • вычисляет canonical content_hash;
  • находит tender по (source_id, external_id);
  • обновляет поля всегда, но ставит pending только при новом hash;
  • сопоставляет attachments по source_url;
  • отсутствующие в новой версии помечает skipped;
  • commit-ит tender и attachment metadata одной транзакцией;
  • возвращает UUID и флаг changed, не ORM-object привязанный к закрытой session.

5. Attachment pipeline

download_record_attachments() запускает независимые coroutines через asyncio.gather; реальный верхний предел задаёт semaphore HTTP client. _download_one() пропускает уже готовый файл, записывает успех или короткую ошибку, и при новом файле переводит готовый tender обратно в pending.

6. Cursor и recovery

Cursor хранится отдельно для ted и contracts_finder. Повтор одного ненулевого cursor внутри запуска считается ошибкой источника и предотвращает бесконечный цикл. republish_pending() восстанавливает разрыв «DB commit прошёл, NATS publish не прошёл».

7. Source isolation

Entry point создаёт клиентов и сервис внутри цикла по источникам. Исключение TED логируется и не запрещает попытку Contracts Finder; CancelledError не поглощается, чтобы контейнер завершался корректно.

Что не является «обходом роботов»

Проект использует открытые API, User-Agent, задержки, jitter и ограничение concurrency. Он не ломает CAPTCHA, авторизацию, WAF или robots policy. Это сознательная этическая и эксплуатационная граница.