80 lines
4.4 KiB
Python
80 lines
4.4 KiB
Python
"""Celery task: собрать полный PDF-отчёт ПТИЦА в фоне (эпик #2259 PR-D).
|
||
|
||
Сборка полного PDF `/site-finder/analysis/{cad}` (§1–§7 + карты) занимает секунды-десятки
|
||
секунд (WeasyPrint-рендер + OSM-тайлы карт + server-side концепция), поэтому НЕ крутится
|
||
inline на синхронном эндпоинте — здесь фоновая on-demand таска, оркестрирующая
|
||
:func:`build_full_report` (full_report_pdf.py) и пишущая PDF-файл + метадата-ран.
|
||
|
||
ПОТОК: `SessionLocal()` → `build_full_report(db, cad)` → метадата-dict (pdf_path/…) →
|
||
close. Best-effort: нет analyze-рана → ValueError внутри оркестратора → ловим, warning,
|
||
return None (таска не валит воркер — отчётировать нечего). Прочий сбой → logger.exception
|
||
+ None (карты/концепция/форсайт сами best-effort внутри оркестратора).
|
||
|
||
Mirror conventions (forecast.py): `@celery_app.task`, `SessionLocal()` + try/finally close,
|
||
`logger` (не print), синхронный `def`. Очередь — дефолтная `celery` (как forecast-таска: у
|
||
неё нет `queue=` в celery_app routing → default; воркер слушает `--queues=celery,scrape_kn,geo`).
|
||
|
||
TIME LIMIT: WeasyPrint-рендер + карты могут занять до ~минуты на тяжёлом участке —
|
||
щедрый per-task лимит (time_limit=600 / soft_time_limit=540), как у forecast-таски.
|
||
|
||
REGISTRATION: добавлено в `include=[...]` celery_app.py (зеркало forecast). Beat НЕТ —
|
||
таска enqueue-on-demand (триггер из POST /{cad}/report).
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
from typing import Any
|
||
|
||
from app.core.db import SessionLocal
|
||
from app.services.exporters.full_report_pdf import build_full_report
|
||
from app.workers.celery_app import celery_app
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
@celery_app.task(
|
||
bind=True,
|
||
name="tasks.full_report.build_full_report",
|
||
time_limit=600,
|
||
soft_time_limit=540,
|
||
)
|
||
def build_full_report_task(self: Any, cad: str) -> dict[str, Any] | None:
|
||
"""Собрать полный PDF-отчёт участка в фоне и записать файл + метадата-ран.
|
||
|
||
Оркестрация целиком в :func:`build_full_report` (full_report_pdf.py): analyze-ран
|
||
(нет → ValueError → None), forecast-ран (нет → Part B «нет данных»), connection-capacity
|
||
и концепция best-effort, карты + WeasyPrint-рендер → файл на volume `/app/reports/` +
|
||
метадата-ран `report-pdf-1.0`. Кэш по (analyze_run_id, forecast_run_id): тот же ключ +
|
||
файл на месте → cache-hit, не пере-рендерит.
|
||
|
||
Args:
|
||
cad: кадастровый номер участка.
|
||
|
||
Returns:
|
||
Метадата отчёта {pdf_path, analyze_run_id, forecast_run_id, generated_at,
|
||
size_bytes} (+ `cache_hit` при кэш-хите), либо None если нет analyze-рана /
|
||
неожиданный сбой (таска failed gracefully, воркер не падает).
|
||
"""
|
||
db = SessionLocal()
|
||
try:
|
||
logger.info("build_full_report_task: старт cad=%s", cad)
|
||
result = build_full_report(db, cad)
|
||
logger.info(
|
||
"build_full_report_task: готово cad=%s path=%s cache_hit=%s",
|
||
cad,
|
||
result.get("pdf_path"),
|
||
result.get("cache_hit", False),
|
||
)
|
||
return result
|
||
except ValueError as exc:
|
||
# Нет analyze-рана — отчётировать нечего (graceful, не crash воркера).
|
||
logger.warning("build_full_report_task: нечего собирать cad=%s: %s", cad, exc)
|
||
return None
|
||
except Exception:
|
||
# Неожиданный сбой рендера/записи: traceback + None (карты/концепция/форсайт
|
||
# уже best-effort внутри оркестратора — сюда всплывает только фатальное).
|
||
logger.exception("build_full_report_task failed for cad=%s (returning None)", cad)
|
||
return None
|
||
finally:
|
||
db.close()
|