154 lines
5.6 KiB
Python
Executable File
154 lines
5.6 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Read-only report for the production PChome growth mapping backlog."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import urllib.error
|
|
import urllib.parse
|
|
import urllib.request
|
|
from typing import Any
|
|
|
|
from scripts.ops import check_production_version_truth as version_guard
|
|
from services.pchome_mapping_backlog_service import summarize_pchome_mapping_backlog
|
|
|
|
|
|
DEFAULT_API_URL = "https://mo.wooo.work/api/ai/pchome-growth/opportunities"
|
|
DEFAULT_LIMIT = 20
|
|
|
|
|
|
def with_limit(url: str, limit: int) -> str:
|
|
parsed = urllib.parse.urlparse(url)
|
|
query = urllib.parse.parse_qs(parsed.query, keep_blank_values=True)
|
|
query["limit"] = [str(max(5, min(int(limit or DEFAULT_LIMIT), 50)))]
|
|
return urllib.parse.urlunparse(parsed._replace(query=urllib.parse.urlencode(query, doseq=True)))
|
|
|
|
|
|
def fetch_json(url: str, timeout: float) -> dict[str, Any]:
|
|
with urllib.request.urlopen(url, timeout=timeout) as response:
|
|
payload = response.read().decode("utf-8")
|
|
data = json.loads(payload)
|
|
if not isinstance(data, dict):
|
|
raise ValueError("API payload must be a JSON object")
|
|
return data
|
|
|
|
|
|
def summarize_payload(payload: dict[str, Any]) -> dict[str, Any]:
|
|
return summarize_pchome_mapping_backlog(payload)
|
|
|
|
|
|
def build_report(
|
|
api_url: str,
|
|
limit: int,
|
|
timeout: float,
|
|
health_url: str,
|
|
skip_version_truth: bool,
|
|
) -> dict[str, Any]:
|
|
version_report = None
|
|
version_errors: list[str] = []
|
|
if not skip_version_truth:
|
|
version_report = version_guard.build_report(health_url, timeout)
|
|
version_ok, version_errors = version_guard.evaluate(version_report, allow_local_version_drift=False)
|
|
if not version_ok:
|
|
return {
|
|
"policy": "read_only_production_pchome_mapping_backlog",
|
|
"result": "BLOCKED",
|
|
"api_url": with_limit(api_url, limit),
|
|
"version_truth": version_report,
|
|
"errors": version_errors,
|
|
}
|
|
|
|
final_url = with_limit(api_url, limit)
|
|
payload = fetch_json(final_url, timeout)
|
|
summary = summarize_payload(payload)
|
|
return {
|
|
**summary,
|
|
"policy": "read_only_production_pchome_mapping_backlog",
|
|
"api_url": final_url,
|
|
"version_truth": version_report,
|
|
"errors": version_errors,
|
|
}
|
|
|
|
|
|
def format_text(report: dict[str, Any]) -> str:
|
|
stats = report.get("stats") or {}
|
|
backlog = report.get("backlog") or {}
|
|
version = ((report.get("version_truth") or {}).get("production") or {}).get("version")
|
|
lines = [
|
|
"pchome_mapping_backlog:",
|
|
f"- policy: {report.get('policy')}",
|
|
f"- result: {report.get('result')}",
|
|
f"- production_version: {version or 'not_checked'}",
|
|
f"- api_url: {report.get('api_url')}",
|
|
f"- generated_at: {report.get('generated_at')}",
|
|
f"- cache_state: {report.get('cache_state') or 'unknown'}",
|
|
f"- candidate_count: {stats.get('candidate_count')}",
|
|
f"- mapped_count: {stats.get('mapped_count')}",
|
|
f"- mapping_rate: {stats.get('mapping_rate')}%",
|
|
f"- needs_mapping_count: {stats.get('needs_mapping_count')}",
|
|
f"- review_candidate_count: {stats.get('review_candidate_count')}",
|
|
f"- overall_sales_7d: {stats.get('overall_sales_7d')}",
|
|
f"- action_counts: {json.dumps(stats.get('action_counts') or {}, ensure_ascii=False, sort_keys=True)}",
|
|
f"- direct_mapping_count: {backlog.get('direct_mapping_count')}",
|
|
f"- mapped_opportunity_count: {backlog.get('mapped_opportunity_count')}",
|
|
]
|
|
for error in report.get("errors") or []:
|
|
lines.append(f"- blocker: {error}")
|
|
|
|
top_items = list((backlog.get("top_needs_mapping") or [])[:5])
|
|
if top_items:
|
|
lines.append("- top_needs_mapping:")
|
|
for item in top_items:
|
|
lines.append(
|
|
" - "
|
|
f"{item.get('pchome_product_id')} | "
|
|
f"sales_7d={item.get('sales_7d')} | "
|
|
f"score={item.get('priority_score')} | "
|
|
f"action={item.get('action_label')}"
|
|
)
|
|
return "\n".join(lines)
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--api-url", default=DEFAULT_API_URL)
|
|
parser.add_argument("--health-url", default=version_guard.DEFAULT_HEALTH_URL)
|
|
parser.add_argument("--limit", type=int, default=DEFAULT_LIMIT)
|
|
parser.add_argument("--timeout", type=float, default=12.0)
|
|
parser.add_argument("--json", action="store_true", help="Print machine-readable report")
|
|
parser.add_argument(
|
|
"--skip-version-truth",
|
|
action="store_true",
|
|
help="Skip production version guard; intended only for offline parser tests.",
|
|
)
|
|
args = parser.parse_args(argv)
|
|
|
|
try:
|
|
report = build_report(
|
|
api_url=args.api_url,
|
|
limit=args.limit,
|
|
timeout=args.timeout,
|
|
health_url=args.health_url,
|
|
skip_version_truth=args.skip_version_truth,
|
|
)
|
|
except (OSError, ValueError, urllib.error.URLError) as exc:
|
|
report = {
|
|
"policy": "read_only_production_pchome_mapping_backlog",
|
|
"result": "BLOCKED",
|
|
"api_url": with_limit(args.api_url, args.limit),
|
|
"errors": [str(exc)],
|
|
}
|
|
print(json.dumps(report, ensure_ascii=False, indent=2) if args.json else format_text(report))
|
|
return 2
|
|
|
|
if args.json:
|
|
print(json.dumps(report, ensure_ascii=False, indent=2))
|
|
else:
|
|
print(format_text(report))
|
|
return 0 if report.get("result") != "BLOCKED" else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|