JavaScript 58.4%
Python 26.8%
CSS 7.5%
Objective-C 3%
R 2.6%
HTML 1.7%
1/* PDB — page API & playground (démarrage rapide, playground, recettes, référence, SDK). */2window.PDBPlayground = (() => {3 const LANGS = [["curl", "curl"], ["py", "Python"], ["js", "JavaScript"], ["r", "R"]];4 const KEY_PH = "VOTRE_CLE";5 const key = () => localStorage.getItem("pdb_key") || "";6 const lang = () => localStorage.getItem("pdb_lang") || "py";7 const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));8 const qs = (o) => Object.entries(o).filter(([, v]) => v !== "" && v != null).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");9 const fmtN = (n) => n == null ? "—" : Number(n).toLocaleString("fr-CA");1011 /* ------------------------------------------------------------------ catalogue des routes */12 const P = (name, desc, opts = {}) => ({ name, desc, ...opts });13 const PROJECT_FILTERS = [14 P("q", "texte libre : nom, description, entreprise, ticker"), P("type", "project_type, liste séparée par des virgules", { enum: "types" }),15 P("sector", "secteur GICS (contient)", { enum: "sectors" }), P("status", "planned | in_progress | completed | mentioned (liste possible)"),16 P("ticker", "ticker exact"), P("cik", "CIK SEC (10 chiffres ou moins)"), P("location", "localisation canonique (contient)"),17 P("tech", "technologie (contient)"), P("partner", "partenaire cité dans une mention (contient)"),18 P("min_amount", "montant divulgué minimal, US$", { type: "number" }), P("max_amount", "montant maximal, US$", { type: "number" }),19 P("year_from", "dernière observation ≥ année", { type: "number" }), P("year_to", "première observation ≤ année", { type: "number" }),20 P("min_confidence", "confiance moyenne minimale (0–1)", { type: "number" }), P("has_amount", "true : seulement les projets avec montant"),21 ];22 const PAGE = [P("sort", "clé de tri"), P("order", "asc | desc"), P("limit", "≤ 500 (défaut 50)", { type: "number" }), P("offset", "décalage", { type: "number" })];23 const ENDPOINTS = [24 { g: "Découverte", m: "GET", p: "/v1/health", key: false, d: "État du service : compteur de projets, état du modèle sémantique. Sans clé.", params: [], ex: [{ l: "État", v: {} }] },25 { g: "Découverte", m: "GET", p: "/v1/stats", d: "Vue d'ensemble : compteurs, répartitions par type, secteur, statut, formulaire, année, thèmes émergents, top entreprises/technologies/lieux. C'est la source du tableau de bord.", params: [], ex: [{ l: "Tout", v: {} }] },26 { g: "Découverte", m: "GET", p: "/v1/taxonomy", d: "Les 21 types de projets (clé, libellé, nombre de projets, capital divulgué).", params: [], ex: [{ l: "Liste", v: {} }] },27 { g: "Découverte", m: "GET", p: "/v1/sectors", d: "Les 11 secteurs GICS avec nombre de projets, d'entreprises et capital.", params: [], ex: [{ l: "Liste", v: {} }] },28 { g: "Découverte", m: "GET", p: "/v1/technologies", d: "Technologies citées dans les projets, par fréquence.", params: [P("q", "filtre texte"), P("limit", "≤ 500", { type: "number" })], ex: [{ l: "Top 25", v: { limit: 25 } }, { l: "Batteries", v: { q: "batter" } }] },29 { g: "Découverte", m: "GET", p: "/v1/locations", d: "Localisations canoniques, par fréquence, avec capital.", params: [P("q", "filtre texte"), P("limit", "≤ 500", { type: "number" })], ex: [{ l: "Top 25", v: { limit: 25 } }, { l: "Texas", v: { q: "texas" } }] },30 { g: "Découverte", m: "GET", p: "/v1/partners", d: "Partenaires cités dans les mentions (coentreprises, clients, fournisseurs nommés).", params: [P("q", "filtre texte"), P("limit", "≤ 500", { type: "number" })], ex: [{ l: "Top 25", v: { limit: 25 } }, { l: "Microsoft", v: { q: "microsoft" } }] },31 { g: "Projets", m: "GET", p: "/v1/projects", d: "Recherche de projets avec filtres combinables. Réponse paginée {total, limit, offset, items}. Tri : amount, mentions, filings, first_seen, last_seen, confidence, name, ticker, type.", params: [...PROJECT_FILTERS, ...PAGE], ex: [32 { l: "Centres de données > 500 M$", v: { type: "data_center", min_amount: "5e8", sort: "amount", limit: 10 } },33 { l: "IA depuis 2024", v: { type: "ai_initiative", year_from: 2024, sort: "last_seen", limit: 20 } },34 { l: "Usines au Texas", v: { type: "plant_construction,manufacturing_expansion", location: "texas", limit: 20 } },35 { l: "Batteries, terminées", v: { tech: "battery", status: "completed", limit: 20 } },36 { l: "Tesla", v: { ticker: "TSLA", sort: "mentions", limit: 20 } },37 { l: "Partenaire NVIDIA", v: { partner: "nvidia", limit: 20 } },38 ] },39 { g: "Projets", m: "GET", p: "/v1/projects/export.csv", d: "Export CSV des projets filtrés (mêmes filtres que /v1/projects, ≤ 50 000 lignes, listes jointes par |).", params: PROJECT_FILTERS, raw: true, ex: [{ l: "Tous les centres de données", v: { type: "data_center" } }, { l: "Services publics avec montant", v: { sector: "Utilities", has_amount: "true" } }] },40 { g: "Projets", m: "GET", p: "/v1/projects/{project_id}", d: "Fiche complète d'un projet : attributs, chronologie (une ligne par mention datée), mentions détaillées (lieux, technologies, partenaires, bénéfices, risques), voisins du graphe et projets similaires par embeddings.", params: [P("project_id", "identifiant MD5 (voir /v1/projects)", { path: true })], ex: [{ l: "Tesla — Energy Storage Products", v: { project_id: "TSLA_ENERGY" } }, { l: "WEC — Data Center Investments", v: { project_id: "46fe2966ba0cbf6228807faca0645b54" } }] },41 { g: "Projets", m: "GET", p: "/v1/projects/{project_id}/similar", d: "Projets sémantiquement proches (similarité cosinus des vecteurs MiniLM 384-d stockés).", params: [P("project_id", "identifiant", { path: true }), P("k", "≤ 50", { type: "number" })], ex: [{ l: "10 voisins", v: { project_id: "46fe2966ba0cbf6228807faca0645b54", k: 10 } }] },42 { g: "Mentions & sections", m: "GET", p: "/v1/mentions", d: "Mentions individuelles (une par projet et par section de filing) : l'unité d'extraction brute, avec formulaire, date, section, montant, confiance. Chaque mention porte le project_id du projet résolu.", params: [P("q", "texte libre"), P("type", "project_type", { enum: "types" }), P("sector", "", { enum: "sectors" }), P("status", ""), P("ticker", ""), P("cik", ""), P("form", "10-K, 10-Q, 8-K (liste possible)"), P("date_from", "AAAA-MM-JJ"), P("date_to", "AAAA-MM-JJ"), P("min_amount", "US$", { type: "number" }), P("min_confidence", "0–1", { type: "number" }), P("accession", "numéro d'accession SEC"), P("sort", "date | amount | confidence | ticker"), P("order", "asc | desc"), P("limit", "≤ 500", { type: "number" }), P("offset", "", { type: "number" })], ex: [43 { l: "8-K de NVIDIA", v: { ticker: "NVDA", form: "8-K", limit: 20 } },44 { l: "IA en 2025, confiance ≥ 0,9", v: { type: "ai_initiative", date_from: "2025-01-01", min_confidence: 0.9, limit: 20 } },45 { l: "Gros montants 10-K", v: { form: "10-K", min_amount: "1e10", sort: "amount", limit: 20 } },46 ] },47 { g: "Mentions & sections", m: "GET", p: "/v1/mentions/{mention_id}", d: "Une mention complète et, si elle vient d'un 10-K, la section source (section_text_url).", params: [P("mention_id", "identifiant SHA-1 (20 car.)", { path: true })], ex: [{ l: "Exemple", v: { mention_id: "MENTION_10K" } }] },48 { g: "Mentions & sections", m: "GET", p: "/v1/sections/{section_id}", d: "Texte intégral d'une section 10-K ingérée par SPID (Items 1, 1A, 2, 7, 7A). Les sections 10-Q/8-K ne sont pas stockées dans la base.", params: [P("section_id", "UUID de section", { path: true }), P("highlight", "mot à repérer : renvoie les positions")], ex: [{ l: "Exemple", v: { section_id: "SECTION_10K" } }] },49 { g: "Entreprises", m: "GET", p: "/v1/companies", d: "Les 500 entreprises avec taille de portefeuille (projets, mentions, types, capital, période). Tri : projects, amount, mentions, ticker.", params: [P("q", "nom ou ticker"), P("sector", "", { enum: "sectors" }), P("sort", ""), P("order", ""), P("limit", "≤ 500", { type: "number" }), P("offset", "", { type: "number" })], ex: [{ l: "Top 20", v: { sort: "projects", limit: 20 } }, { l: "Santé par capital", v: { sector: "Health Care", sort: "amount", limit: 20 } }] },50 { g: "Entreprises", m: "GET", p: "/v1/companies/{ident}", d: "Profil d'une entreprise (ticker ou CIK) : répartitions par type, statut, année, formulaire ; lieux, technologies, partenaires ; portefeuille complet (≤ 500 projets).", params: [P("ident", "ticker ou CIK", { path: true })], ex: [{ l: "Tesla", v: { ident: "TSLA" } }, { l: "Duke Energy", v: { ident: "DUK" } }, { l: "Microsoft (CIK)", v: { ident: "789019" } }] },51 { g: "Graphe", m: "GET", p: "/v1/graph/search", d: "Chercher un nœud du graphe de connaissances par libellé, avec son degré.", params: [P("q", "texte"), P("type", "Company | Project | Location | Technology | Partner"), P("limit", "≤ 200", { type: "number" })], ex: [{ l: "Texas", v: { q: "texas" } }, { l: "Technologies IA", v: { q: "ai", type: "Technology" } }] },52 { g: "Graphe", m: "GET", p: "/v1/graph/node/{node_id}", d: "Un nœud et ses arêtes (owns, located_in, uses) avec les nœuds voisins. Identifiants : C:<cik>, P:<project_id>, L:<lieu en minuscules>, T:<technologie>, PR:<partenaire>.", params: [P("node_id", "ex. T:AI, L:texas, C:0001318605", { path: true }), P("limit", "≤ 2000", { type: "number" })], ex: [{ l: "T:AI", v: { node_id: "T:AI", limit: 50 } }, { l: "L:texas", v: { node_id: "L:texas", limit: 50 } }, { l: "C:Tesla", v: { node_id: "C:0001318605", limit: 50 } }] },53 { g: "Recherche", m: "GET", p: "/v1/search/semantic", d: "Recherche en langage naturel : la requête est encodée avec all-MiniLM-L6-v2 côté serveur et comparée aux 19 227 vecteurs de projets. Anglais recommandé (langue des filings).", params: [P("q", "description en langage naturel"), P("k", "≤ 100", { type: "number" }), P("type", "filtre project_type", { enum: "types" }), P("sector", "filtre secteur", { enum: "sectors" })], ex: [{ l: "Usine de cellules de batteries", v: { q: "battery cell factory", k: 10 } }, { l: "IA générative pour clients", v: { q: "generative AI assistant for customers", k: 10 } }, { l: "Terminal GNL", v: { q: "LNG export terminal", k: 10 } }] },54 { g: "SQL", m: "GET", p: "/v1/schema", d: "Tables, colonnes et types de la base DuckDB.", params: [], ex: [{ l: "Schéma", v: {} }] },55 { g: "SQL", m: "POST", p: "/v1/sql", d: "Bac à sable SQL DuckDB en lecture seule (SELECT, WITH, DESCRIBE, SUMMARIZE). Corps JSON {sql, limit}. 20 s et 5 000 lignes maximum. Réponse {columns, rows, truncated, elapsed_ms}.", params: [P("sql", "requête SQL", { body: true, textarea: true }), P("limit", "≤ 5000", { body: true, type: "number" })], ex: [56 { l: "Projets par type", v: { sql: "select project_type, count(*) n, round(sum(total_amount_usd)/1e9,1) capital_gusd\nfrom projects group by 1 order by n desc", limit: 25 } },57 { l: "Mentions IA par année", v: { sql: "select year(filing_date) as yr, count(*) n\nfrom project_mentions where project_type='ai_initiative' group by 1 order by 1", limit: 30 } },58 { l: "Panel entreprise × année", v: { sql: "select cik, any_value(ticker) ticker, year(first_seen) as yr, count(*) n_projets, sum(total_amount_usd) capital\nfrom projects group by 1,3 order by 1,3", limit: 5000 } },59 { l: "Technologies co-citées", v: { sql: "select a.t t1, b.t t2, count(*) n\nfrom (select project_id, unnest(technologies) t from projects) a\njoin (select project_id, unnest(technologies) t from projects) b on a.project_id=b.project_id and a.t<b.t\ngroup by 1,2 order by n desc", limit: 50 } },60 ] },61 ];62 const GROUPS = [...new Set(ENDPOINTS.map((e) => e.g))];6364 /* ------------------------------------------------------------------ générateurs de code */65 function buildRequest(e, vals) {66 let path = e.p; const query = {}; let body = null;67 for (const prm of e.params) {68 const v = vals[prm.name]; if (v === undefined || v === "") continue;69 if (prm.path) path = path.replace(`{${prm.name}}`, encodeURIComponent(v));70 else if (prm.body) (body ||= {})[prm.name] = prm.type === "number" ? Number(v) : v;71 else query[prm.name] = v;72 }73 path = path.replace(/\{[^}]+\}/g, "");74 return { method: e.m, path, query, body, url: location.origin + path + (Object.keys(query).length ? "?" + qs(query) : "") };75 }76 const pyDict = (o) => "{" + Object.entries(o).map(([k, v]) => `${JSON.stringify(k)}: ${typeof v === "number" ? v : JSON.stringify(v)}`).join(", ") + "}";77 const rList = (o) => "list(" + Object.entries(o).map(([k, v]) => `${/^[a-z_]+$/i.test(k) ? k : "`" + k + "`"} = ${typeof v === "number" ? v : JSON.stringify(v)}`).join(", ") + ")";78 function snippet(l, r, k = KEY_PH, nokey = false) {79 const base = location.origin + r.path;80 if (nokey) return { curl: `curl '${r.url}'`, py: `import requests\n\nr = requests.get("${r.url}")\nr.raise_for_status()\nprint(r.json())`, js: `const r = await fetch("${r.url}");\nconsole.log(await r.json());`, r: `library(httr2)\nrequest("${r.url}") |> req_perform() |> resp_body_json()` }[l];81 if (l === "curl") return r.method === "POST"82 ? `curl -X POST '${base}' \\\n -H 'X-API-Key: ${k}' -H 'Content-Type: application/json' \\\n -d '${JSON.stringify(r.body || {})}'`83 : `curl '${r.url}' -H 'X-API-Key: ${k}'`;84 if (l === "py") return r.method === "POST"85 ? `import requests\n\nr = requests.post("${base}",\n headers={"X-API-Key": "${k}"},\n json=${pyDict(r.body || {})})\nr.raise_for_status()\ndata = r.json()\nprint(data)`86 : `import requests\n\nr = requests.get("${base}",\n headers={"X-API-Key": "${k}"},${Object.keys(r.query).length ? `\n params=${pyDict(r.query)},` : ""}\n)\nr.raise_for_status()\ndata = r.json()\nprint(data)`;87 if (l === "js") return `const r = await fetch("${r.url}", {\n method: "${r.method}",\n headers: { "X-API-Key": "${k}"${r.body ? ', "Content-Type": "application/json"' : ""} },${r.body ? `\n body: JSON.stringify(${JSON.stringify(r.body)}),` : ""}\n});\nif (!r.ok) throw new Error(\`HTTP \${r.status}\`);\nconst data = await r.json();\nconsole.log(data);`;88 if (l === "r") return `library(httr2)\n\nresp <- request("${base}") |>\n req_headers(\`X-API-Key\` = "${k}") |>${Object.keys(r.query).length ? `\n req_url_query(!!!${rList(r.query)}) |>` : ""}${r.body ? `\n req_body_json(${rList(r.body)}) |>` : ""}\n req_perform()\ndata <- resp_body_json(resp)\nstr(data, max.level = 1)`;89 }9091 /* ------------------------------------------------------------------ recettes */92 const RECIPES = [93 { t: "Premier appel : compter les projets", d: "Vérifier la clé et lire les compteurs globaux.", ep: "/v1/stats", v: {}, code: {94 curl: `curl https://www.pdb-api.co/v1/stats -H 'X-API-Key: ${KEY_PH}' | python3 -m json.tool | head -30`,95 py: `import requests\nBASE, H = "https://www.pdb-api.co/v1", {"X-API-Key": "${KEY_PH}"}\n\nov = requests.get(f"{BASE}/stats", headers=H).json()["overview"]\nprint(f"{ov['projects']:,} projets · {ov['mentions']:,} mentions · {ov['companies']} entreprises")`,96 js: `const BASE = "https://www.pdb-api.co/v1", H = { "X-API-Key": "${KEY_PH}" };\nconst { overview } = await (await fetch(\`\${BASE}/stats\`, { headers: H })).json();\nconsole.log(overview.projects, "projets", overview.mentions, "mentions");`,97 r: `library(httr2)\nBASE <- "https://www.pdb-api.co/v1"; KEY <- "${KEY_PH}"\nov <- request(paste0(BASE, "/stats")) |> req_headers(\`X-API-Key\` = KEY) |> req_perform() |> resp_body_json()\nov$overview$projects` } },98 { t: "Filtrer et trier des projets", d: "Centres de données de plus de 500 M$, du plus gros au plus petit.", ep: "/v1/projects", v: { type: "data_center", min_amount: "5e8", sort: "amount", limit: 10 }, code: {99 curl: `curl 'https://www.pdb-api.co/v1/projects?type=data_center&min_amount=5e8&sort=amount&limit=10' -H 'X-API-Key: ${KEY_PH}'`,100 py: `r = requests.get(f"{BASE}/projects", headers=H, params={\n "type": "data_center", "min_amount": 5e8, "sort": "amount", "limit": 10})\nfor p in r.json()["items"]:\n print(f"{p['ticker']:6s} {p['project_name'][:45]:45s} {p['total_amount_usd']/1e9:6.1f} G$ {p['canonical_location']}")`,101 js: `const q = new URLSearchParams({ type: "data_center", min_amount: 5e8, sort: "amount", limit: 10 });\nconst { total, items } = await (await fetch(\`\${BASE}/projects?\${q}\`, { headers: H })).json();\nitems.forEach(p => console.log(p.ticker, p.project_name, p.total_amount_usd / 1e9, "G$"));`,102 r: `res <- request(paste0(BASE, "/projects")) |> req_headers(\`X-API-Key\` = KEY) |>\n req_url_query(type = "data_center", min_amount = 5e8, sort = "amount", limit = 10) |>\n req_perform() |> resp_body_json()\ndo.call(rbind, lapply(res$items, \\(p) data.frame(ticker = p$ticker, projet = p$project_name, gusd = p$total_amount_usd / 1e9)))` } },103 { t: "Tout récupérer avec la pagination → DataFrame", d: "Boucler sur limit/offset (≤ 500 par page) pour construire un jeu de données complet.", ep: "/v1/projects", v: { sector: "Utilities", limit: 500 }, code: {104 curl: `# page 1, 2, 3… : incrémenter offset de 500 jusqu'à atteindre total\nfor off in 0 500 1000 1500; do\n curl -s "https://www.pdb-api.co/v1/projects?sector=Utilities&limit=500&offset=$off" -H 'X-API-Key: ${KEY_PH}' > utilities_$off.json\ndone`,105 py: `import pandas as pd\n\ndef fetch_all(path, **filters):\n offset, rows = 0, []\n while True:\n page = requests.get(f"{BASE}/{path}", headers=H, params={**filters, "limit": 500, "offset": offset}).json()\n rows += page["items"]\n offset += len(page["items"])\n if not page["items"] or offset >= page["total"]:\n return rows\n\ndf = pd.DataFrame(fetch_all("projects", sector="Utilities"))\ndf["technologies"] = df["technologies"].str.join(" | ")\nprint(df.shape)\ndf.groupby("project_type")["total_amount_usd"].agg(["count", "median"]).sort_values("count", ascending=False)`,106 js: `async function fetchAll(path, filters) {\n const rows = []; let offset = 0;\n for (;;) {\n const q = new URLSearchParams({ ...filters, limit: 500, offset });\n const page = await (await fetch(\`\${BASE}/\${path}?\${q}\`, { headers: H })).json();\n rows.push(...page.items); offset += page.items.length;\n if (!page.items.length || offset >= page.total) return rows;\n }\n}\nconst utilities = await fetchAll("projects", { sector: "Utilities" });\nconsole.log(utilities.length, "projets");`,107 r: `fetch_all <- function(path, ...) {\n out <- list(); offset <- 0\n repeat {\n page <- request(paste0(BASE, "/", path)) |> req_headers(\`X-API-Key\` = KEY) |>\n req_url_query(..., limit = 500, offset = offset) |> req_perform() |> resp_body_json()\n out <- c(out, page$items); offset <- offset + length(page$items)\n if (length(page$items) == 0 || offset >= page$total) break\n }\n out\n}\nprojets <- fetch_all("projects", sector = "Utilities")\ndf <- dplyr::bind_rows(lapply(projets, \\(p) as.data.frame(p[c("ticker","project_type","project_name","status","total_amount_usd","first_seen")])))` } },108 { t: "Export CSV direct", d: "Le plus simple pour Excel, R ou pandas : un seul appel, jusqu'à 50 000 lignes.", ep: "/v1/projects/export.csv", v: { type: "data_center" }, code: {109 curl: `curl 'https://www.pdb-api.co/v1/projects/export.csv?type=data_center' -H 'X-API-Key: ${KEY_PH}' -o data_centers.csv`,110 py: `import io, pandas as pd\ncsv_text = requests.get(f"{BASE}/projects/export.csv", headers=H, params={"type": "data_center"}).text\ndf = pd.read_csv(io.StringIO(csv_text))\ndf.head()`,111 js: `const csv = await (await fetch(\`\${BASE}/projects/export.csv?type=data_center\`, { headers: H })).text();\nrequire("fs").writeFileSync("data_centers.csv", csv);`,112 r: `csv <- request(paste0(BASE, "/projects/export.csv")) |> req_headers(\`X-API-Key\` = KEY) |>\n req_url_query(type = "data_center") |> req_perform() |> resp_body_string()\ndf <- read.csv(text = csv)` } },113 { t: "Chronologie d'un projet", d: "Suivre un projet de son annonce (8-K) à ses mentions ultérieures (10-K, 10-Q).", ep: "/v1/projects/{project_id}", v: { project_id: "46fe2966ba0cbf6228807faca0645b54" }, code: {114 curl: `PID=$(curl -s 'https://www.pdb-api.co/v1/projects?ticker=TSLA&q=energy%20storage&limit=1' -H 'X-API-Key: ${KEY_PH}' | python3 -c 'import json,sys; print(json.load(sys.stdin)["items"][0]["project_id"])')\ncurl "https://www.pdb-api.co/v1/projects/$PID" -H 'X-API-Key: ${KEY_PH}'`,115 py: `pid = requests.get(f"{BASE}/projects", headers=H, params={"ticker": "TSLA", "q": "energy storage", "limit": 1}).json()["items"][0]["project_id"]\nfiche = requests.get(f"{BASE}/projects/{pid}", headers=H).json()\nprint(fiche["project"]["project_name"], fiche["project"]["status"])\nfor t in fiche["timeline"]:\n print(t["filing_date"], t["form_type"], t["status"], t["amount_usd"], "—", t["snippet"][:80])\nprint("similaires :", [s["project_name"] for s in fiche["similar"][:5]])`,116 js: `const first = (await (await fetch(\`\${BASE}/projects?ticker=TSLA&q=energy%20storage&limit=1\`, { headers: H })).json()).items[0];\nconst fiche = await (await fetch(\`\${BASE}/projects/\${first.project_id}\`, { headers: H })).json();\nfiche.timeline.forEach(t => console.log(t.filing_date, t.form_type, t.status, t.amount_usd));`,117 r: `pid <- (request(paste0(BASE, "/projects")) |> req_headers(\`X-API-Key\` = KEY) |>\n req_url_query(ticker = "TSLA", q = "energy storage", limit = 1) |> req_perform() |> resp_body_json())$items[[1]]$project_id\nfiche <- request(paste0(BASE, "/projects/", pid)) |> req_headers(\`X-API-Key\` = KEY) |> req_perform() |> resp_body_json()\nsapply(fiche$timeline, \\(t) paste(t$filing_date, t$form_type, t$status))` } },118 { t: "Profil d'une entreprise", d: "Répartitions par type, année et formulaire, plus le portefeuille complet.", ep: "/v1/companies/{ident}", v: { ident: "DUK" }, code: {119 curl: `curl https://www.pdb-api.co/v1/companies/DUK -H 'X-API-Key: ${KEY_PH}'`,120 py: `duk = requests.get(f"{BASE}/companies/DUK", headers=H).json()\nprint(duk["company"])\npd.DataFrame(duk["by_type"]).set_index("label")["n"].plot.barh(title="Duke Energy — projets par type")`,121 js: `const duk = await (await fetch(\`\${BASE}/companies/DUK\`, { headers: H })).json();\nconsole.table(duk.by_type.map(x => ({ type: x.label, n: x.n })));`,122 r: `duk <- request(paste0(BASE, "/companies/DUK")) |> req_headers(\`X-API-Key\` = KEY) |> req_perform() |> resp_body_json()\nbarplot(sapply(duk$by_type, \\(x) x$n), names.arg = sapply(duk$by_type, \\(x) x$project_type), las = 2)` } },123 { t: "Recherche sémantique", d: "Décrire un projet en langage naturel et obtenir les projets les plus proches.", ep: "/v1/search/semantic", v: { q: "battery cell factory", k: 10 }, code: {124 curl: `curl 'https://www.pdb-api.co/v1/search/semantic?q=battery+cell+factory&k=10' -H 'X-API-Key: ${KEY_PH}'`,125 py: `hits = requests.get(f"{BASE}/search/semantic", headers=H, params={"q": "battery cell factory", "k": 10}).json()["items"]\nfor h in hits:\n print(f"{h['score']:.3f} {h['ticker']:6s} {h['project_name']}")`,126 js: `const { items } = await (await fetch(\`\${BASE}/search/semantic?\${new URLSearchParams({ q: "battery cell factory", k: 10 })}\`, { headers: H })).json();\nitems.forEach(h => console.log(h.score.toFixed(3), h.ticker, h.project_name));`,127 r: `hits <- request(paste0(BASE, "/search/semantic")) |> req_headers(\`X-API-Key\` = KEY) |>\n req_url_query(q = "battery cell factory", k = 10) |> req_perform() |> resp_body_json()\nsapply(hits$items, \\(h) sprintf("%.3f %s %s", h$score, h$ticker, h$project_name))` } },128 { t: "SQL : agrégations libres", d: "Quand les filtres ne suffisent pas : une requête DuckDB de lecture, résultat en colonnes/lignes.", ep: "/v1/sql", v: { sql: "select year(filing_date) as yr, project_type, count(*) n\nfrom project_mentions\nwhere project_type in ('ai_initiative','data_center','cloud_migration')\ngroup by 1,2 order by 1,2", limit: 200 }, code: {129 curl: `curl -X POST https://www.pdb-api.co/v1/sql -H 'X-API-Key: ${KEY_PH}' -H 'Content-Type: application/json' \\\n -d '{"sql":"select year(filing_date) as yr, project_type, count(*) n from project_mentions where project_type in (\\'ai_initiative\\',\\'data_center\\') group by 1,2 order by 1,2","limit":200}'`,130 py: `sql = """\nselect year(filing_date) as yr, project_type, count(*) n\nfrom project_mentions\nwhere project_type in ('ai_initiative','data_center','cloud_migration')\ngroup by 1,2 order by 1,2\n"""\nres = requests.post(f"{BASE}/sql", headers=H, json={"sql": sql, "limit": 500}).json()\ndf = pd.DataFrame(res["rows"], columns=res["columns"])\ndf.pivot(index="yr", columns="project_type", values="n").plot(title="Mentions par année")`,131 js: `const res = await (await fetch(\`\${BASE}/sql\`, { method: "POST", headers: { ...H, "Content-Type": "application/json" },\n body: JSON.stringify({ sql: "select project_type, count(*) n from projects group by 1 order by n desc", limit: 25 }) })).json();\nconsole.table(res.rows.map(r => Object.fromEntries(res.columns.map((c, i) => [c, r[i]]))));`,132 r: `res <- request(paste0(BASE, "/sql")) |> req_headers(\`X-API-Key\` = KEY) |>\n req_body_json(list(sql = "select project_type, count(*) n from projects group by 1 order by n desc", limit = 25)) |>\n req_perform() |> resp_body_json()\ndf <- as.data.frame(do.call(rbind, lapply(res$rows, unlist))); names(df) <- unlist(res$columns); df` } },133 { t: "Graphe : qui construit au Texas avec des batteries ?", d: "Partir d'un nœud Lieu, remonter aux projets, croiser avec une technologie.", ep: "/v1/graph/node/{node_id}", v: { node_id: "L:texas", limit: 100 }, code: {134 curl: `curl 'https://www.pdb-api.co/v1/graph/node/L:texas?limit=100' -H 'X-API-Key: ${KEY_PH}'`,135 py: `node = requests.get(f"{BASE}/graph/node/L:texas", headers=H, params={"limit": 500}).json()\nprojets_texas = {e["neighbor_id"][2:] for e in node["edges"] if e["neighbor_type"] == "Project"}\nbatt = requests.get(f"{BASE}/projects", headers=H, params={"location": "texas", "tech": "batter", "limit": 100}).json()["items"]\nprint(len(projets_texas), "projets au Texas ;", len(batt), "avec batteries :", [(p["ticker"], p["project_name"]) for p in batt][:5])`,136 js: `const node = await (await fetch(\`\${BASE}/graph/node/L:texas?limit=500\`, { headers: H })).json();\nconsole.log(node.degree, "projets rattachés à texas");`,137 r: `node <- request(paste0(BASE, "/graph/node/L:texas")) |> req_headers(\`X-API-Key\` = KEY) |> req_url_query(limit = 500) |> req_perform() |> resp_body_json()\nnode$degree` } },138 { t: "Gérer les erreurs et la limite de débit", d: "401 clé invalide, 404 introuvable, 408 SQL trop long, 429 trop de requêtes (240/min), 501 sémantique indisponible.", ep: "/v1/health", v: {}, code: {139 curl: `curl -s -o /dev/null -w '%{http_code}\\n' https://www.pdb-api.co/v1/stats -H 'X-API-Key: mauvaise' # 401`,140 py: `import time\n\ndef get(path, **params):\n for attempt in range(4):\n r = requests.get(f"{BASE}/{path}", headers=H, params=params, timeout=60)\n if r.status_code == 429: # limite de débit : attendre puis réessayer\n time.sleep(2 * (attempt + 1)); continue\n if r.status_code >= 400:\n raise RuntimeError(f"HTTP {r.status_code}: {r.json().get('detail')}")\n return r.json()\n raise RuntimeError("trop de tentatives")`,141 js: `async function get(path, params = {}) {\n for (let i = 0; i < 4; i++) {\n const r = await fetch(\`\${BASE}/\${path}?\${new URLSearchParams(params)}\`, { headers: H });\n if (r.status === 429) { await new Promise(s => setTimeout(s, 2000 * (i + 1))); continue; }\n if (!r.ok) throw new Error(\`HTTP \${r.status}: \${(await r.json()).detail}\`);\n return r.json();\n }\n}`,142 r: `resp <- request(paste0(BASE, "/stats")) |> req_headers(\`X-API-Key\` = KEY) |>\n req_retry(max_tries = 4, is_transient = \\(r) resp_status(r) == 429) |> req_perform()` } },143 ];144145 /* ------------------------------------------------------------------ référence : dictionnaire des champs */146 const FIELDS = {147 projects: [["project_id", "MD5 de cik|type|ancre ; identifiant stable"], ["cik / ticker / company_name / sector", "entreprise (GICS)"], ["project_type", "un des 21 types (voir /v1/taxonomy)"], ["project_name / description", "nom et description de la mention la plus confiante"], ["canonical_location", "première localisation (minuscules) ayant servi d'ancre"], ["technologies[]", "union des technologies citées"], ["status", "statut du dépôt le plus récent : planned, in_progress, completed, mentioned"], ["total_amount_usd", "montant MAXIMAL divulgué parmi les mentions (pas une somme)"], ["n_mentions / n_filings", "nombre de mentions et de dépôts distincts"], ["first_seen / last_seen", "dates de dépôt extrêmes (fiables pour dater)"], ["first_year / last_year", "années citées dans le texte (peuvent être projetées)"], ["avg_confidence", "confiance moyenne du modèle (0,45–1)"]],148 mentions: [["mention_id", "SHA-1[0:20] de accession|section|type|ancre"], ["accession_number / section_id / section_name", "dépôt et section source"], ["form_type / filing_date / fiscal_year", "10-K, 10-Q ou 8-K ; date de dépôt (fiscal_year non renseigné)"], ["project_name / project_type / description / objective", "extraction du LLM"], ["amount_usd / amount_raw", "montant en US$ et chaîne d'origine"], ["locations[] / technologies[] / partners[] / suppliers[]", "listes extraites (≤ 8 éléments)"], ["benefits[] / risks[] / years[]", "bénéfices, risques, années citées"], ["status / confidence / backend", "statut, confiance 0–1, backend (llm)"], ["project_id", "projet résolu auquel la mention est rattachée (ajouté par l'API)"]],149 timeline: [["project_id / accession_number / filing_date / form_type", "un point par mention datée"], ["status / amount_usd / confidence / snippet", "état à cette date, montant, confiance, extrait de 300 caractères"]],150 };151 const ERRORS = [["200", "OK"], ["400", "Requête SQL refusée (mot-clé interdit, syntaxe) ou paramètre invalide"], ["401", "Clé absente ou invalide (en-tête X-API-Key)"], ["403", "Route /app réservée à l'interface web"], ["404", "Projet, mention, section, entreprise ou nœud introuvable"], ["408", "Requête SQL interrompue après 20 s"], ["422", "Paramètre mal typé (voir detail)"], ["429", "Plus de 240 requêtes par minute pour votre adresse"], ["501", "Recherche sémantique indisponible sur le serveur"], ["503", "Service ou clé non configurés"]];152153 /* ------------------------------------------------------------------ rendu */154 let ctx, meta = { types: [], sectors: [] }, cur = 7, tab = "start", exampleIds = {};155 function render(c) {156 ctx = c; const view = c.view;157 const hp = new URLSearchParams((location.hash.split("?")[1] || ""));158 if (hp.get("tab")) tab = hp.get("tab");159 if (hp.get("ep")) { const i = ENDPOINTS.findIndex((e) => e.p === hp.get("ep")); if (i >= 0) { cur = i; tab = "play"; } }160 view.innerHTML = `<div class="page-head"><div><h1>API & playground</h1><p>API REST JSON de la base SPID. Base : <span class="mono">${esc(location.origin)}/v1</span> · authentification par en-tête <span class="mono">X-API-Key</span> · 240 requêtes/min · pagination <span class="mono">limit/offset</span> (≤ 500).</p></div>161 <div class="langsel">Langage des exemples : ${LANGS.map(([k, l]) => `<button class="${lang() === k ? "active" : ""}" data-lang="${k}">${l}</button>`).join("")}</div></div>162 <div class="tabs big" id="api-tabs">${[["start", "Démarrage rapide"], ["play", "Playground"], ["recipes", "Recettes"], ["ref", "Référence"], ["sdk", "SDK Python"]].map(([k, l]) => `<button class="${tab === k ? "active" : ""}" data-tab="${k}">${l}</button>`).join("")}</div>163 <div id="api-body"></div>`;164 view.querySelectorAll("[data-lang]").forEach((b) => b.addEventListener("click", () => { localStorage.setItem("pdb_lang", b.dataset.lang); render(ctx); }));165 view.querySelector("#api-tabs").addEventListener("click", (e) => { if (e.target.dataset.tab) { tab = e.target.dataset.tab; render(ctx); } });166 Promise.all([ctx.api("/taxonomy"), ctx.api("/sectors")]).then(([t, s]) => { meta = { types: t, sectors: s }; if (tab === "play") renderPlay(); }).catch(() => { });167 ({ start: renderStart, play: renderPlay, recipes: renderRecipes, ref: renderRef, sdk: renderSdk }[tab] || renderStart)();168 resolveExampleIds();169 }170 async function resolveExampleIds() {171 if (exampleIds.TSLA_ENERGY) return;172 try {173 const p = await ctx.api("/projects", { ticker: "TSLA", q: "energy storage", limit: 1 }); exampleIds.TSLA_ENERGY = p.items[0]?.project_id;174 const m = await ctx.api("/mentions", { form: "10-K", min_confidence: 0.95, limit: 1 }); exampleIds.MENTION_10K = m.items[0]?.mention_id; exampleIds.SECTION_10K = m.items[0]?.section_id;175 } catch { }176 }177 const sub = (v) => exampleIds[v] || v;178179 const keyBox = () => `<div class="keybox"><label><b>Votre clé d'API</b><input class="inp" id="key" type="password" value="${esc(key())}" placeholder="saisir la clé (reste dans ce navigateur)" autocomplete="off"></label>180 <button class="btn btn-ghost btn-sm" id="key-show">Afficher</button><button class="btn btn-sm" id="key-test">Tester la clé</button><span id="key-st" class="small muted"></span>181 <div class="small muted" style="flex-basis:100%">La clé n'est pas publiée sur ce site. Elle est fournie par l'équipe UQO (<a href="mailto:simon-pierre.boucher@uqo.ca">simon-pierre.boucher@uqo.ca</a>). Elle est conservée en localStorage et envoyée uniquement à cette API.</div></div>`;182 function bindKey(root) {183 const inp = root.querySelector("#key"); if (!inp) return;184 inp.addEventListener("input", () => { localStorage.setItem("pdb_key", inp.value); root.querySelectorAll("pre.code[data-req]").forEach(() => { }); });185 root.querySelector("#key-show").addEventListener("click", () => (inp.type = inp.type === "password" ? "text" : "password"));186 root.querySelector("#key-test").addEventListener("click", async () => {187 const st = root.querySelector("#key-st"); st.textContent = "…";188 const r = await fetch("/v1/taxonomy", { headers: { "X-API-Key": key() } });189 st.innerHTML = r.ok ? `<span class="ok" style="padding:3px 8px">✓ clé valide (HTTP 200)</span>` : `<span class="err" style="padding:3px 8px;display:inline-block;margin:0">✗ HTTP ${r.status} — ${esc((await r.json()).detail || "")}</span>`;190 });191 }192 const codeBlock = (code, id = "") => `<div class="codewrap"><button class="copy" data-copy>Copier</button><pre class="code" ${id ? `id="${id}"` : ""}>${esc(code)}</pre></div>`;193 function bindCopy(root) { root.querySelectorAll("[data-copy]").forEach((b) => b.addEventListener("click", () => { navigator.clipboard.writeText(b.nextElementSibling.textContent); b.textContent = "Copié ✓"; setTimeout(() => (b.textContent = "Copier"), 1500); })); }194195 /* ---- Démarrage rapide */196 function renderStart() {197 const l = lang(); const body = ctx.view.querySelector("#api-body");198 const r1 = buildRequest(ENDPOINTS[0], {}); const r2 = buildRequest(ENDPOINTS[1], {}); const r3 = buildRequest(ENDPOINTS.find((e) => e.p === "/v1/projects"), { type: "ai_initiative", year_from: 2024, sort: "last_seen", limit: 5 });199 const install = { curl: "# curl est déjà installé sur macOS / Linux / Windows 10+", py: "pip install requests pandas # ou : téléchargez pdb_client.py (onglet SDK Python)", js: "# Node ≥ 18 (fetch natif) ou navigateur — aucune dépendance", r: 'install.packages(c("httr2", "dplyr"))' }[l];200 body.innerHTML = `<div class="grid g2">201 <div class="card" style="grid-column:1/-1">${keyBox()}</div>202 <div class="card"><h3><span class="step">1</span> Installer</h3>${codeBlock(install)}</div>203 <div class="card"><h3><span class="step">2</span> Vérifier le service (sans clé)</h3>${codeBlock(snippet(l, r1, KEY_PH, true))}</div>204 <div class="card"><h3><span class="step">3</span> Premier appel authentifié</h3>${codeBlock(snippet(l, r2, key() || KEY_PH))}<p class="small muted">Réponse : <span class="mono">{overview:{projects, mentions, companies…}, by_type:[…], by_sector:[…], mentions_by_year:[…]}</span></p></div>205 <div class="card"><h3><span class="step">4</span> Filtrer des projets</h3>${codeBlock(snippet(l, r3, key() || KEY_PH))}<p class="small muted">Réponse paginée : <span class="mono">{total, limit, offset, items:[{project_id, ticker, project_name, project_type, status, total_amount_usd, …}]}</span>. Pour tout récupérer, incrémentez <span class="mono">offset</span> de <span class="mono">limit</span> jusqu'à <span class="mono">total</span> (recette « pagination »).</p></div>206 <div class="card" style="grid-column:1/-1"><h3>Modèle de données en 30 secondes</h3><div class="grid g3">207 <div><b>Projet</b> (19 227) — unité principale. Un projet = mentions fusionnées par entreprise × type × ancre (lieu ou premier mot du nom). <span class="mono">/v1/projects</span>, <span class="mono">/v1/projects/{id}</span>.</div>208 <div><b>Mention</b> (39 930) — l'extraction brute : un projet cité dans une section d'un filing, avec date, formulaire, montant, confiance. <span class="mono">/v1/mentions</span>. La chronologie d'un projet = ses mentions triées par date.</div>209 <div><b>Graphe</b> (32 226 nœuds) et <b>embeddings</b> (384-d) — navigation par lieu/technologie/partenaire et recherche par le sens. <span class="mono">/v1/graph/*</span>, <span class="mono">/v1/search/semantic</span>, <span class="mono">/v1/projects/{id}/similar</span>.</div></div>210 <p class="small muted" style="margin-top:8px">Point d'attention : <span class="mono">total_amount_usd</span> est le montant <i>maximal</i> divulgué pour le projet, auto-déclaré et hétérogène ; <span class="mono">first_seen/last_seen</span> datent l'observation, <span class="mono">first_year/last_year</span> sont les années citées dans le texte (parfois projetées).</p></div>211 <div class="card" style="grid-column:1/-1"><h3>Codes de réponse</h3><table class="ref"><tbody>${ERRORS.map(([c, d]) => `<tr><td class="mono"><b>${c}</b></td><td>${esc(d)}</td></tr>`).join("")}</tbody></table></div>212 </div>`;213 bindKey(body); bindCopy(body);214 }215216 /* ---- Playground */217 const HIST_KEY = "pdb_hist";218 function renderPlay() {219 const body = ctx.view.querySelector("#api-body"); const e = ENDPOINTS[cur];220 body.innerHTML = `<div class="api-layout"><div class="card"><h3>Points d'accès</h3><ul class="endpoints">${GROUPS.map((g) => `<li class="grp">${esc(g)}</li>` + ENDPOINTS.map((x, i) => x.g === g ? `<li data-i="${i}" class="${i === cur ? "active" : ""}"><span class="m ${x.m === "POST" ? "post" : ""}">${x.m}</span><span class="mono">${esc(x.p.replace("/v1", ""))}</span></li>` : "").join("")).join("")}</ul>221 <h3 style="margin-top:14px">Historique</h3><div id="hist" class="hist"></div></div>222 <div><div class="card">${keyBox()}</div><div class="card" id="pg"></div></div></div>`;223 body.querySelectorAll(".endpoints li[data-i]").forEach((li) => li.addEventListener("click", () => { cur = +li.dataset.i; renderPlay(); }));224 bindKey(body); renderHist(body);225 const pg = body.querySelector("#pg");226 const ex0 = e.ex?.[0]?.v || {};227 pg.innerHTML = `<h3><span class="m ${e.m === "POST" ? "post" : ""}" style="font-family:var(--mono);color:${e.m === "POST" ? "var(--orange)" : "var(--vert)"}">${e.m}</span> <span class="mono">${esc(e.p)}</span>${e.key === false ? ' <span class="pill pill-vert">sans clé</span>' : ""}</h3><p class="muted">${esc(e.d)}</p>228 ${e.ex?.length ? `<div class="examples"><span class="small muted" style="align-self:center">Exemples :</span>${e.ex.map((x, i) => `<button data-ex="${i}">${esc(x.l)}</button>`).join("")}</div>` : ""}229 <div class="params">${e.params.map((p) => p.textarea ? `<label style="grid-column:1/-1"><b>${esc(p.name)}</b> <span>${esc(p.desc)}</span><textarea class="sql" data-p="${esc(p.name)}" rows="5">${esc(sub(ex0[p.name] ?? ""))}</textarea></label>` :230 `<label><b>${esc(p.name)}${p.path ? " *" : ""}</b><span>${esc(p.desc)}</span>${p.enum ? `<input class="inp" list="dl-${p.enum}" data-p="${esc(p.name)}" value="${esc(sub(ex0[p.name] ?? ""))}">` : `<input class="inp" type="${p.type === "number" ? "text" : "text"}" data-p="${esc(p.name)}" value="${esc(sub(ex0[p.name] ?? ""))}" placeholder="${esc(p.desc.slice(0, 40))}">`}</label>`).join("") || `<span class="muted small">Aucun paramètre.</span>`}</div>231 <datalist id="dl-types">${meta.types.map((t) => `<option value="${t.project_type}">${esc(t.label)}</option>`).join("")}</datalist><datalist id="dl-sectors">${meta.sectors.map((s) => `<option value="${esc(s.sector)}">`).join("")}</datalist>232 <div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap"><button class="btn" id="send">▶ Envoyer</button><span class="muted small" id="st"></span><span class="sp" style="flex:1"></span><a id="explore" class="btn btn-ghost btn-sm" hidden>Ouvrir dans l'explorateur</a></div>233 <div class="tabs" style="margin-top:14px" id="reqtabs">${LANGS.map(([k, l]) => `<button class="${lang() === k ? "active" : ""}" data-lang="${k}">${l}</button>`).join("")}</div>234 ${codeBlock("", "req")}235 <div style="display:flex;gap:10px;align-items:center;margin-top:14px"><h3 style="margin:0">Réponse</h3><div class="tabs" style="margin:0;border:0" id="resptabs"><button class="active" data-v="json">JSON</button><button data-v="table">Tableau</button></div><span class="sp" style="flex:1"></span><button class="btn btn-ghost btn-sm" id="dl" hidden>Télécharger</button></div>236 <div id="resp"><pre class="code">—</pre></div>`;237 const vals = () => { const o = {}; pg.querySelectorAll("[data-p]").forEach((i) => { if (i.value !== "") o[i.dataset.p] = i.value; }); return o; };238 const showReq = () => { const r = buildRequest(e, vals()); pg.querySelector("#req").textContent = snippet(lang(), r, key() || KEY_PH); const ex = exploreLink(e, vals(), r); const a = pg.querySelector("#explore"); a.hidden = !ex; if (ex) a.href = ex; };239 pg.querySelectorAll("[data-p]").forEach((i) => i.addEventListener("input", showReq)); showReq();240 pg.querySelectorAll("[data-ex]").forEach((b) => b.addEventListener("click", () => { const v = e.ex[b.dataset.ex].v; pg.querySelectorAll("[data-p]").forEach((i) => (i.value = sub(v[i.dataset.p] ?? ""))); showReq(); send(); }));241 pg.querySelector("#reqtabs").addEventListener("click", (ev) => { if (ev.target.dataset.lang) { localStorage.setItem("pdb_lang", ev.target.dataset.lang); pg.querySelectorAll("#reqtabs button").forEach((b) => b.classList.toggle("active", b.dataset.lang === ev.target.dataset.lang)); ctx.view.querySelectorAll(".langsel button").forEach((b) => b.classList.toggle("active", b.dataset.lang === ev.target.dataset.lang)); showReq(); } });242 bindCopy(pg);243 let last = null, lastRaw = "";244 const renderResp = () => {245 const v = pg.querySelector("#resptabs .active").dataset.v; const box = pg.querySelector("#resp");246 if (!last) { box.innerHTML = `<pre class="code">${esc(lastRaw.slice(0, 60000))}</pre>`; return; }247 if (v === "table") { const rows = tabular(last); box.innerHTML = rows ? rows : `<div class="warn">Réponse non tabulaire — voir JSON.</div>`; }248 else { const txt = JSON.stringify(last, null, 2); box.innerHTML = `<div class="codewrap"><button class="copy" data-copy>Copier</button><pre class="code json">${hl(txt.length > 80000 ? txt.slice(0, 80000) + "\n… (tronqué)" : txt)}</pre></div>`; bindCopy(box); }249 };250 pg.querySelector("#resptabs").addEventListener("click", (ev) => { if (ev.target.dataset.v) { pg.querySelectorAll("#resptabs button").forEach((b) => b.classList.toggle("active", b === ev.target)); renderResp(); } });251 async function send() {252 const r = buildRequest(e, vals()); const st = pg.querySelector("#st"); st.textContent = "…"; const t0 = performance.now();253 try {254 const resp = await fetch(r.url, { method: r.method, headers: { ...(e.key === false ? {} : { "X-API-Key": key() }), ...(r.body ? { "Content-Type": "application/json" } : {}) }, body: r.body ? JSON.stringify(r.body) : undefined });255 lastRaw = await resp.text(); last = null; try { last = JSON.parse(lastRaw); } catch { }256 const ms = Math.round(performance.now() - t0);257 st.innerHTML = `<span class="${resp.ok ? "pill pill-vert" : "pill pill-rouge"}">HTTP ${resp.status}</span> ${ms} ms · ${(lastRaw.length / 1024).toFixed(1)} ko${last?.total != null ? ` · total ${fmtN(last.total)}` : ""}`;258 const dl = pg.querySelector("#dl"); dl.hidden = false; dl.onclick = () => { const a = document.createElement("a"); a.href = URL.createObjectURL(new Blob([lastRaw], { type: e.raw ? "text/csv" : "application/json" })); a.download = e.raw ? "export.csv" : "response.json"; a.click(); };259 pushHist({ ep: e.p, vals: vals(), status: resp.status, ms, t: Date.now() }); renderHist(body); renderResp();260 } catch (err) { st.textContent = ""; pg.querySelector("#resp").innerHTML = `<div class="err">${esc(String(err))}</div>`; }261 }262 pg.querySelector("#send").addEventListener("click", send);263 }264 function exploreLink(e, vals, r) {265 if (e.p === "/v1/projects" || e.p === "/v1/projects/export.csv") return "#/projects?" + qs(Object.fromEntries(Object.entries(r.query).filter(([k]) => !["limit", "offset"].includes(k))));266 if (e.p.startsWith("/v1/projects/{") && vals.project_id) return "#/project/" + encodeURIComponent(vals.project_id);267 if (e.p === "/v1/companies/{ident}" && vals.ident) return "#/company/" + encodeURIComponent(vals.ident);268 if (e.p === "/v1/mentions") return "#/mentions?" + qs(r.query);269 if (e.p === "/v1/mentions/{mention_id}" && vals.mention_id) return "#/mention/" + encodeURIComponent(vals.mention_id);270 if (e.p === "/v1/graph/node/{node_id}" && vals.node_id) return "#/graph?node=" + encodeURIComponent(vals.node_id);271 if (e.p === "/v1/graph/search" && vals.q) return "#/graph?q=" + encodeURIComponent(vals.q);272 if (e.p === "/v1/search/semantic" && vals.q) return "#/semantic?q=" + encodeURIComponent(vals.q);273 if (e.p === "/v1/sql") return "#/sql";274 return null;275 }276 function tabular(d) {277 let rows = Array.isArray(d) ? d : d?.items || d?.edges || d?.projects || d?.timeline || null;278 if (d?.columns && d?.rows) rows = d.rows.map((r) => Object.fromEntries(d.columns.map((c, i) => [c, r[i]])));279 if (!rows || !rows.length || typeof rows[0] !== "object") return null;280 const cols = Object.keys(rows[0]).filter((c) => !["description", "props", "neighbor_props", "vector"].includes(c)).slice(0, 14);281 return `<div class="table-wrap" style="max-height:60vh"><table><thead><tr>${cols.map((c) => `<th>${esc(c)}</th>`).join("")}</tr></thead><tbody>${rows.slice(0, 500).map((r) => `<tr>${cols.map((c) => { const v = r[c]; return `<td class="${typeof v === "number" ? "num" : ""}">${esc(Array.isArray(v) ? v.join(" | ") : v && typeof v === "object" ? JSON.stringify(v) : v)}</td>`; }).join("")}</tr>`).join("")}</tbody></table></div><div class="small muted" style="margin-top:6px">${fmtN(rows.length)} lignes${rows.length > 500 ? " (500 affichées)" : ""}</div>`;282 }283 const hl = (txt) => esc(txt).replace(/("(?:\\.|[^"\\])*")(\s*:)?/g, (m, s, c) => c ? `<span class="k">${s}</span>${c}` : `<span class="s">${s}</span>`).replace(/\b(-?\d+(?:\.\d+)?(?:e[+-]?\d+)?)\b/g, `<span class="n">$1</span>`).replace(/\b(true|false|null)\b/g, `<span class="b">$1</span>`);284 function pushHist(h) { const a = JSON.parse(localStorage.getItem(HIST_KEY) || "[]"); a.unshift(h); localStorage.setItem(HIST_KEY, JSON.stringify(a.slice(0, 12))); }285 function renderHist(root) {286 const a = JSON.parse(localStorage.getItem(HIST_KEY) || "[]"); const box = root.querySelector("#hist"); if (!box) return;287 box.innerHTML = a.length ? a.map((h, i) => `<div class="hist-it" data-h="${i}"><span class="pill ${h.status < 400 ? "pill-vert" : "pill-rouge"}">${h.status}</span> <span class="mono small">${esc(h.ep.replace("/v1", ""))}</span><div class="small muted">${esc(Object.entries(h.vals).map(([k, v]) => `${k}=${String(v).slice(0, 18)}`).join(" ")) || "—"} · ${h.ms} ms</div></div>`).join("") + `<button class="btn btn-ghost btn-sm" id="hist-clear">Effacer</button>` : `<span class="small muted">Aucune requête envoyée.</span>`;288 box.querySelectorAll(".hist-it").forEach((d) => d.addEventListener("click", () => { const h = a[d.dataset.h]; cur = ENDPOINTS.findIndex((e) => e.p === h.ep); renderPlay(); const pg = root.querySelector("#pg"); pg.querySelectorAll("[data-p]").forEach((i) => (i.value = h.vals[i.dataset.p] ?? "")); pg.querySelector("[data-p]")?.dispatchEvent(new Event("input")); }));289 box.querySelector("#hist-clear")?.addEventListener("click", () => { localStorage.removeItem(HIST_KEY); renderHist(root); });290 }291292 /* ---- Recettes */293 function renderRecipes() {294 const l = lang(); const body = ctx.view.querySelector("#api-body");295 const pre = { py: `# Préambule commun aux recettes Python\nimport requests, pandas as pd\nBASE = "https://www.pdb-api.co/v1"\nH = {"X-API-Key": "${key() || KEY_PH}"}`, js: `// Préambule commun (Node ≥ 18 ou navigateur)\nconst BASE = "https://www.pdb-api.co/v1";\nconst H = { "X-API-Key": "${key() || KEY_PH}" };`, r: `# Préambule commun aux recettes R\nlibrary(httr2); library(dplyr)\nBASE <- "https://www.pdb-api.co/v1"; KEY <- "${key() || KEY_PH}"`, curl: `# Remplacez ${KEY_PH} par votre clé. Ajoutez « | python3 -m json.tool » pour lire le JSON.` }[l];296 body.innerHTML = `<div class="card">${codeBlock(pre)}</div><div class="grid g2">${RECIPES.map((r, i) => `<div class="card recipe"><h3>${i + 1}. ${esc(r.t)}</h3><p class="muted small">${esc(r.d)}</p>${codeBlock(r.code[l].replace(new RegExp(KEY_PH, "g"), key() || KEY_PH))}<div style="margin-top:8px"><button class="btn btn-ghost btn-sm" data-try="${i}">Essayer dans le playground</button></div></div>`).join("")}</div>`;297 bindCopy(body);298 body.querySelectorAll("[data-try]").forEach((b) => b.addEventListener("click", () => { const r = RECIPES[b.dataset.try]; cur = ENDPOINTS.findIndex((e) => e.p === r.ep); tab = "play"; render(ctx); const pg = ctx.view.querySelector("#pg"); pg.querySelectorAll("[data-p]").forEach((i) => (i.value = sub(r.v[i.dataset.p] ?? ""))); pg.querySelector("[data-p]")?.dispatchEvent(new Event("input")); window.scrollTo(0, 0); }));299 }300301 /* ---- Référence */302 function renderRef() {303 const body = ctx.view.querySelector("#api-body");304 body.innerHTML = `<div class="card"><h3>Conventions</h3><ul class="small"><li><b>Authentification</b> : en-tête <span class="mono">X-API-Key: <clé></span>, ou <span class="mono">Authorization: Bearer <clé></span>, ou paramètre <span class="mono">?api_key=</span> (déconseillé). <span class="mono">/v1/health</span> est libre.</li>305 <li><b>Pagination</b> : <span class="mono">limit</span> (≤ 500) et <span class="mono">offset</span> ; réponse <span class="mono">{total, limit, offset, items}</span>.</li>306 <li><b>Filtres texte</b> : insensibles à la casse, correspondance « contient » (sauf <span class="mono">ticker</span> et <span class="mono">cik</span>, exacts). Les paramètres <span class="mono">type</span>, <span class="mono">status</span>, <span class="mono">form</span> acceptent des listes séparées par des virgules.</li>307 <li><b>Montants</b> en US$ (<span class="mono">5e8</span> accepté). <b>Dates</b> au format ISO <span class="mono">AAAA-MM-JJ</span>. <b>Listes</b> renvoyées comme tableaux JSON ; en CSV, jointes par <span class="mono">|</span>.</li>308 <li><b>Limite</b> : 240 requêtes par minute et par adresse IP (429 au-delà). Bac à sable SQL : 20 s, 5 000 lignes, lecture seule, pas d'accès aux fichiers.</li>309 <li><b>OpenAPI</b> : <a href="/openapi.json" target="_blank">openapi.json</a> · <a href="/docs" target="_blank">Swagger UI</a> · <a href="/redoc" target="_blank">ReDoc</a>.</li></ul></div>310 <div class="card"><h3>Routes</h3><div class="table-wrap"><table class="ref"><thead><tr><th>Méthode</th><th>Route</th><th>Description</th><th>Paramètres</th></tr></thead><tbody>${ENDPOINTS.map((e) => `<tr><td class="mono"><b>${e.m}</b></td><td class="mono"><a href="#/api?tab=play&ep=${encodeURIComponent(e.p)}">${esc(e.p)}</a></td><td>${esc(e.d)}</td><td class="small">${e.params.map((p) => `<span class="mono">${esc(p.name)}</span>`).join(", ") || "—"}</td></tr>`).join("")}</tbody></table></div></div>311 ${Object.entries(FIELDS).map(([t, f]) => `<div class="card"><h3>Champs — ${t}</h3><table class="ref"><tbody>${f.map(([k, d]) => `<tr><td class="mono">${esc(k)}</td><td>${esc(d)}</td></tr>`).join("")}</tbody></table></div>`).join("")}312 <div class="card"><h3>Types de projets</h3><table class="ref"><thead><tr><th>project_type</th><th>Libellé</th><th class="num">Projets</th></tr></thead><tbody>${meta.types.map((t) => `<tr><td class="mono">${t.project_type}</td><td>${esc(t.label)}</td><td class="num">${fmtN(t.n)}</td></tr>`).join("") || "<tr><td colspan=3 class='muted'>chargement…</td></tr>"}</tbody></table></div>313 <div class="card"><h3>Codes de réponse</h3><table class="ref"><tbody>${ERRORS.map(([c, d]) => `<tr><td class="mono"><b>${c}</b></td><td>${esc(d)}</td></tr>`).join("")}</tbody></table></div>`;314 if (!meta.types.length) ctx.api("/taxonomy").then((t) => { meta.types = t; if (tab === "ref") renderRef(); }).catch(() => { });315 }316317 /* ---- SDK */318 function renderSdk() {319 const body = ctx.view.querySelector("#api-body"); const k = key() || KEY_PH;320 body.innerHTML = `<div class="grid g2">321 <div class="card" style="grid-column:1/-1"><h3>pdb_client.py — client Python sans dépendance</h3><p class="muted">Un fichier à déposer à côté de votre script ou notebook. Pagination automatique, réessais sur 429, conversion pandas/CSV, toutes les routes.</p>322 <a class="btn" href="/sdk/pdb_client.py" download>⬇ Télécharger pdb_client.py</a> <a class="btn btn-ghost" href="/sdk/pdb_client.py" target="_blank">Voir le source</a> <a class="btn btn-or" href="/sdk/pdb_api.R" download>⬇ Script R prêt à l'emploi (pdb_api.R)</a> <a class="btn btn-ghost" href="/sdk/pdb_api.m" download>⬇ MATLAB (pdb_api.m)</a> <a class="btn btn-ghost" href="/sdk/pdb_api.do" download>⬇ Stata (pdb_api.do)</a></div>323 <div class="card" style="grid-column:1/-1"><h3>R — pdb_api.R</h3><p class="muted">Fichier R complet (httr2 + dplyr) : fonctions <span class="mono">pdb_get()</span>, <span class="mono">pdb_sql()</span>, <span class="mono">pdb_all()</span> (pagination) et dix exemples exécutables (filtres, secteur entier, CSV, chronologie, entreprise, sémantique, SQL, panel entreprise × année, graphe). Testé avec R 4.6.</p>${codeBlock(`install.packages(c("httr2", "dplyr"))\nSys.setenv(PDB_API_KEY = "${k}")\nsource("pdb_api.R") # définit pdb_get / pdb_sql / pdb_all\n\ndc <- pdb_get("projects", type = "data_center", min_amount = 5e8, sort = "amount", limit = 10)$items\nutil <- pdb_all("projects", sector = "Utilities") # data.frame complet\nia <- pdb_sql("select year(filing_date) as yr, count(*) n from project_mentions where project_type = 'ai_initiative' group by 1 order by 1")`)}</div>324 <div class="card"><h3>MATLAB — pdb_api.m</h3><p class="muted">webread / webwrite natifs (R2018a+), fonctions <span class="mono">pdb_get</span>, <span class="mono">pdb_sql</span>, <span class="mono">pdb_all</span> et dix sections exécutables. Non testé sur MATLAB ici : signalez toute erreur.</p>${codeBlock(`BASE = "${location.origin}/v1"; KEY = "${k}";\nopts = weboptions("HeaderFields", ["X-API-Key", KEY], "ContentType", "json");\nr = webread(BASE + "/projects", "type", "data_center", "min_amount", "5e8", "sort", "amount", "limit", "10", opts);\ndc = struct2table(r.items);\n\nres = webwrite(BASE + "/sql", struct("sql", "select project_type, count(*) n from projects group by 1 order by n desc", "limit", 25), ...\n weboptions("HeaderFields", ["X-API-Key", KEY], "MediaType", "application/json"));\nT = cell2table(res.rows, "VariableNames", res.columns);`)}</div>325 <div class="card"><h3>Stata — pdb_api.do</h3><p class="muted">Stata ne peut pas envoyer d'en-tête HTTP : le .do définit <span class="mono">pdb_projects</span>, <span class="mono">pdb_mentions</span>, <span class="mono">pdb_sql</span> via Python intégré (Stata 16+) et une voie <span class="mono">shell curl</span> + <span class="mono">import delimited</span> pour toute version. Non testé sur Stata ici.</p>${codeBlock(`global PDB_KEY "${k}"\ndo pdb_api.do // définit les programmes\npdb_projects, filters(type=data_center min_amount=5e8 sort=amount)\npdb_sql "select year(filing_date) as yr, count(*) n from project_mentions where project_type = 'ai_initiative' group by 1 order by 1"\n\n* sans Python :\nshell curl -s "${location.origin}/v1/projects/export.csv?type=plant_construction" -H "X-API-Key: $PDB_KEY" -o usines.csv\nimport delimited using "usines.csv", clear varnames(1) encoding(utf8)`)}</div>326 <div class="card"><h3>Prise en main</h3>${codeBlock(`from pdb_client import PDB\n\npdb = PDB("${k}") # ou export PDB_API_KEY=...\nprint(pdb.health())\n\nov = pdb.stats()["overview"]\nprint(ov["projects"], "projets")\n\n# une page\npage = pdb.projects(type="data_center", min_amount=5e8, sort="amount", limit=10)\nfor p in page["items"]:\n print(p["ticker"], p["project_name"], p["total_amount_usd"])`)}</div>327 <div class="card"><h3>Tout un jeu de données en DataFrame</h3>${codeBlock(`# pagination automatique (500 par page)\nrows = pdb.iter_projects(sector="Utilities")\ndf = PDB.to_dataframe(rows)\nprint(df.shape)\n\n# mentions 8-K de 2025 sur l'IA\nm = PDB.to_dataframe(pdb.iter_mentions(type="ai_initiative", form="8-K", date_from="2025-01-01"))\nm.groupby("ticker").size().sort_values(ascending=False).head(10)\n\n# export CSV\nPDB.to_csv(pdb.iter_projects(type="plant_construction"), "usines.csv")`)}</div>328 <div class="card"><h3>Fiche, similaires, entreprise, graphe</h3>${codeBlock(`fiche = pdb.project(page["items"][0]["project_id"])\nfiche["project"], fiche["timeline"], fiche["mentions"], fiche["similar"]\n\npdb.similar(fiche["project"]["project_id"], k=5)\n\nduk = pdb.company("DUK") # ticker ou CIK\nduk["by_type"], duk["projects"][:3]\n\nnode = pdb.graph_node("L:texas", limit=500)\n[e["neighbor_label"] for e in node["edges"]][:10]`)}</div>329 <div class="card"><h3>Sémantique et SQL</h3>${codeBlock(`for h in pdb.semantic("battery cell factory", k=5):\n print(round(h["score"], 3), h["ticker"], h["project_name"])\n\nres = pdb.sql("""\n select year(filing_date) as yr, count(*) n\n from project_mentions where project_type = 'data_center'\n group by 1 order by 1\n""")\nimport pandas as pd\npd.DataFrame(res["rows"], columns=res["columns"]).set_index("yr").plot()\n\n# ou directement en enregistrements\npdb.sql_records("select ticker, count(*) n from projects group by 1 order by n desc limit 5")`)}</div>330 <div class="card"><h3>Dans un notebook Jupyter / Colab</h3>${codeBlock(`!curl -sO ${location.origin}/sdk/pdb_client.py\nimport os; os.environ["PDB_API_KEY"] = "${k}"\nfrom pdb_client import PDB\npdb = PDB()`)}</div>331 <div class="card"><h3>JavaScript / TypeScript (sans SDK)</h3>${codeBlock(`const BASE = "${location.origin}/v1";\nconst H = { "X-API-Key": process.env.PDB_API_KEY };\n\nexport async function pdb(path, params = {}) {\n const r = await fetch(\`\${BASE}/\${path}?\${new URLSearchParams(params)}\`, { headers: H });\n if (!r.ok) throw new Error(\`HTTP \${r.status}: \${(await r.json()).detail}\`);\n return r.json();\n}\nexport async function* iterate(path, params = {}) {\n for (let offset = 0; ; ) {\n const page = await pdb(path, { ...params, limit: 500, offset });\n yield* page.items; offset += page.items.length;\n if (!page.items.length || offset >= page.total) return;\n }\n}`)}</div>332 </div>`;333 bindCopy(body);334 }335336 return { render };337})();338