Files
leocrm/app/plugins/builtins/report_generator/jobs.py
T
Agent Zero abbe7a18fc fix(audit): P0-P3 audit fixes — 838 ruff errors → 0, 30 F821 bugs fixed, 118 files changed
- P0: hooks.py 3-tuple fix, trigger_dispatcher Contract, contacts/plugin unregister_actions_by_owner
- P0: 5 test files — check_permission mocks removed, hardcoded DB credential → env var
- P1: attachment_service DmsFile via Contract helper, restore_registry/history_hooks dedup
- P1: mail/plugin restore unregister, mcp_client datetime.now(UTC), saved_views/filters patterns
- P1: ProtectedRoute fail-closed, 13 test assertion fixes (bcrypt, DB-URLs, SECRET_KEYs)
- P2: deprecated notifications → post_system_message (3 files), forgejo Base, report_generator lazy import
- P2: webhooks permissions, deps.py/roles.py plugin perms removed, import_export default
- P2: address/tags/entity_links patterns removed, worker.py Contract-Umgehungen fixed
- P2: 28 frontend TODOs (hardcoded constants, deprecated notification API)
- P3: dead code, duplicates, deprecated imports, private attr, __import__ inline
- P3: 8 frontend TODOs (LucideIcons, inline styles, XSS, i18n)
- ruff: 838 → 0 (612 auto-fix + 246 manual + 27 F821 regression fix)
- F821: 30 → 0 (AutomationDefinition, DmsFile, user_id, Path, Any, String)
- Contract-Umgehungen: 2 neue gefunden (worker.py:169, worker.py:280) und gefixt
2026-08-16 01:17:18 +02:00

153 lines
4.6 KiB
Python

"""Background job for isolated report generation.
This job runs in the ARQ worker process, NOT in the API process.
This isolates PDF/HTML rendering from the API:
- No shell access in worker
- No Docker socket
- No unnecessary secrets
- CPU/RAM limited by worker process
- Only approved templates
- URL fetching disabled (safe_url_fetcher)
The job:
1. Fetches the template from DB
2. Renders the template (SandboxedEnvironment)
3. Generates PDF/CSV/Excel/JSON
4. Stores the result in DMS
5. Returns the DMS file ID
"""
from __future__ import annotations
import io
import logging
import uuid
from typing import Any
from sqlalchemy import select
from app.core.db import create_db_session
from app.core.job_registry import register_job
from app.core.storage import get_storage_backend
from app.plugins.builtins.report_generator.models import ReportTemplate
from app.plugins.builtins.report_generator.pdf_generator import (
generate_pdf_from_template_content,
render_template_string,
)
logger = logging.getLogger(__name__)
def _generate_csv(rendered: str) -> io.BytesIO:
import csv
buf = io.BytesIO()
writer = csv.writer(io.TextIOWrapper(buf, encoding="utf-8", newline=""))
for line in rendered.strip().split("\n"):
writer.writerow(line.split(","))
buf.seek(0)
return buf
def _generate_excel(rendered: str) -> io.BytesIO:
import openpyxl
wb = openpyxl.Workbook()
ws = wb.active
for i, line in enumerate(rendered.strip().split("\n"), 1):
for j, cell in enumerate(line.split(","), 1):
ws.cell(row=i, column=j, value=cell)
buf = io.BytesIO()
wb.save(buf)
buf.seek(0)
return buf
async def generate_report_job(
ctx: dict[str, Any],
tenant_id: str,
user_id: str,
template_id: str,
data: dict[str, Any],
output_format: str | None = None,
) -> dict[str, Any]:
"""Background job: Generate a report and store in DMS.
Returns:
{"dms_file_id": ..., "filename": ..., "format": ..., "size": ...}
"""
import hashlib
from app.plugins.builtins.contracts import get_contract_registry
_dms_contract = get_contract_registry().get("dms")
dms_file = _dms_contract.dms_file
async with create_db_session() as db:
# 1. Fetch template
result = await db.execute(
select(ReportTemplate).where(
ReportTemplate.id == uuid.UUID(template_id),
ReportTemplate.tenant_id == uuid.UUID(tenant_id),
ReportTemplate.deleted_at.is_(None),
)
)
template = result.scalar_one_or_none()
if template is None:
raise ValueError(f"Template {template_id} not found")
fmt = output_format or template.output_format
# 2. Generate content
if fmt in ("pdf", "print"):
raw_bytes, ext = generate_pdf_from_template_content(
template.content, data, fmt,
)
elif fmt == "csv":
rendered = render_template_string(template.content, data)
file_data = _generate_csv(rendered)
raw_bytes = file_data.getvalue()
ext = "csv"
elif fmt == "excel":
rendered = render_template_string(template.content, data)
file_data = _generate_excel(rendered)
raw_bytes = file_data.getvalue()
ext = "xlsx"
elif fmt == "json":
rendered = render_template_string(template.content, data)
import json
raw_bytes = json.dumps(rendered, indent=2).encode()
ext = "json"
else:
raise ValueError(f"Unsupported format: {fmt}")
# 3. Store in DMS
content_hash = hashlib.sha256(raw_bytes).hexdigest()
storage_path = f"reports/{tenant_id}/{uuid.uuid4().hex}.{ext}"
storage = get_storage_backend()
await storage.save(storage_path, raw_bytes)
dms_file = dms_file(
tenant_id=uuid.UUID(tenant_id),
name=f"{template.name}.{ext}",
folder_id=None,
uploaded_by=uuid.UUID(user_id),
mime_type="application/pdf" if fmt in ("pdf", "print") else f"text/{ext}",
size_bytes=len(raw_bytes),
storage_path=storage_path,
content_hash=content_hash,
owner_id=uuid.UUID(user_id),
)
db.add(dms_file)
await db.flush()
await db.commit()
await db.refresh(dms_file)
return {
"dms_file_id": str(dms_file.id),
"filename": dms_file.name,
"format": fmt,
"size": len(raw_bytes),
}
# Register with the job registry
register_job("generate_report", generate_report_job)