All checks were successful
CI / changes (pull_request) Successful in 6s
CI / changes (push) Successful in 8s
CI / frontend-tests (push) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (push) Successful in 6m33s
CI / backend-tests (pull_request) Successful in 6m33s
Deploy / changes (push) Successful in 6s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m44s
Deploy / build-worker (push) Successful in 5m24s
Deploy / deploy (push) Successful in 1m30s
Сервис get_developer_attribution поверх fn_developer_for_parcel (миграция 149): топ-1 застройщик участка + track-record (РНС/РВЭ + домрф) + nearby_developers. Дедуп по норм-ИНН (лучший match_method первым). Wire в analyze additive без флага (чистый DB-резолвер по индексам, graceful → None). Beat-refresh developer_registry ежемесячно 05:30 МСК после ekburg-permits. Pydantic-схемы + 18 тестов. Источники: ekburg_construction_permits (РНС, ИНН) ⋈ domrf_kn_objects по нормализованному ИНН + spatial/quarter fallback. 68% domrf-застройщиков имеют РНС. EXPLAIN: point-lookup 0.06ms, резолвер ~30ms (functional/GIST индексы в миграции). Closes #1088
58 lines
2.5 KiB
Python
58 lines
2.5 KiB
Python
"""Celery task: refresh developer_registry materialized view (#1088 «GG-форсайт»).
|
||
|
||
Вызывает ``fn_refresh_developer_registry()`` (миграция 149,
|
||
``data/sql/149_developer_attribution.sql``) — REFRESH MATERIALIZED VIEW CONCURRENTLY
|
||
``developer_registry`` (реестр застройщиков по норм-ИНН: канон-имя + track-record
|
||
permits+domrf). CONCURRENTLY не блокирует SELECT-читателей (analyze) во время refresh.
|
||
|
||
Scheduled via hardcoded beat entry в workers/beat_schedule.py:
|
||
'developer-registry-refresh-monthly' — ежемесячно 1-го числа в 05:30 МСК,
|
||
через 30 мин после 'ekburg-permits-monthly' (05:00 МСК), чтобы permits-сторона
|
||
реестра обновлялась на свежих РНС/РВЭ. domrf-сторона обновляется ежедневно
|
||
(scrape_kn), но ИНН/track-record застройщиков меняются редко — monthly refresh
|
||
реестра с запасом достаточно (резолвер всё равно читает domrf-quarter напрямую).
|
||
|
||
Issue: #1088.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
from typing import Any
|
||
|
||
from sqlalchemy import text
|
||
|
||
from app.core.db import SessionLocal
|
||
from app.workers.celery_app import celery_app
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
@celery_app.task(
|
||
bind=True,
|
||
name="tasks.developer_registry_refresh.refresh_developer_registry",
|
||
max_retries=2,
|
||
)
|
||
def refresh_developer_registry(self: Any) -> dict[str, Any]:
|
||
"""REFRESH MATERIALIZED VIEW CONCURRENTLY developer_registry.
|
||
|
||
Лёгкая задача (реестр ~1024 застройщика). CONCURRENTLY — non-blocking для
|
||
читателей analyze; требует unique-индекс uq_developer_registry_inn (создан
|
||
миграцией 149). Возвращает row-count реестра после refresh для observability.
|
||
|
||
Returns:
|
||
dict для Celery result store / логирования.
|
||
"""
|
||
db = SessionLocal()
|
||
try:
|
||
db.execute(text("SELECT fn_refresh_developer_registry()"))
|
||
db.commit()
|
||
row = db.execute(text("SELECT COUNT(*) FROM developer_registry")).first()
|
||
count = int(row[0]) if row else 0
|
||
logger.info("refresh_developer_registry: completed, developers=%d", count)
|
||
return {"status": "ok", "developer_registry_rows": count}
|
||
except Exception as e:
|
||
logger.exception("refresh_developer_registry failed: %s", e)
|
||
raise
|
||
finally:
|
||
db.close()
|