All checks were successful
CI / changes (pull_request) Successful in 8s
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Successful in 1m15s
CI / openapi-codegen-check (pull_request) Successful in 2m7s
CI / backend-tests (pull_request) Successful in 14m57s
Оркестратор build_full_report собирает §1-§7 + карты из analyze/forecast
ранов (реальная schema форсайта = "1.0"), best-effort дотягивает
connection-capacity (§3-резервы + сети рядом) и server-side концепцию
(поток POST /concepts: ConceptInput comfort → generate), рендерит
WeasyPrint'ом (локальный import — web-процесс native не тянет) на volume
/app/reports с метадата-раном report-pdf-1.0. Кэш по (analyze_run_id,
forecast_run_id) с проверкой существования файла (сирота → ре-рендер).
Запись атомарная (tmp+os.replace), имя файла из whitelist-санитизации
кад.номера. Celery task on-demand (time_limit 600), endpoint'ы POST
/{cad}/report (202/200) + status + download (дата из generated_at).
Deploy: bind-mount ./reports:/app/reports (backend+worker) + mkdir/chown
uid1000 в deploy.yml (docker создал бы root:root → PermissionError →
вечный building). reports/ в .gitignore. api-types регенерён (+219).
Refs #2259
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()
|