feat(cadastre): bulk_harvest worker + grid-walker + admin API (#168 PR3/5) #171
No reviewers
Labels
No labels
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
Fable 5 ревью
feedback/max
generative
GG-форсайт
needs-discussion
needs-human
observability
pause-bots
performance
priority/p0
priority/p1
priority/p2
priority/p3
scope/backend
scope/db
scope/devops
scope/frontend
scope/qa
scrapers
security
site-finder
stage/1
stage/2
status/blocked
status/done
status/needs-analysis
status/needs-fix
status/qa
status/ready
status/review
status/wip
tech-debt
tradein
ux
week ревью 1
wontfix
вторичка
ИРД
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#171
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "feat/cadastre-bulk-worker"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
NSPDBulkClient) and #169 (schema)Components
services/cadastre/bulk_harvest.py— orchestrator4-phase
harvest_quarter():search_by_quarter→ upsert 20 per catmeta_counts > 20для 36368/363697 upsert helpers — все используют
CAST(:x AS jsonb)(НЕ:x::jsonbловушка),begin_nested()SAVEPOINT per grid-walk row,ON CONFLICT (cad_num) DO UPDATEдля idempotency.services/cadastre/grid_geometry.pyquarter_bbox_3857(db, quarter)— PostGISST_Extent(ST_Transform(geom, 3857))generate_grid_click_points(bbox, grid_size=15)— 225 sub-bbox tiles + center click_xy (256, 256)workers/tasks/scrape_cadastre.py— Celery tasksbulk_harvest_quarter_task—acks_late=True,dont_autoretry_for=(NspdBulkWafError,),rate_limit='3/s',soft_time_limit=600enqueue_cadastre_harvest— read job → enqueue per-quarter taskscleanup_cadastre_zombies— periodic:heartbeat_at < NOW() - 10min AND status='running'→ status='zombie'api/v1/admin_cadastre.py— 5 endpoints behindAdminTokenAuthPOST /cadastre/jobs— body{scope: 'pilot'/'ekb_full'/'manual_list', quarters?, limit?, name?}GET /cadastre/jobs— list recentGET /cadastre/jobs/{id}— phase_state + last eventsPOST /cadastre/jobs/{id}/cancelPOST /cadastre/jobs/{id}/resumeResumability
acks_late=True— Celery не ACK'ает task до завершения; redeploy mid-run → task переподхватится с очередиON CONFLICT DO UPDATE) — повтор не создаёт дубликатыphase_state JSONBвcadastre_jobs— task проверяет current phase, продолжает с того местаTest plan
test_cadastre_bulk.py) — mocked DB + mocked NSPDBulkClienttest_admin_cadastre.py) — FastAPI TestClient + AdminTokenAuthPOST /api/v1/admin/scrape/cadastre/jobs {scope:'manual_list', quarters:['66:41:0303161']}→ cad_parcels has 170 rows, cad_buildings 256, cad_zouit 15Dependencies
NSPDBulkClient✅ MERGEDb4bc704PR3 нельзя использовать в runtime без #169 (нет таблиц), но code-only PR можно мержить — DB tests
pytest.importorskipпропускаются если schema отсутствует.What's NOT in this PR
Closes 3/5 of #168.
⚠️ Bot review — CI fail, fix needed
Backend CI ❌ — ruff lint errors на 3 строках
tests/services/test_cadastre_bulk.py:ruff видит что
# noqa: E402(module-level import not at top) suppress'ит правило которое в этих местах не triggered — поэтому директива unused.Quick fix:
Скорее всего imports уже на правильном positions (после module setup), но не на module-top — поэтому E402 не fires и noqa лишний.
После push нового SHA — на следующем cron-тике re-review. Параллельно жду agent для verification остальных pattern checks (SAVEPOINT,
:param::cast, saga state, grid-walker logic, AdminTokenAuth, schema field mapping).Frontend CI ✅, тело PR подтверждает что patterns применены (CAST/begin_nested/ON CONFLICT/AdminTokenAuth), но без agent cross-check и зелёного CI не мержу.
⚠️ Updated bot review — 2 blockers, request changes
Cross-checked агентом — нашёл второй blocker помимо CI fail. Plus minor concerns. Не мержу.
🔴 Blocker #1: CI ruff lint fail
3 unused
# noqa: E402вtests/services/test_cadastre_bulk.py(lines 25, 29, 35). Fix:uv run ruff check --fix backend/tests/services/test_cadastre_bulk.py.🔴 Blocker #2: Function signature mismatch —
tile_sizevstile_width/tile_heightЭто в точности тот тип бага что PR #147 warned about — signature drift между definition и callers.
backend/app/services/cadastre/grid_geometry.py:1447-1451— def:tile_width: int = 512, tile_height: int = 512backend/app/services/cadastre/bulk_harvest.py:618— caller:generate_grid_click_points(bbox, grid_size=grid_size, tile_size=tile_size)backend/tests/services/test_cadastre_bulk.py:2454— test:generate_grid_click_points(bbox, grid_size=5, tile_size=256)→
TypeError: generate_grid_click_points() got an unexpected keyword argument 'tile_size'как только Phase 3 grid-walk fires (на любом quarter > 20 parcels — а это большинство ЕКБ кварталов). Phase 3 dead on arrival.Почему unit tests это пропустили:
pytest.importorskip("app.schemas.nspd_bulk")в начале test module → skip всего файла пока #169 schema не applied. Тесты молча skipped, CI не triggers TypeError.Fix варианты:
tile_size(если semantically один параметр)tile_width=tile_size, tile_height=tile_sizePer «no minor follow-up» policy — fix в этом PR.
Минор (фиксить в этом PR раз касаемся файлов)
Heartbeat gap inside grid-walk:
progress_cbfires только при category start/end, не per-cell. 225 WMS calls × variable latency может exceed 5-min stale threshold. Если NSPD медленный →cleanup_cadastre_zombiesflip'нет job вpausedmid-grid-walk. Добавьupdate_progressкаждые ~25 cells или N секунд.Doc drift
cleanup_cadastre_zombies: PR body говорит "10 min", code (heartbeat_at < NOW() - 5min) — 5 минут. Выбери одно (5 минут OK, но docs sync).progress_cbswallows exceptions + rolls back (line 1686) — silent heartbeat loss possible.Что хорошо (strong patterns followed)
✅
CAST(:x AS jsonb/date)везде — нет:param::castантипаттерна. Тест на line 2554 даже asserts absence. PR #152/#160 lesson learned.✅ SAVEPOINT
with db.begin_nested():per grid-walk upsert (line 651)✅ ON CONFLICT DO UPDATE на every cad_* upsert
✅ Parameterized SQL throughout — единственный f-string (line 1617
_update_job) join'ит fixed literals изsetslist, no user input. Safe.✅ AdminTokenAuth на всех 5 endpoints
✅ Saga state: queued → running → done/paused/cancelled/failed.
acks_late=True+dont_autoretry_for=(NspdBulkWafError,)+ retry_backoff✅ GeoJSON → PostGIS:
ST_Transform(ST_SetSRID(ST_GeomFromGeoJSON(:geom), 3857), 4326)сST_MultiVerdict
Не мержу. После push нового SHA с фиксами обоих блокеров (+минорами в идеале) — re-review на cron-тике.
⚠️ Bot re-review (SHA
60f075c) — 3 blockers, не мержуCI ruff fixed ✓, но новые/неисправленные blocker'ы.
🔴 Blocker #1: Tile_size mismatch — НЕ ИСПРАВЛЕН
Diff показывает что mismatch остался:
backend/app/services/cadastre/grid_geometry.py(definition):backend/app/services/cadastre/bulk_harvest.py(caller — всё ещё bug):Phase 3 grid-walk на любом overflow category →
TypeError: generate_grid_click_points() got an unexpected keyword argument 'tile_size'.Fix варианты:
tile_size: int = 512(если semantically один параметр — judging по callsite это похоже на square tile)generate_grid_click_points(bbox, grid_size=grid_size, tile_width=tile_size, tile_height=tile_size)🔴 Blocker #2:
app.dependency_overridesAttributeError (новый CI fail)10 тестов в
tests/api/v1/test_admin_cadastre.pyпадают на:Author использует
app.dependency_overrides[get_db] = _mock_get_db(db)— ноappтут импортирован как module (backend/app/__init__.py), не FastAPI instance.Fix:
Заменить во всех 7+ местах в
test_admin_cadastre.py(+dependency_overrides.clear()в teardown также).🟡 Blocker #3 (вероятно): другие 2 F в pytest output
CI лог показал ещё 2 F помимо 7 в test_admin_cadastre — нужно посмотреть полный pytest output чтобы знать какие. Возможно related к the same import bug, возможно отдельные.
Что зафиксено
✅ CI ruff lint (3 unused noqa) — fixed in this SHA
✅ Tests no longer all-skipped (
importorskipснят/обновлён)Verdict
Не мержу. Жду новый SHA где:
tile_sizemismatch — выбери unification варианта (a) или (b)from app.main import appв test_admin_cadastre.pyТакже рекомендую запустить локально
uv run pytest backend/tests/ -vперед push'ем чтобы поймать все падения сразу. CI работает но author cycle быстрее когда tests pass локально.Final review (SHA
7e95c97, CI ✅) — all 3 blockers fixedCross-checked агентом — все 3 блокера properly resolved (не papered over).
Blockers — all fixed
✅ Blocker #1:
generate_grid_click_pointssignature drift — definition вgrid_geometry.py:1447теперьtile_size: int = 512(option a from review), caller вbulk_harvest.py:618consistent. Testtest_generate_grid_click_points_custom_gridexercises kwarg.✅ Blocker #2:
app.dependency_overridesAttributeError —tests/api/v1/test_admin_cadastre.py:1964теперьfrom app.main import app, все 8 API тестов используют FastAPI instance. Author добавил inline comment объясняющий trap.✅ Blocker #3: Pytest failures — 28 новых test functions добавлено (13 service + 8 API + 7 geometry/safe/derive). CI: 162 passed / 4 skipped. Skips guarded
requires_scrape_cadastre(PR #170 dependency) — не re-importorskip cover-up.Patterns still hold
✅
CAST(:x AS jsonb/date)везде, explicit guard testtest_upsert_parcel_uses_cast_not_double_colon(line 2527) asserts SQL содержитCAST(:raw_props AS jsonb)и НЕ содержит::jsonb/::date. Hardcoded pattern enforcement в test suite — отличная защита от regression.✅
db.begin_nested()SAVEPOINT per grid-walk upsert (line 651)✅
ON CONFLICT DO UPDATEна всех 7 upsert helpers✅
AdminTokenAuthна всех 5 admin endpoints✅ All SQL parameterized через
text()+:paramLesser concerns — partially addressed
✅
progress_cbexception swallow — fixed: теперьlogger.warning+ rollback (line 1684-1694)🟡 Per-cell heartbeat в
_grid_walk_category— не addressed.update_progressтолько при start/end category, не per-cell. Не блокер: agent math 225 calls × 1/3s rate_limit = ~75s/category, ниже 5-min zombie threshold (300s). Theoretical risk если NSPD сильно медленный.🟡 PR body doc drift "10 min vs 5 min" — code consistent (5 min), wording в description устаревший. Cosmetic.
approve merge