feat(permits): ЕКБ РНС/РВЭ xlsx parser + schema + ETL (#105 PR A) #211
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#211
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "feat/105-ekburg-permits-parser"
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
Issue #105 PR A (Phase 1+2): xlsx parser + schema + Celery ETL для свежих РНС/РВЭ ЕКБ 2022-2026 с екатеринбург.рф. Closes part of #103 (ИСОГД scraper) + #104 (open.midural РНС stale).
Files (5, ~928 LOC total)
data/sql/96_ekburg_construction_permits.sqlekburg_construction_permits+ 4 indexes (gist geom, issue_date, developer_inn, source_year).UNIQUE(permit_type, permit_number).backend/app/services/scrapers/ekburg_permits.pyEkburgPermitsClient+PermitRowdataclass. Live-verified xlsx structure по всем 5 файлам (2022-2026)backend/app/workers/tasks/ekburg_permits_sync.pyrefresh_year(year)+refresh_all(). UPSERT черезCAST(:raw_row AS jsonb)(psycopg-v3). SAVEPOINT per-row черезbegin_nested()backend/app/workers/celery_app.pyekburg_permits_syncbackend/tests/services/test_ekburg_permits.pyReal xlsx structure (live-verified, 0-based column indices)
_clean_innhelper_to_floathandlerImplementation details
строительс→ RNS,ввод→ RVE_clean_innhandles int/float/str из openpyxl;_to_floathandles comma decimalЧто НЕ в PR (Phase 3-5 follow-ups)
job_settingsentry)./analyzeintegration →engineering.recent_permits_500m+ cross-check сdomrf_kn_objects.Test plan
CAST(:x AS jsonb), никакого:x::type)SELECT COUNT(*) FROM ekburg_construction_permitsпосле первогоrefresh_all()triggered manually → ≥3000 rows (5 лет × ~700 per year per issue acceptance)Closes part of #105 (full close after Phase 3-5 PRs).
Related: #103 (ИСОГД scraper — superseded), #104 (open.midural РНС — superseded), #36 D4 (pipeline 24mo cross-check для Phase 5).
Deep Code Review — verdict
Summary
59d7300(matches scan)🔴 Critical
нет
🟠 High
нет
🟡 Medium (желательно fix, не блокирует)
backend/app/services/scrapers/ekburg_permits.py:142-155—_clean_innregex\d{10,12}без word-boundary: на строке"6685000001-12345"вернёт668500000112(12 цифр, не реальный ИНН). На текущих 5 xlsx-файлах ловушка не триггерится (live-verified), но fragile.backend/app/services/scrapers/ekburg_permits.py:175—_detect_header_rowfallback to7молча при отсутствии «застройщик» в первых 10 строках. Стоитlogger.warning(...)или хотя бы DEBUG, иначе формат-дрейф источника проявится только как «парсится 0 строк».backend/app/workers/tasks/ekburg_permits_sync.py:128-130—refresh_allвызываетrefresh_year(year)напрямую (не.delay()), значит весь цикл из 5 лет занимает один worker slot последовательно. Для monthly cron OK, но при ad-hoc запуске блокирует очередьceleryна ~5-10 мин. Рассмотриcelery.group([refresh_year.s(y) for y in years]).apply_async()в follow-up.🟢 Low / nits
ekburg_permits.py:127-139—_to_strобрабатывает только\xa0NBSP, но не U+202F (narrow NBSP) и U+2007 (figure space). Edge case при copy-paste из Word, на Росстат-формах не встречалось.ekburg_permits.py:225—__exit__(*_: Any) -> None— технически OK, но менее conventional чем(exc_type, exc, tb).ekburg_permits_sync.py:97—@celery_app.taskбезacks_late=True+ retry policy. UPSERT идемпотентен, но при kill worker'а посредине задача теряется до next monthly run. Не блокирующее для refresh-семантики.ekburg_permits.py:148-152—_clean_innдля float:str(int(6685180480.0))правильно, ноfloat> 2^53 потеряет точность (12-digit ИНН max ~10^12 < 2^53=9·10^15, так что safe — оставляю как nit).data/sql/96_ekburg_construction_permits.sql:46-48—geom GEOMETRY(POINT, 4326)будет NULL до Phase 3 (geocoding). GIST index на колонке с 100% NULL — no-op, не страшно, но в COMMENT'е стоит явно прописать «filled in Phase 3».Cross-file impact analysis
backend/app/workers/celery_app.py:30— добавление"app.workers.tasks.ekburg_permits_sync"вinclude. Не ломает existing tasks (поминутно проверил список — registry-only change).ekburg_construction_permits— нет existing consumers (grepengineering\.|ekburg_construction_permitsbackend/ → только сам task). Phase 5engineering.recent_permits_500mintegration в отдельном PR.96_*.sqlследующий свободный после merged95_mv_quarter_price.sql..forgejo/workflows/deploy.yml:208:ls data/sql/*.sql | sort) применит файл при merge —CREATE TABLE IF NOT EXISTS+IF NOT EXISTSна indexes делают это безопасно при re-run.celery_app.pyподхватитtasks.ekburg_permits_sync.refresh_year/refresh_allпосле рестарта worker'а.Vault cross-check
code/modules/ekburg_permits_parser.md(создан в этой же сессии — корректно следует CLAUDE.md rule #6).with db.begin_nested():— повторяет precedent вdomrf_kn.py:427/452/472/723/756/781/821, соответствует vaultBug_Pzz_Loader_Missing_Savepoint_May14.CAST(:raw_row AS jsonb)— psycopg v3 compliant (vaultPattern_CAST_AS_Type,Bug_SQLAlchemy_DoubleColon_Cast). Нет:x::typeconstructs.research/OpenData_Sources_Sverdl_May12.md("АБСОЛЮТНОЕ ОТКРЫТИЕ — Актуальные РНС/РВЭ для ЕКБ найдены на ekburg.rf").Conventions check
httpx(notrequests),logger.*(notprint),from __future__ import annotations,async defN/A (sync Celery task — корректно).psycopg v3:CAST(:raw_row AS jsonb), parametrized binds через dict — нет SQL injection vectors.BEGIN; ... COMMIT;wrap на DDL.CREATE TABLE IF NOT EXISTS,CREATE INDEX IF NOT EXISTS,ON CONFLICT ... DO UPDATE.feat/105-ekburg-permits-parser, нет--no-verify/--force/--amendв коммите.Performance findings
geom, btree наissue_date/developer_inn/source_year+ composite(permit_type, source_year). Покрывает ожидаемые запросы по году/застройщику/диапазону дат.domrf_kn_objects(Phase 5).openpyxl.load_workbook(BytesIO, read_only=True)— streaming, не загружает весь sheet в память.refresh_all— pingable за <30s, timeout 60s достаточен.Positive observations
live-verified xlsx structure— все 5 файлов 2022-2026 распарсены вручную (per PR body)openpyxl.Workbook()в памяти — не требуют network/DB_SKIP_SHEETSблокирует «Справочник»/«Лист1»permit_typeпо имени листа + autodetect header row — robust к format-дрейфу между 2022 и 2023+_to_floatкорректно обрабатывает запятую decimal separator (формат 2022)_is_data_rowфильтрует numeric-only нумерацию столбцов (1-30)Recommended next steps
_INN_REс word-boundary — single-line fixlogger.warningв_detect_header_rowfallbacktasks.ekburg_permits_sync.refresh_year(2026)для smoke-теста, проверитьSELECT COUNT(*) FROM ekburg_construction_permits WHERE source_year=2026≥ 100ekburg-permits-monthlyвjob_settings(1-е число месяца, 05:00 МСК)construction_address→geom(либо resolve СКФ-66 → 4326)/analyzeintegration черезengineering.recent_permits_500mComplexity / blast radius score
DROP TABLE ekburg_construction_permits+ revert celery include + delete files. No data loss outside scraped data which is re-fetchable.Auto-merge: ❌ BLOCKED per scope policy
PR touches blocked paths:
data/sql/**,backend/app/services/scrapers/**,backend/app/workers/**. Per.claude/agents/deep-code-reviewer.mdAuto-merge policy → needs human merge.@lekss361 verdict ✅ APPROVE — готово к merge, нужна твоя команда.