""" Export script for thesis analytics demonstrations. This script collects the most important analytical outputs from the admin backend and stores them as JSON files that can be used for: - dashboard screenshots - appendix examples - reproducible demo exports for the thesis Typical usage: python scripts/thesis_analytics_export.py \ --base-url https://example.com/api \ --token "$TOKEN" \ --customer-id 123 \ --entity-id 10 --entity-id 11 \ --start-time 2025-01-01T00:00:00Z \ --end-time 2025-12-31T23:59:59Z \ --output-dir ./exports/thesis_demo """ from __future__ import annotations import argparse import json import sys from dataclasses import dataclass from datetime import datetime, timezone from pathlib import Path from typing import Any from urllib.error import HTTPError, URLError from urllib.parse import urlencode from urllib.request import Request, urlopen @dataclass(frozen=True) class ExportSpec: name: str path_template: str extra_params: dict[str, Any] | None = None requires_entities: bool = False EXPORT_SPECS = [ ExportSpec( name="conversation_stats", path_template="/conversation/{customer_id}/stats", ), ExportSpec( name="conversation_stats_monthly", path_template="/conversation/{customer_id}/stats/grouped", extra_params={"frequency": "monthly"}, ), ExportSpec( name="conversation_counts_by_agent_monthly", path_template="/conversation/{customer_id}/conversation-counts/grouped", extra_params={"frequency": "monthly"}, ), ExportSpec( name="log_answer_overview", path_template="/log/{customer_id}/log-answers", ), ExportSpec( name="log_answer_monthly", path_template="/log/{customer_id}/log-answers-grouped", extra_params={"frequency": "monthly"}, ), ExportSpec( name="top_topics", path_template="/topic/{customer_id}/top-topics", ), ExportSpec( name="trending_topics", path_template="/topic/{customer_id}/trending-topics", ), ExportSpec( name="unknown_topics", path_template="/log/{customer_id}/unknown-topics", ), ExportSpec( name="customer_satisfaction", path_template="/rating/conversation/{customer_id}/satisfaction", ), ExportSpec( name="customer_satisfaction_monthly", path_template="/rating/conversation/{customer_id}/conversation-rating/grouped", extra_params={"frequency": "monthly"}, ), ExportSpec( name="topics_dissatisfaction", path_template="/rating/conversation/{customer_id}/topics-dissatisfaction", ), ExportSpec( name="faq_clusters", path_template="/faq/clusters", extra_params={"limit": 10}, requires_entities=True, ), ] def build_query(params: dict[str, Any]) -> str: pairs: list[tuple[str, str]] = [] for key, value in params.items(): if value is None: continue if isinstance(value, list): for item in value: pairs.append((key, str(item))) else: pairs.append((key, str(value))) if not pairs: return "" return "?" + urlencode(pairs, doseq=True) def perform_get(url: str, token: str | None, timeout: int) -> Any: headers = {"Accept": "application/json"} if token: headers["Authorization"] = f"Bearer {token}" request = Request(url, headers=headers, method="GET") with urlopen(request, timeout=timeout) as response: raw = response.read().decode("utf-8") if not raw.strip(): return None return json.loads(raw) def fetch_export( base_url: str, token: str | None, customer_id: int, entity_ids: list[int], start_time: str | None, end_time: str | None, timeout: int, spec: ExportSpec, ) -> dict[str, Any]: params: dict[str, Any] = { "startTime": start_time, "endTime": end_time, } if entity_ids: params["entityIds"] = entity_ids if spec.extra_params: params.update(spec.extra_params) if spec.requires_entities and not entity_ids: return { "ok": False, "skipped": True, "reason": "No entity IDs provided.", } path = spec.path_template.format(customer_id=customer_id) url = base_url.rstrip("/") + path + build_query(params) try: data = perform_get(url, token=token, timeout=timeout) return { "ok": True, "url": url, "data": data, } except HTTPError as exc: body = exc.read().decode("utf-8", errors="replace") return { "ok": False, "url": url, "status": exc.code, "error": body or str(exc), } except URLError as exc: return { "ok": False, "url": url, "error": str(exc), } def fetch_topic_stats( base_url: str, token: str | None, customer_id: int, timeout: int, topic_names: list[str], ) -> dict[str, Any]: result: dict[str, Any] = {} for topic_name in topic_names: path = f"/topic/{customer_id}/topic-stats" url = base_url.rstrip("/") + path + build_query({"topicName": topic_name}) try: result[topic_name] = { "ok": True, "url": url, "data": perform_get(url, token=token, timeout=timeout), } except HTTPError as exc: body = exc.read().decode("utf-8", errors="replace") result[topic_name] = { "ok": False, "url": url, "status": exc.code, "error": body or str(exc), } except URLError as exc: result[topic_name] = { "ok": False, "url": url, "error": str(exc), } return result def write_json(path: Path, payload: Any) -> None: path.parent.mkdir(parents=True, exist_ok=True) path.write_text( json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8", ) def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="Export analytics bundle for thesis demonstrations." ) parser.add_argument( "--base-url", required=True, help="Base URL of the admin backend API, e.g. https://example.com/api", ) parser.add_argument( "--token", default=None, help="Bearer token for authenticated requests.", ) parser.add_argument( "--customer-id", type=int, required=True, help="Customer ID to export analytics for.", ) parser.add_argument( "--entity-id", type=int, action="append", default=[], help="Entity ID filter. May be repeated.", ) parser.add_argument( "--start-time", default=None, help="Start time in ISO-8601 format.", ) parser.add_argument( "--end-time", default=None, help="End time in ISO-8601 format.", ) parser.add_argument( "--topic-name", action="append", default=[], help="Optional topic name for detailed topic statistics. May be repeated.", ) parser.add_argument( "--output-dir", default="exports/thesis_analytics", help="Directory where JSON exports will be stored.", ) parser.add_argument( "--timeout", type=int, default=60, help="HTTP timeout in seconds.", ) return parser.parse_args() def main() -> int: args = parse_args() output_dir = Path(args.output_dir) timestamp = datetime.now(timezone.utc).isoformat() bundle: dict[str, Any] = { "metadata": { "generated_at": timestamp, "base_url": args.base_url, "customer_id": args.customer_id, "entity_ids": args.entity_id, "start_time": args.start_time, "end_time": args.end_time, "topic_names": args.topic_name, }, "exports": {}, } print(f"Exporting analytics bundle for customer {args.customer_id}") for spec in EXPORT_SPECS: print(f" - {spec.name}") payload = fetch_export( base_url=args.base_url, token=args.token, customer_id=args.customer_id, entity_ids=args.entity_id, start_time=args.start_time, end_time=args.end_time, timeout=args.timeout, spec=spec, ) bundle["exports"][spec.name] = payload write_json(output_dir / f"{spec.name}.json", payload) if args.topic_name: print(" - topic_stats") topic_payload = fetch_topic_stats( base_url=args.base_url, token=args.token, customer_id=args.customer_id, timeout=args.timeout, topic_names=args.topic_name, ) bundle["exports"]["topic_stats"] = topic_payload write_json(output_dir / "topic_stats.json", topic_payload) write_json(output_dir / "analytics_bundle.json", bundle) print(f"Done. Files stored in {output_dir}") return 0 if __name__ == "__main__": sys.exit(main())