#!/usr/bin/env python3
"""AI 选型台 · 数据采集器

公开源，免密钥：
  1. models.dev/api.json                 模型主数据（发布日/价格/上下文/模态/开源）
  2. openrouter.ai/api/v1/models         在售模型 + Artificial Analysis 三指数
  3. huggingface lmarena-ai/leaderboard  人类偏好 Elo（文本/图像/视频/文档/检索）
  4. 官方 RSS + arXiv                    本周发布与论文

输出：
  data/radar.json        模型表 + 媒体榜 + 本周动态
  data/snapshots/*.json  每周快照，用于算升降

用法：python3 collect.py [--offline]
"""
from __future__ import annotations

import argparse
import gzip
import io
import json
import os
import re
import sys
import urllib.request
from datetime import date, datetime, timedelta, timezone
from email.utils import parsedate_to_datetime
from xml.etree import ElementTree as ET

ROOT = os.path.dirname(os.path.abspath(__file__))
DATA = os.path.join(ROOT, "data")
SNAP = os.path.join(DATA, "snapshots")
UA = ("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 "
      "(KHTML, like Gecko) Chrome/128.0 Safari/537.36 ai-selector/1.0")
TIMEOUT = 30
STATUS: list[dict] = []

VENDOR_CN = {
    "openai": "OpenAI", "google": "Google", "anthropic": "Anthropic", "meta": "Meta",
    "xai": "xAI", "deepseek": "DeepSeek", "moonshot": "月之暗面", "alibaba": "阿里",
    "zhipu": "智谱", "zhipuai": "智谱", "bytedance": "字节跳动", "tencent": "腾讯", "baidu": "百度",
    "xiaomi": "小米", "minimax": "MiniMax", "stepfun": "阶跃星辰", "mistral": "Mistral",
    "amazon": "Amazon", "nvidia": "NVIDIA", "cohere": "Cohere", "perplexity": "Perplexity",
    "thinkingmachines": "Thinking Machines", "ant": "蚂蚁", "meituan": "美团",
    "moonshotai": "月之暗面", "xiaomi": "小米",
    "microsoft": "Microsoft", "microsoft-ai": "Microsoft AI", "bfl": "Black Forest Labs",
    "kling": "快手可灵", "runway": "Runway", "reve": "Reve", "aorizon": "Aorizon",
    "hidream": "HiDream", "inception": "Inception", "liquid": "Liquid AI",
    "nous": "Nous", "arcee": "Arcee", "ibm": "IBM", "upstage": "Upstage", "other": "其他",
}

# LMArena 品类：站内 key -> 数据集目录
ARENA = {
    "text": "text",
    "webdev": "webdev",
    "vision": "vision",
    "search": "search",
    "agent": "agent",
    "document": "document",
    "text_to_image": "text_to_image",
    "image_edit": "image_edit",
    "text_to_video": "text_to_video",
    "image_to_video": "image_to_video",
    "video_edit": "video_edit",
}

MEDIA_CATS = ["text_to_image", "image_edit", "text_to_video", "image_to_video", "video_edit"]

FEEDS = [
    ("OpenAI", "https://openai.com/news/rss.xml"),
    ("Google DeepMind", "https://deepmind.google/blog/rss.xml"),
    ("Hugging Face", "https://huggingface.co/blog/feed.xml"),
    ("Google AI", "https://blog.google/technology/ai/rss/"),
]


def log(name: str, ok: bool, count: int = 0, err: str = ""):
    STATUS.append({"name": name, "ok": ok, "count": count, "error": err})
    print(f"  {'✓' if ok else '✗'} {name:<28} {count if ok else err}")


def fetch(url: str, tries: int = 3) -> bytes:
    last = None
    for i in range(tries):
        try:
            req = urllib.request.Request(url, headers={
                "User-Agent": UA, "Accept-Encoding": "gzip", "Accept": "*/*"})
            with urllib.request.urlopen(req, timeout=TIMEOUT) as r:
                raw = r.read()
                if r.headers.get("Content-Encoding") == "gzip":
                    raw = gzip.decompress(raw)
                return raw
        except Exception as e:  # noqa: BLE001
            last = e
    raise last  # type: ignore[misc]


def get_json(url: str):
    return json.loads(fetch(url).decode("utf-8"))


# --------------------------------------------------------------------------- #
# 名称归一化与匹配
# --------------------------------------------------------------------------- #
STOP = {"high", "max", "xhigh", "low", "medium", "thinking", "reasoning", "preview",
        "latest", "instruct", "chat", "pro", "beta", "nothinking"}


def norm(s: str) -> str:
    s = (s or "").lower()
    s = re.sub(r"\(.*?\)", " ", s)
    s = re.sub(r"[^a-z0-9]+", "", s)
    return s


def dedupe_key(name: str) -> str:
    x = (name or "").strip().lower()
    x = re.sub(r"^(meta|openai|google|anthropic|nvidia|alibaba|bytedance|deepseek|moonshot|zhipu|z-ai|pro)[:\s/]+", "", x)
    x = re.sub(r"\((meta|google|openai|nvidia|alibaba|bytedance|anthropic)\)", "", x)
    x = re.sub(r"\blatest\b|\bpreview\b|\bbeta\b", "", x)
    x = re.sub(r"\s+v?\d+\.0$", "", x)
    x = re.sub(r"[\s\-]+\d{4,8}$", "", x)
    x = re.sub(r"[\s\-]+\(?\d{2}[-/]?\d{2}\)?$", "", x)
    return norm(x)


def base_key(mid: str) -> str:
    """anthropic/claude-opus-5.5 -> claudeopus55"""
    mid = mid.split(":")[0].split("/")[-1]
    mid = re.sub(r"-(\d{4,8})$", "", mid)
    mid = re.sub(r"-(preview|latest|instruct|chat)$", "", mid)
    return norm(mid)


NAME_OVERRIDE = {
    "gptimage25sunburst": "GPT Image 2.5 Sunburst",
    "gptimage25flare": "GPT Image 2.5 Flare",
    "gptimage2medium": "GPT Image 2",
    "geminnanobanana21": "Nano Banana 2.1",
    "geminiomni11flash": "Gemini Omni 1.1 Flash",
    "geminiomniflash": "Gemini Omni Flash",
    "dreaminaseedance25720p": "Seedance 2.5",
    "dreaminaseedance20720p": "Seedance 2.0",
    "wan30": "Wan 3.0",
    "minimaxh3": "MiniMax H3",
    "flux3video": "FLUX 3 Video",
    "grokimaginevideo15agent": "Grok Imagine Video 1.5",
    "grokimaginevideo15720p": "Grok Imagine Video 1.5",
    "klingo3pro": "Kling O3 Pro",
    "runwaygen4aleph": "Runway Gen-4 Aleph",
    "seedream50pro": "Seedream 5.0 Pro",
    "maimage26": "MAI Image 2.6",
    "claudeopus5high": "Claude Opus 5",
    "claudefable51max": "Claude Fable 5.1",
}


def pretty(name: str) -> str:
    k = norm(name)
    if k in NAME_OVERRIDE:
        return NAME_OVERRIDE[k]
    s = re.sub(r"[-_]+", " ", name)
    s = re.sub(r"\s*\((?:high|max|xhigh|medium|low|thinking)\)\s*", "", s, flags=re.I)
    s = re.sub(r"\s+", " ", s).strip()
    parts = []
    for w in s.split(" "):
        if re.fullmatch(r"gpt", w, re.I):
            parts.append("GPT")
        elif re.fullmatch(r"[a-z]{1,4}\d.*", w, re.I) and not w.isupper():
            parts.append(w.upper() if len(w) <= 4 else w[:1].upper() + w[1:])
        else:
            parts.append(w[:1].upper() + w[1:] if w else w)
    return " ".join(parts)


def week_of(d: date) -> str:
    y, w, _ = d.isocalendar()
    return f"{y}-W{w:02d}"


# --------------------------------------------------------------------------- #
# 1. 模型主数据
# --------------------------------------------------------------------------- #
def collect_models():
    print("» 模型主数据")
    try:
        md = get_json("https://models.dev/api.json")
        log("models.dev", True, sum(len(p.get("models", {})) for p in md.values()))
    except Exception as e:  # noqa: BLE001
        md = {}
        log("models.dev", False, err=str(e)[:80])

    canon: dict[str, dict] = {}
    for pid, prov in md.items():
        if not isinstance(prov, dict):
            continue
        for mid, m in (prov.get("models") or {}).items():
            cid = m.get("canonical_model_id") or mid
            if cid in canon:
                continue
            cost = m.get("cost") or {}
            canon[cid] = {
                "id": cid,
                "name": m.get("name") or mid,
                "vendor": (cid.split("/")[0] if "/" in cid else pid).lower(),
                "release_date": m.get("release_date") or "",
                "last_updated": m.get("last_updated") or "",
                "knowledge": m.get("knowledge") or "",
                "context": (m.get("limit") or {}).get("context") or 0,
                "max_output": (m.get("limit") or {}).get("output") or 0,
                "price_in": cost.get("input"),
                "price_out": cost.get("output"),
                "open_weights": bool(m.get("open_weights")),
                "reasoning": bool(m.get("reasoning")),
                "modality": "+".join((m.get("modalities") or {}).get("input") or []) + "->" +
                            "+".join((m.get("modalities") or {}).get("output") or []),
                "family": m.get("family") or "",
                "scores": {},
            }

    # OpenRouter：价格 + AA 指数
    try:
        ors = get_json("https://openrouter.ai/api/v1/models")["data"]
        log("openrouter", True, len(ors))
    except Exception as e:  # noqa: BLE001
        ors = []
        log("openrouter", False, err=str(e)[:80])

    or_by_key: dict[str, dict] = {}
    for m in ors:
        mid = m.get("id", "")
        if not mid:
            continue
        key = base_key(mid)
        main = not mid.endswith(":batch") and not mid.endswith(":free")
        item = or_by_key.get(key)
        if item and item["main"] and not main:
            continue
        price = m.get("pricing") or {}
        try:
            pin = round(float(price.get("prompt", 0)) * 1_000_000, 4)
            pout = round(float(price.get("completion", 0)) * 1_000_000, 4)
        except (TypeError, ValueError):
            pin = pout = None
        bench = m.get("benchmarks") or {}
        aa = bench.get("artificial_analysis") or {}
        or_by_key[key] = {
            "main": main, "id": mid, "name": m.get("name") or mid,
            "created": m.get("created") or 0, "context": m.get("context_length") or 0,
            "price_in": pin, "price_out": pout,
            "modality": (m.get("architecture") or {}).get("modality") or "",
            "aa_intelligence": aa.get("intelligence_index"),
            "aa_coding": aa.get("coding_index"),
            "aa_agentic": aa.get("agentic_index"),
            "design_arena": bench.get("design_arena") or [],
        }

    models: dict[str, dict] = {}
    for cid, m in canon.items():
        key = base_key(cid)
        o = or_by_key.pop(key, None)
        if o:
            m["scores"]["aa_intelligence"] = o["aa_intelligence"]
            m["scores"]["aa_coding"] = o["aa_coding"]
            m["scores"]["aa_agentic"] = o["aa_agentic"]
            m["links"] = {"openrouter": "https://openrouter.ai/" + o["id"]}
            m["or_created"] = o["created"]
            m["arena_name"] = o["name"]
            if not m["price_in"] and o["price_in"]:
                m["price_in"], m["price_out"] = o["price_in"], o["price_out"]
            if not m["context"] and o["context"]:
                m["context"] = o["context"]
            if o["design_arena"]:
                m["design_arena"] = o["design_arena"]
            m["listed"] = True
        else:
            m["listed"] = False
        m["key"] = key
        models[cid] = m

    # OpenRouter 独有但 models.dev 没有的（新模型常常先上架）
    for key, o in or_by_key.items():
        if key in {m["key"] for m in models.values()}:
            continue
        vendor = o["id"].split("/")[0].lower()
        models[o["id"]] = {
            "id": o["id"], "name": o["name"], "vendor": vendor,
            "release_date": datetime.utcfromtimestamp(o["created"]).strftime("%Y-%m-%d") if o["created"] else "",
            "last_updated": "", "knowledge": "", "context": o["context"], "max_output": 0,
            "price_in": o["price_in"], "price_out": o["price_out"], "open_weights": False,
            "reasoning": False, "modality": o["modality"], "family": "",
            "scores": {"aa_intelligence": o["aa_intelligence"], "aa_coding": o["aa_coding"],
                       "aa_agentic": o["aa_agentic"]},
            "links": {"openrouter": "https://openrouter.ai/" + o["id"]},
            "or_created": o["created"], "arena_name": o["name"], "key": key,
            "design_arena": o["design_arena"], "listed": True,
        }

    return list(models.values())


# --------------------------------------------------------------------------- #
# 2. 竞技场
# --------------------------------------------------------------------------- #
def collect_arena():
    print("» LMArena 榜单")
    import pyarrow.parquet as pq  # 延迟导入

    out: dict[str, list] = {}
    base = ("https://huggingface.co/datasets/lmarena-ai/leaderboard-dataset/"
            "resolve/main/{}/latest-00000-of-00001.parquet")
    for key, cat in ARENA.items():
        try:
            raw = fetch(base.format(cat))
            t = pq.read_table(io.BytesIO(raw))
            rows = [r for r in t.to_pylist() if (r.get("category") or "overall") == "overall"]
            if not rows:
                rows = t.to_pylist()
            clean = []
            for r in rows:
                clean.append({
                    "model": pretty(str(r.get("model_name", ""))),
                    "raw": r.get("model_name", ""),
                    "org": VENDOR_CN.get(str(r.get("organization", "")).lower(),
                                         str(r.get("organization", ""))),
                    "rating": round(float(r.get("rating") or 0), 1),
                    "low": round(float(r.get("rating_lower") or 0), 1),
                    "high": round(float(r.get("rating_upper") or 0), 1),
                    "votes": int(r.get("vote_count") or 0),
                    "rank": int(r.get("rank") or 0),
                    "published": r.get("leaderboard_publish_date") or "",
                    "key": norm(str(r.get("model_name", ""))),
                })
            clean.sort(key=lambda x: x["rank"] or 9999)
            out[key] = clean
            log(f"lmarena:{key}", True, len(clean))
        except Exception as e:  # noqa: BLE001
            out[key] = []
            log(f"lmarena:{key}", False, err=str(e)[:60])
    return out


def attach_arena(models: list[dict], arena: dict):
    for cat in ("text", "webdev", "vision", "search", "agent", "document"):
        index = {r["key"]: r for r in arena.get(cat, [])}
        for m in models:
            k = m.get("key") or base_key(m["id"])
            hit = index.get(k) or index.get(norm(m.get("arena_name", "")))
            if not hit:
                # 去掉档位后缀再试一次
                for cand, row in index.items():
                    if cand.startswith(k) and len(cand) - len(k) <= 6:
                        hit = row
                        break
            if hit:
                m["scores"][f"arena_{cat}"] = {
                    "rating": hit["rating"], "rank": hit["rank"], "votes": hit["votes"],
                    "ci": [hit["low"], hit["high"]], "published": hit["published"],
                    "display": hit["model"],
                }


# --------------------------------------------------------------------------- #
# 3. 本周动态
# --------------------------------------------------------------------------- #
def collect_news(limit_days: int = 8):
    print("» 官方 RSS")
    cutoff = datetime.now(timezone.utc) - timedelta(days=limit_days)
    items = []
    for name, url in FEEDS:
        try:
            xml = fetch(url)
            root = ET.fromstring(xml)
            n = 0
            for it in root.iter("item"):
                title = (it.findtext("title") or "").strip()
                link = (it.findtext("link") or "").strip()
                pub = it.findtext("pubDate") or it.findtext("{http://purl.org/dc/elements/1.1/}date")
                dt = None
                if pub:
                    try:
                        dt = parsedate_to_datetime(pub)
                    except (TypeError, ValueError):
                        try:
                            dt = datetime.fromisoformat(pub.replace("Z", "+00:00"))
                        except ValueError:
                            dt = None
                if dt and dt.tzinfo is None:
                    dt = dt.replace(tzinfo=timezone.utc)
                if dt and dt < cutoff:
                    continue
                if title and link:
                    items.append({"source": name, "title": title, "url": link,
                                  "date": dt.astimezone(timezone(timedelta(hours=8))).strftime("%m-%d") if dt else ""})
                    n += 1
            log(f"rss:{name}", True, n)
        except Exception as e:  # noqa: BLE001
            log(f"rss:{name}", False, err=str(e)[:60])
    items.sort(key=lambda x: x["date"], reverse=True)
    STRONG = re.compile(r"(gpt-?\d|claude|gemini|llama|qwen|kimi|deepseek|glm-?\d|grok|mistral|"
                        r"introduc|release|preview|open[- ]weight|benchmark|multimodal|reasoning model)", re.I)
    WEAK = re.compile(r"(model|agent|context|launch|announc)", re.I)
    BAD = re.compile(r"(customer|how .* uses|runs \d|signups|case study|partner|collaborat|hiring|policy|"
                     r"government|regulation|funding|acquisition|next chapter|puts .* to work|"
                     r"with openai models)", re.I)
    scored_news = []
    for i in items:
        t = i["title"]
        if BAD.search(t):
            continue
        sc = 2 if STRONG.search(t) else (1 if WEAK.search(t) else 0)
        if sc:
            scored_news.append((sc, i))
    scored_news.sort(key=lambda x: (x[0], x[1]["date"]), reverse=True)
    out, seen = [], set()
    for _, i in scored_news:
        k = re.sub(r"[^a-z0-9]+", "", i["title"].lower())[:60]
        if k in seen:
            continue
        seen.add(k)
        out.append(i)
    return out[:18] or items[:12]


def collect_papers(days: int = 7):
    try:
        q = urllib.parse.quote('cat:cs.CL AND (abs:"benchmark" OR abs:"evaluation")')
        xml = fetch(f"http://export.arxiv.org/api/query?search_query={q}&sortBy=submittedDate&sortOrder=descending&max_results=8")
        root = ET.fromstring(xml)
        ns = {"a": "http://www.w3.org/2005/Atom"}
        out = []
        for e in root.findall("a:entry", ns):
            out.append({"title": " ".join((e.findtext("a:title", default="", namespaces=ns)).split()),
                        "url": e.findtext("a:id", default="", namespaces=ns),
                        "date": (e.findtext("a:published", default="", namespaces=ns) or "")[:10]})
        log("arxiv", True, len(out))
        return out
    except Exception as e:  # noqa: BLE001
        log("arxiv", False, err=str(e)[:60])
        return []


# --------------------------------------------------------------------------- #
# 主流程
# --------------------------------------------------------------------------- #
def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--offline", action="store_true")
    args = ap.parse_args()

    os.makedirs(SNAP, exist_ok=True)
    now = datetime.now(timezone(timedelta(hours=8)))
    week = week_of(now.date())

    if args.offline and os.path.exists(os.path.join(DATA, "radar.json")):
        print("离线模式：保留现有 data/radar.json")
        return

    def pdate(s):
        for fmt in ("%Y-%m-%d", "%Y-%m", "%Y"):
            try:
                return datetime.strptime(s, fmt).date()
            except (TypeError, ValueError):
                continue
        return None

    models = collect_models()
    arena = collect_arena()
    attach_arena(models, arena)
    news = collect_news()
    papers = collect_papers()

    LABS = {"openai", "google", "anthropic", "meta", "xai", "deepseek", "moonshot", "moonshotai",
            "alibaba", "zhipu", "zhipuai", "bytedance", "tencent", "baidu", "xiaomi", "minimax",
            "stepfun", "mistral", "amazon", "nvidia", "cohere", "perplexity", "thinkingmachines",
            "ant", "meituan", "microsoft", "bfl", "kling", "runway", "reve", "hidream", "aorizon"}

    raw_scored = [m for m in models if any(v for v in (m["scores"].get("aa_intelligence"),
                                                       (m["scores"].get("arena_text") or {}).get("rating")))]
    # 去重 + 收敛：只保留近两年仍在售 / 有足够票数的型号，避免把历史模型和供应商副本堆进表
    best: dict[str, dict] = {}
    for m in raw_scored:
        k = dedupe_key(m["name"])
        cur = best.get(k)
        rank = (m["scores"].get("aa_intelligence") or 0,
                (m["scores"].get("arena_text") or {}).get("rating") or 0)
        if not cur or rank > ((cur["scores"].get("aa_intelligence") or 0),
                              (cur["scores"].get("arena_text") or {}).get("rating") or 0):
            best[k] = m
    scored = []
    for m in best.values():
        rd = pdate(m["release_date"])
        votes = (m["scores"].get("arena_text") or {}).get("votes") or 0
        aa = m["scores"].get("aa_intelligence")
        ar = (m["scores"].get("arena_text") or {}).get("rating") or 0
        bad_name = ("/" in m["name"] or "--" in m["name"] or "TEE" in m["name"]
                    or "latest" in m["name"].lower() or m["name"][:1].islower()
                    or m["vendor"] not in LABS)
        if bad_name:
            continue
        if rd and rd >= now.date() - timedelta(days=450) and (aa is not None and aa >= 28 or ar >= 1440):
            scored.append(m)
        elif not rd and aa is not None and aa >= 28:
            scored.append(m)
    scored.sort(key=lambda m: (m["scores"].get("aa_intelligence") or 0,
                               (m["scores"].get("arena_text") or {}).get("rating") or 0), reverse=True)

    media = {c: arena.get(c, [])[:40] for c in MEDIA_CATS}

    # 周快照与涨跌
    prev_file = None
    snaps = sorted(f for f in os.listdir(SNAP) if f.endswith(".json"))
    for f in reversed(snaps):
        if f != f"{week}.json":
            prev_file = os.path.join(SNAP, f)
            break
    prev = {}
    if prev_file:
        try:
            prev = json.load(open(prev_file)).get("models", {})
        except Exception:  # noqa: BLE001
            prev = {}
    changes = []
    for m in scored[:120]:
        old = prev.get(m["id"])
        if not old:
            continue
        for field, label in (("aa_intelligence", "AA 智能指数"),
                             ("aa_coding", "AA 编码"),
                             ("aa_agentic", "AA 智能体")):
            new_v, old_v = m["scores"].get(field), old.get(field)
            if new_v is not None and old_v is not None and abs(new_v - old_v) >= 0.1:
                changes.append({"name": m["name"], "field": label,
                                "from": old_v, "to": new_v, "delta": round(new_v - old_v, 1)})
    changes.sort(key=lambda x: abs(x["delta"]), reverse=True)

    snapshot = {"week": week, "generated_at": now.isoformat(timespec="seconds"),
                "models": {m["id"]: {"aa_intelligence": m["scores"].get("aa_intelligence"),
                                     "aa_coding": m["scores"].get("aa_coding"),
                                     "aa_agentic": m["scores"].get("aa_agentic")}
                           for m in scored}}
    with open(os.path.join(SNAP, f"{week}.json"), "w") as f:
        json.dump(snapshot, f, ensure_ascii=False, indent=1)

    seen_new = set()
    new_this_week = []
    for m in models:
        if m["vendor"] not in LABS or not pdate(m["release_date"]):
            continue
        if pdate(m["release_date"]) < now.date() - timedelta(days=9):
            continue
        k = dedupe_key(m["name"])
        if k in seen_new:
            continue
        seen_new.add(k)
        new_this_week.append(m)
    new_this_week.sort(key=lambda m: (pdate(m["release_date"]), m["scores"].get("aa_intelligence") or 0), reverse=True)

    payload = {
        "meta": {
            "generated_at": now.isoformat(timespec="seconds"),
            "week": week,
            "model_count": len(models),
            "raw_count": len(raw_scored),
            "scored_count": len(scored),
            "media_count": sum(len(v) for v in media.values()),
            "source_status": STATUS,
            "sources_ok": sum(1 for s in STATUS if s["ok"]),
            "sources_total": len(STATUS),
            "baseline": not bool(prev_file),
            "prev_week": os.path.basename(prev_file).replace(".json", "") if prev_file else "",
        },
        "models": scored,
        "media": media,
        "changes": changes[:12],
        "news": news,
        "papers": papers,
        "new_models": [{"name": m["name"], "vendor": VENDOR_CN.get(m["vendor"], m["vendor"]),
                        "date": m["release_date"], "open": m["open_weights"],
                        "price_in": m["price_in"], "price_out": m["price_out"],
                        "aa": m["scores"].get("aa_intelligence")} for m in new_this_week],
    }
    out = os.path.join(DATA, "radar.json")
    with open(out, "w") as f:
        json.dump(payload, f, ensure_ascii=False)
    print(f"\n✓ {out}  {len(models)} 模型 / {len(scored)} 有分 / {len(news)} 条动态 / "
          f"{os.path.getsize(out)//1024} KB / {now:%Y-%m-%d %H:%M}")


if __name__ == "__main__":
    import urllib.parse  # noqa: E402
    main()
