feat(22d): catalog scraper — skip-today filter + force flag #348

Merged
lekss361 merged 1 commit from feat/22d-catalog-skip-today-force into main 2026-05-17 23:15:22 +00:00
Owner

Summary

  • Selector _SELECT_TARGETS_SQL: вместо 30-day staleness теперь skip-today — пропускаем только то, что catalog_scraped_at = сегодня. Новые (IS NULL) и вчерашние грузим всегда.
  • Task и admin endpoint получили force: bool = False. force=True → кнопка "Загрузить все" — снимает skip и грузит все объекты последнего snapshot.
  • 5 новых тестов: skip-today active, force passthrough, max_objects, rows processed, SQL contains skip-today predicate.

Зачем

Full sweep на 1532 obj ≈ 50 мин/run. До этого фильтр был "не обновлялось ≥30 дней", поэтому повторный запуск после партиала всё равно гнал тот же batch. Теперь partial+resume даёт реальный инкремент. Force остаётся для случаев "обновился schema, нужно перезалить всё".

SQL diff

- WHERE (catalog_scraped_at IS NULL OR catalog_scraped_at < NOW() - INTERVAL '30 days')
+ WHERE (
+     CAST(:force AS boolean)
+     OR catalog_scraped_at IS NULL
+     OR DATE(catalog_scraped_at) < CURRENT_DATE
+ )

CAST(:force AS boolean):force::boolean не сработает в SQLAlchemy 2.0 + psycopg3 (см. .claude/rules/backend.md).

Test plan

  • ruff check clean
  • 5 новых unit-тестов pass (tests/workers/test_scrape_kn_catalog_objects.py)
  • 41 existing scraper test still pass (tests/services/scrapers/test_domrf_catalog_object.py)
  • Post-deploy smoke: 1-й вызов с force=false → грузит N obj, 2-й вызов сразу с force=falseobj_ids_count=0, 3-й с force=true → грузит снова.
  • Beat run во вторник 04:00 UTC — verify лог содержит force=False и obj_ids_count соответствует объектам не-сегодня.
## Summary - Selector `_SELECT_TARGETS_SQL`: вместо 30-day staleness теперь **skip-today** — пропускаем только то, что `catalog_scraped_at` = сегодня. Новые (`IS NULL`) и вчерашние грузим всегда. - Task и admin endpoint получили `force: bool = False`. `force=True` → кнопка "Загрузить все" — снимает skip и грузит все объекты последнего snapshot. - 5 новых тестов: skip-today active, force passthrough, max_objects, rows processed, SQL contains skip-today predicate. ## Зачем Full sweep на 1532 obj ≈ 50 мин/run. До этого фильтр был "не обновлялось ≥30 дней", поэтому повторный запуск после партиала всё равно гнал тот же batch. Теперь partial+resume даёт реальный инкремент. Force остаётся для случаев "обновился schema, нужно перезалить всё". ## SQL diff ```diff - WHERE (catalog_scraped_at IS NULL OR catalog_scraped_at < NOW() - INTERVAL '30 days') + WHERE ( + CAST(:force AS boolean) + OR catalog_scraped_at IS NULL + OR DATE(catalog_scraped_at) < CURRENT_DATE + ) ``` `CAST(:force AS boolean)` — `:force::boolean` не сработает в SQLAlchemy 2.0 + psycopg3 (см. `.claude/rules/backend.md`). ## Test plan - [x] `ruff check` clean - [x] 5 новых unit-тестов pass (`tests/workers/test_scrape_kn_catalog_objects.py`) - [x] 41 existing scraper test still pass (`tests/services/scrapers/test_domrf_catalog_object.py`) - [ ] Post-deploy smoke: 1-й вызов с `force=false` → грузит N obj, 2-й вызов сразу с `force=false` → `obj_ids_count=0`, 3-й с `force=true` → грузит снова. - [ ] Beat run во вторник 04:00 UTC — verify лог содержит `force=False` и `obj_ids_count` соответствует объектам не-сегодня.
lekss361 added 1 commit 2026-05-17 23:12:16 +00:00
Was: 30-day staleness window (catalog_scraped_at < NOW() - INTERVAL '30 days')
Now: skip только если catalog_scraped_at = сегодня; новые (NULL) и вчерашние
грузим всегда. Admin endpoint и beat task получили `force: bool = False` —
True игнорирует skip и грузит все объекты последнего snapshot (кнопка
"Загрузить все").

- SQL: WHERE (CAST(:force AS boolean) OR catalog_scraped_at IS NULL
       OR DATE(catalog_scraped_at) < CURRENT_DATE)
- Task signature: scrape_kn_catalog_objects(region_code, max_objects, force)
- Admin POST /admin/scrape/kn-catalog-objects payload: + force: bool
- Tests: 5 новых (skip-today active, force passthrough, max_objects,
  rows processed, SQL contains skip-today predicate)

Зачем: full sweep на 1532 obj — ~50 мин/run. До сих пор повторный запуск
после партиала всё равно проходил тот же batch — нет признака "уже
обновлено сегодня". Кнопка "Загрузить все" остаётся для force-pass всего
региона (например после schema change).
Author
Owner

Deep Code Review — verdict APPROVE

Files: 3 (admin endpoint, task, tests) · +153/-15 · mergeable, no conflicts

Checks pass

  • psycopg v3: CAST(:force AS boolean) correct (no :force::boolean trap). All other binds parameterised.
  • Logic: IS NULLDATE(catalog_scraped_at) < CURRENT_DATECAST(:force AS boolean) — short-circuit via OR корректен. Race-condition guard (MAX(snapshot_date) WHERE region_cd = :region_code) сохранён из оригинала.
  • Plumbing: force идёт через Pydantic body → apply_async kwargs → SQL bind → response echo + logger. Beat schedule не передаёт force → defaults to False .
  • Tests: 5 unit-тестов покрывают default-skip, force passthrough, max_objects bind, row processing, SQL smoke predicate.
  • Conventions: нет psycopg2 / print / f-string SQL / hardcoded creds. logger.* с observable force=%s.

Edge case (acceptable)

Partial-failed run today не будет ретраиться до завтра без force=True. Author documented это в PR body как accepted trade-off — кнопка "Загрузить все" покрывает recovery.

Post-merge smoke (from PR test plan)

  • 1-й вызов force=false → грузит N obj
  • 2-й вызов сразу force=falseobj_ids_count=0 (skip-today active)
  • 3-й force=true → грузит снова
  • Beat run во вторник 04:00 UTC → лог содержит force=False

Merging via squash.

## Deep Code Review — verdict ✅ APPROVE **Files**: 3 (admin endpoint, task, tests) · +153/-15 · mergeable, no conflicts ### Checks pass - **psycopg v3**: `CAST(:force AS boolean)` correct (no `:force::boolean` trap). All other binds parameterised. - **Logic**: `IS NULL` ∨ `DATE(catalog_scraped_at) < CURRENT_DATE` ∨ `CAST(:force AS boolean)` — short-circuit via `OR` корректен. Race-condition guard (`MAX(snapshot_date) WHERE region_cd = :region_code`) сохранён из оригинала. - **Plumbing**: `force` идёт через Pydantic body → `apply_async kwargs` → SQL bind → response echo + logger. Beat schedule не передаёт `force` → defaults to `False` ✅. - **Tests**: 5 unit-тестов покрывают default-skip, force passthrough, max_objects bind, row processing, SQL smoke predicate. - **Conventions**: нет psycopg2 / print / f-string SQL / hardcoded creds. `logger.*` с observable `force=%s`. ### Edge case (acceptable) Partial-failed run today не будет ретраиться до завтра без `force=True`. Author documented это в PR body как accepted trade-off — кнопка "Загрузить все" покрывает recovery. ### Post-merge smoke (from PR test plan) - 1-й вызов `force=false` → грузит N obj - 2-й вызов сразу `force=false` → `obj_ids_count=0` (skip-today active) - 3-й `force=true` → грузит снова - Beat run во вторник 04:00 UTC → лог содержит `force=False` Merging via squash.
lekss361 merged commit f9adb5cc2f into main 2026-05-17 23:15:22 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#348
No description provided.