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:
- проверяет scheme/host/IP;
- добавляет politeness delay + jitter;
- занимает semaphore только на время активного HTTP I/O;
- не следует redirect автоматически;
- разрешает максимум пять redirect независимо от retry attempts;
- повторяет timeout/network/429/5xx, а Contracts Finder ещё 403 с cooldown;
- уважает числовой или HTTP-date
Retry-After; - переводит исчерпание попыток в
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¶
- вычисляет 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. Это сознательная этическая и эксплуатационная граница.