gendesign/backend/app/workers/tasks/full_report.py
bot-backend 880bac7738
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
feat(reports): Celery-джоба полного PDF-отчёта + кэш + 3 endpoint'а (#2259 PR-D)
Оркестратор 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
2026-07-03 15:56:58 +05:00

80 lines
4.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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()