#!/usr/bin/env python3 """Discover all Clarizen API types by crawling services + types docs.""" import json import os import re import subprocess import sys from html import unescape from pathlib import Path from urllib.parse import urljoin ROOT = Path(__file__).resolve().parents[1] BASE = os.environ.get("CLARIZEN_BASE_URL", "https://api.clarizen.com") TOKEN = os.environ.get("CLARIZEN_API_KEY") or os.environ.get("API_KEY_AW") if not TOKEN: sys.exit("Set CLARIZEN_API_KEY or API_KEY_AW") TYPE_LINK = re.compile(r'href="(/V2\.0/services/types/([^"]+))"', re.I) SERVICE_LINK = re.compile(r'href="(/V2\.0/services/(?!types)[^"]+)"', re.I) VARIANT_NAME = re.compile(r"
Type
\s*
\s*([^<]+)", re.I) PROP_ROW = re.compile( r"(.*?)\s*(?:)?([^<]+)", re.I | re.S, ) ENUM_ROW = re.compile(r"([^<]+)[^<]*", re.I) def fetch(path: str) -> str: url = urljoin(BASE, path) r = subprocess.run( [ "curl", "-sL", "--max-time", "20", url, "-H", f"Authorization: ApiKey {TOKEN}", "-H", "Accept: text/html", ], capture_output=True, text=True, ) return r.stdout def normalize_type_name(href_tail: str) -> str: name = href_tail.split("/")[-1] if name.lower() in ("object", "string", "integer", "boolean", "json object"): return name return name[0].upper() + name[1:] if name else name def parse_type_page(html: str, type_name: str) -> dict: variants = [] parts = re.split(r"
Type
", html, flags=re.I) for part in parts[1:]: name_m = re.search(r"
\s*([^<]+)", part, re.I) if not name_m: continue variant = unescape(name_m.group(1)).strip() desc_m = re.search( r"
Description
\s*
([^<]*)", part, re.I | re.S ) description = unescape(desc_m.group(1)).strip() if desc_m else "" properties = [] prop_section = re.search(r"
Properties
(.*?)
", part, re.I | re.S) if not prop_section: prop_section = re.search(r"
Properties
(.*)", part, re.I | re.S) if prop_section: block = prop_section.group(1) rows = PROP_ROW.findall(block) for pname, href, ptype in rows: clean_name = re.sub(r"<[^>]+>", "", pname) properties.append( { "name": unescape(clean_name).strip(), "typeHref": href or None, "type": unescape(re.sub(r"<[^>]+>", "", ptype)).strip(), } ) enum_values = [] if re.search(r"
Possible Values
", part, re.I): ev_block = re.search( r"
Possible Values
(.*?)
", part, re.I | re.S ) if ev_block: enum_values = [ unescape(m.group(1)).strip() for m in re.finditer( r"([^<]+)", ev_block.group(1), re.I ) ] variants.append( { "variant": variant, "description": description, "properties": properties, "enumValues": enum_values, } ) return {"name": type_name, "variants": variants} def main(): seeds = ["/V2.0/services/"] # all operations from index index = fetch("/V2.0/services/") seeds.extend(m.group(1) for m in SERVICE_LINK.finditer(index)) discovered_types: set[str] = set() queue: list[str] = [] parsed: dict[str, dict] = {} def enqueue_type(href: str): tail = href.split("/types/")[-1] key = normalize_type_name(tail) if key.lower() in ("object", "string", "integer", "boolean", "json object"): return if key not in discovered_types: discovered_types.add(key) queue.append(key) for seed in seeds: html = fetch(seed) if seed != "/V2.0/services/" else index if seed != "/V2.0/services/": html = fetch(seed) for m in TYPE_LINK.finditer(html): enqueue_type(m.group(1)) while queue: tname = queue.pop(0) if tname in parsed: continue path = f"/V2.0/services/types/{tname}" html = fetch(path) if "
Type
" not in html and "
Possible Values
" not in html: # try lowercase path = f"/V2.0/services/types/{tname[0].lower()}{tname[1:]}" html = fetch(path) info = parse_type_page(html, tname) parsed[tname] = info for v in info["variants"]: for p in v["properties"]: if p.get("typeHref"): enqueue_type(p["typeHref"]) out = { "typeCount": len(parsed), "types": sorted(parsed.keys()), "parsed": parsed, } out_path = ROOT / "src" / "generated" / "clarizen-schema.json" out_path.parent.mkdir(parents=True, exist_ok=True) out_path.write_text(json.dumps(out, indent=2), encoding="utf-8") print(f"Wrote {out_path} ({len(parsed)} types)") if __name__ == "__main__": main()