151 lines
5.9 KiB
Python
151 lines
5.9 KiB
Python
"""Celery task: monthly refresh ekburg permits xlsx (Issue #105).
|
||
|
||
Запускается через beat (1-е число каждого месяца в 05:00 МСК).
|
||
Добавить в beat_schedule через job_settings или hardcoded entry (Phase 2 followup).
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import logging
|
||
|
||
from sqlalchemy import text
|
||
from sqlalchemy.orm import Session
|
||
|
||
from app.core.db import SessionLocal
|
||
from app.services.scrapers.ekburg_permits import (
|
||
EKBURG_PERMITS_URLS,
|
||
EkburgPermitsClient,
|
||
PermitRow,
|
||
)
|
||
from app.workers.celery_app import celery_app
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
def _upsert_permit(db: Session, row: PermitRow) -> None:
|
||
"""UPSERT одной строки разрешения в ekburg_construction_permits."""
|
||
db.execute(
|
||
text("""
|
||
INSERT INTO ekburg_construction_permits (
|
||
permit_type, permit_number,
|
||
issue_date, expiry_date,
|
||
developer_inn, developer_name,
|
||
object_name, object_type,
|
||
construction_address, cadastral_number,
|
||
total_area_sqm, living_area_sqm, living_area_fact_sqm,
|
||
rve_number, rve_date,
|
||
raw_coord_x, raw_coord_y,
|
||
source_year, source_url, raw_row
|
||
)
|
||
VALUES (
|
||
:permit_type, :permit_number,
|
||
:issue_date, :expiry_date,
|
||
:developer_inn, :developer_name,
|
||
:object_name, :object_type,
|
||
:construction_address, :cadastral_number,
|
||
:total_area_sqm, :living_area_sqm, :living_area_fact_sqm,
|
||
:rve_number, :rve_date,
|
||
:raw_coord_x, :raw_coord_y,
|
||
:source_year, :source_url, CAST(:raw_row AS jsonb)
|
||
)
|
||
ON CONFLICT (permit_type, permit_number) DO UPDATE SET
|
||
issue_date = EXCLUDED.issue_date,
|
||
expiry_date = EXCLUDED.expiry_date,
|
||
developer_inn = EXCLUDED.developer_inn,
|
||
developer_name = EXCLUDED.developer_name,
|
||
object_name = EXCLUDED.object_name,
|
||
object_type = EXCLUDED.object_type,
|
||
construction_address = EXCLUDED.construction_address,
|
||
cadastral_number = EXCLUDED.cadastral_number,
|
||
total_area_sqm = EXCLUDED.total_area_sqm,
|
||
living_area_sqm = EXCLUDED.living_area_sqm,
|
||
living_area_fact_sqm = EXCLUDED.living_area_fact_sqm,
|
||
rve_number = EXCLUDED.rve_number,
|
||
rve_date = EXCLUDED.rve_date,
|
||
raw_coord_x = EXCLUDED.raw_coord_x,
|
||
raw_coord_y = EXCLUDED.raw_coord_y,
|
||
raw_row = EXCLUDED.raw_row,
|
||
fetched_at = NOW()
|
||
"""),
|
||
{
|
||
"permit_type": row.permit_type,
|
||
"permit_number": row.permit_number,
|
||
"issue_date": row.issue_date,
|
||
"expiry_date": row.expiry_date,
|
||
"developer_inn": row.developer_inn,
|
||
"developer_name": row.developer_name,
|
||
"object_name": row.object_name,
|
||
"object_type": row.object_type,
|
||
"construction_address": row.construction_address,
|
||
"cadastral_number": row.cadastral_number,
|
||
"total_area_sqm": row.total_area_sqm,
|
||
"living_area_sqm": row.living_area_sqm,
|
||
"living_area_fact_sqm": row.living_area_fact_sqm,
|
||
"rve_number": row.rve_number,
|
||
"rve_date": row.rve_date,
|
||
"raw_coord_x": row.raw_coord_x,
|
||
"raw_coord_y": row.raw_coord_y,
|
||
"source_year": row.source_year,
|
||
"source_url": row.source_url,
|
||
"raw_row": json.dumps(row.raw_row, ensure_ascii=False),
|
||
},
|
||
)
|
||
|
||
|
||
@celery_app.task(name="tasks.ekburg_permits_sync.refresh_year", queue="celery")
|
||
def refresh_year(year: int) -> dict[str, int]:
|
||
"""Скачать + распарсить + upsert РНС/РВЭ за один год.
|
||
|
||
Возвращает {"inserted": N, "errors": N}.
|
||
"""
|
||
inserted = 0
|
||
errors = 0
|
||
|
||
url = EKBURG_PERMITS_URLS.get(year)
|
||
if not url:
|
||
logger.error("ekburg_permits_sync: no URL for year=%d", year)
|
||
return {"inserted": 0, "errors": 1}
|
||
|
||
with EkburgPermitsClient() as client:
|
||
try:
|
||
content = client.download_xlsx(year)
|
||
except Exception as exc:
|
||
logger.error("ekburg_permits_sync: download failed year=%d: %s", year, exc)
|
||
return {"inserted": 0, "errors": 1}
|
||
|
||
with SessionLocal() as db:
|
||
for row in client.parse_xlsx(content, year, url):
|
||
try:
|
||
with db.begin_nested():
|
||
_upsert_permit(db, row)
|
||
inserted += 1
|
||
except Exception as exc:
|
||
logger.warning(
|
||
"ekburg_permits_sync: upsert failed %s/%s year=%d: %s",
|
||
row.permit_type,
|
||
row.permit_number,
|
||
year,
|
||
exc,
|
||
)
|
||
errors += 1
|
||
db.commit()
|
||
|
||
logger.info(
|
||
"ekburg_permits_sync: year=%d done inserted=%d errors=%d",
|
||
year,
|
||
inserted,
|
||
errors,
|
||
)
|
||
return {"inserted": inserted, "errors": errors}
|
||
|
||
|
||
@celery_app.task(name="tasks.ekburg_permits_sync.refresh_all", queue="celery")
|
||
def refresh_all() -> dict[str, dict[str, int]]:
|
||
"""Обновить все 5 лет (2022-2026). Планируется через Celery beat ежемесячно."""
|
||
results: dict[str, dict[str, int]] = {}
|
||
for year in sorted(EKBURG_PERMITS_URLS.keys()):
|
||
logger.info("ekburg_permits_sync: starting year=%d", year)
|
||
results[str(year)] = refresh_year(year)
|
||
logger.info("ekburg_permits_sync: refresh_all done: %s", results)
|
||
return results
|