"""Pry — Automated White-Label Report Generator. Generate branded PDF/HTML reports from scraped data for client delivery.""" # SPDX-License-Identifier: MIT # Copyright (c) 2026 Rug Munch Media LLC # # Part of Pry — https://git.rugmunch.io/RugMunchMedia/pryscraper # Licensed under MIT. See LICENSE. import logging import os import uuid from datetime import UTC, datetime, timedelta from typing import Any from paths import PRY_DATA_DIR logger = logging.getLogger(__name__) REPORTS_DIR = PRY_DATA_DIR / "reports" REPORTS_DIR.mkdir(parents=True, exist_ok=True) def generate_report( report_type: str, data: dict[str, Any], branding: dict[str, Any] | None = None, output_format: str = "html", ) -> dict[str, Any]: """Generate a white-label report from scraped data. Args: report_type: competitive_analysis, price_monitor, seo_audit, content_tracker data: The scraped data to include in the report branding: Custom branding (agency_name, logo_url, brand_color, domain) output_format: html or pdf Returns report metadata and file path. """ report_id = uuid.uuid4().hex[:8] brand = branding or {} agency_name = brand.get("agency_name", "Pry Intelligence") brand_color = brand.get("brand_color", "#f59e0b") logo_url = brand.get("logo_url", "") now = datetime.now(UTC) report_date = now.strftime("%B %d, %Y") period_start = (now - timedelta(days=7)).strftime("%b %d") period_end = now.strftime("%b %d, %Y") if report_type == "competitive_analysis": html = _build_competitive_report( data, agency_name, brand_color, logo_url, report_date, period_start, period_end ) elif report_type == "price_monitor": html = _build_price_report( data, agency_name, brand_color, logo_url, report_date, period_start, period_end ) elif report_type == "seo_audit": html = _build_seo_report(data, agency_name, brand_color, logo_url, report_date) elif report_type == "content_tracker": html = _build_content_report( data, agency_name, brand_color, logo_url, report_date, period_start, period_end ) else: return {"error": f"Unknown report type: {report_type}"} # Save HTML report report_path = REPORTS_DIR / f"{report_id}_{report_type}.html" try: report_path.write_text(html) except OSError as e: return {"error": str(e)} return { "success": True, "report_id": report_id, "type": report_type, "format": "html", "path": str(report_path), "title": f"{agency_name} — {report_type.replace('_', ' ').title()}", "generated_at": datetime.now(UTC).isoformat(), } def _build_html_base( title: str, body: str, brand_color: str, logo_url: str, agency_name: str ) -> str: """Build HTML document with white-label branding.""" logo_html = ( f'Logo' if logo_url else f'

{agency_name}

' ) return f""" {title}

{title}

{body}
""" def _build_competitive_report( data: dict[str, Any], agency: str, color: str, logo: str, date: str, ps: str, pe: str ) -> str: competitors = data.get("competitors", []) changes = data.get("changes", []) # Build competitor table table_rows = "" for comp in competitors: price_change = comp.get("price_change", 0) badge = ( f' 0 else "down"}">{price_change:+.1f}%' if price_change else '' ) table_rows += f"{comp.get('name', 'Unknown')}${comp.get('price', 0):.2f}{badge}{comp.get('last_updated', 'N/A')}" body = f"""

Report Period: {ps} — {pe}

📊 Competitive Pricing Overview

Tracking {len(competitors)} competitors across your market.

{table_rows}
CompetitorPriceChangeLast Updated

🔔 Key Changes This Period

{"".join(f'
Change {c}
' for c in changes[:10]) if changes else '

No significant changes detected this period.

'}
""" return _build_html_base(f"{agency} — Competitive Analysis", body, color, logo, agency) def _build_price_report( data: dict[str, Any], agency: str, color: str, logo: str, date: str, ps: str, pe: str ) -> str: products = data.get("products", []) total_drops = sum(1 for p in products if p.get("price_change", 0) < 0) if products else 0 total_rises = sum(1 for p in products if p.get("price_change", 0) > 0) if products else 0 rows = "" for p in products or []: pc = p.get("price_change", 0) badge = ( f' 0 else "down"}">{pc:+.1f}%' if pc else 'stable' ) rows += f"{p.get('name', 'N/A')}${p.get('previous_price', 0):.2f}${p.get('current_price', 0):.2f}{badge}" body = f"""

{ps} — {pe}

{len(products)}

Products Tracked

{total_drops}

Price Drops

{total_rises}

Price Rises

📋 Price Changes

{rows}
ProductPreviousCurrentChange
""" return _build_html_base(f"{agency} — Price Monitor", body, color, logo, agency) def _build_seo_report(data: dict[str, Any], agency: str, color: str, logo: str, date: str) -> str: seo = data.get("seo_data", {}) changes = data.get("changes", []) change_items = "" for c in changes[:10]: icon = "🔴" if c.get("severity") == "high" else "🟡" change_items += f"
{icon} {c.get('field', 'Unknown')}: {c.get('to', '')[:80]}
" body = f"""

🔍 SEO Analysis

Title{seo.get("title", "N/A")[:100]}
Meta Description{seo.get("meta_description", "N/A")[:150]}
Word Count{seo.get("word_count", 0):,}
Internal Links{seo.get("links_internal", 0)}
External Links{seo.get("links_external", 0)}
Schema Markup{"✅ Present" if seo.get("has_schema") else "❌ Not found"}

🔄 Recent Changes

{change_items if change_items else '

No changes detected since last scan.

'}
""" return _build_html_base(f"{agency} — SEO Audit", body, color, logo, agency) def _build_content_report( data: dict[str, Any], agency: str, color: str, logo: str, date: str, ps: str, pe: str ) -> str: pages = data.get("pages", []) changes = data.get("changes", []) rows = "" for p in pages: status = ( '' if p.get("status") == "unchanged" else '' ) rows += f"{status}{p.get('title', 'Untitled')[:60]}{p.get('last_changed', 'N/A')[:10]}{p.get('word_count', 0):,}" body = f"""

📄 Content Tracking — {ps} to {pe}

Monitoring {len(pages)} pages for content changes.

{rows}
StatusPageChangedWords

📝 Content Changes Found

{"".join(f'
{c}
' for c in changes[:10]) if changes else '

No content changes detected this period.

'}
""" return _build_html_base(f"{agency} — Content Tracker", body, color, logo, agency) def list_reports() -> list[dict[str, Any]]: """List all generated reports.""" reports = [] for path in sorted(REPORTS_DIR.glob("*.html"), key=os.path.getmtime, reverse=True): parts = path.stem.split("_", 1) report_id = parts[0] report_type = parts[1] if len(parts) > 1 else "unknown" reports.append( { "id": report_id, "type": report_type, "path": str(path), "size_bytes": path.stat().st_size, "generated_at": datetime.fromtimestamp(path.stat().st_mtime).isoformat(), } ) return reports