/* PDB — page API & playground (démarrage rapide, playground, recettes, référence, SDK). */ window.PDBPlayground = (() => { const LANGS = [["curl", "curl"], ["py", "Python"], ["js", "JavaScript"], ["r", "R"]]; const KEY_PH = "VOTRE_CLE"; const key = () => localStorage.getItem("pdb_key") || ""; const lang = () => localStorage.getItem("pdb_lang") || "py"; const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); const qs = (o) => Object.entries(o).filter(([, v]) => v !== "" && v != null).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&"); const fmtN = (n) => n == null ? "—" : Number(n).toLocaleString("fr-CA"); /* ------------------------------------------------------------------ catalogue des routes */ const P = (name, desc, opts = {}) => ({ name, desc, ...opts }); const PROJECT_FILTERS = [ P("q", "texte libre : nom, description, entreprise, ticker"), P("type", "project_type, liste séparée par des virgules", { enum: "types" }), P("sector", "secteur GICS (contient)", { enum: "sectors" }), P("status", "planned | in_progress | completed | mentioned (liste possible)"), P("ticker", "ticker exact"), P("cik", "CIK SEC (10 chiffres ou moins)"), P("location", "localisation canonique (contient)"), P("tech", "technologie (contient)"), P("partner", "partenaire cité dans une mention (contient)"), P("min_amount", "montant divulgué minimal, US$", { type: "number" }), P("max_amount", "montant maximal, US$", { type: "number" }), P("year_from", "dernière observation ≥ année", { type: "number" }), P("year_to", "première observation ≤ année", { type: "number" }), P("min_confidence", "confiance moyenne minimale (0–1)", { type: "number" }), P("has_amount", "true : seulement les projets avec montant"), ]; 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" })]; const ENDPOINTS = [ { 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: {} }] }, { 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: {} }] }, { 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: {} }] }, { 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: {} }] }, { 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" } }] }, { 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" } }] }, { 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" } }] }, { 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: [ { l: "Centres de données > 500 M$", v: { type: "data_center", min_amount: "5e8", sort: "amount", limit: 10 } }, { l: "IA depuis 2024", v: { type: "ai_initiative", year_from: 2024, sort: "last_seen", limit: 20 } }, { l: "Usines au Texas", v: { type: "plant_construction,manufacturing_expansion", location: "texas", limit: 20 } }, { l: "Batteries, terminées", v: { tech: "battery", status: "completed", limit: 20 } }, { l: "Tesla", v: { ticker: "TSLA", sort: "mentions", limit: 20 } }, { l: "Partenaire NVIDIA", v: { partner: "nvidia", limit: 20 } }, ] }, { 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" } }] }, { 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" } }] }, { 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 } }] }, { 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: [ { l: "8-K de NVIDIA", v: { ticker: "NVDA", form: "8-K", limit: 20 } }, { l: "IA en 2025, confiance ≥ 0,9", v: { type: "ai_initiative", date_from: "2025-01-01", min_confidence: 0.9, limit: 20 } }, { l: "Gros montants 10-K", v: { form: "10-K", min_amount: "1e10", sort: "amount", limit: 20 } }, ] }, { 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" } }] }, { 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" } }] }, { 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 } }] }, { 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" } }] }, { 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" } }] }, { 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:, P:, L:, T:, PR:.", 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 } }] }, { 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 } }] }, { g: "SQL", m: "GET", p: "/v1/schema", d: "Tables, colonnes et types de la base DuckDB.", params: [], ex: [{ l: "Schéma", v: {} }] }, { 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: [ { 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 } }, { 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 } }, { 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 } }, { 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 e.g))]; /* ------------------------------------------------------------------ générateurs de code */ function buildRequest(e, vals) { let path = e.p; const query = {}; let body = null; for (const prm of e.params) { const v = vals[prm.name]; if (v === undefined || v === "") continue; if (prm.path) path = path.replace(`{${prm.name}}`, encodeURIComponent(v)); else if (prm.body) (body ||= {})[prm.name] = prm.type === "number" ? Number(v) : v; else query[prm.name] = v; } path = path.replace(/\{[^}]+\}/g, ""); return { method: e.m, path, query, body, url: location.origin + path + (Object.keys(query).length ? "?" + qs(query) : "") }; } const pyDict = (o) => "{" + Object.entries(o).map(([k, v]) => `${JSON.stringify(k)}: ${typeof v === "number" ? v : JSON.stringify(v)}`).join(", ") + "}"; 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(", ") + ")"; function snippet(l, r, k = KEY_PH, nokey = false) { const base = location.origin + r.path; 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]; if (l === "curl") return r.method === "POST" ? `curl -X POST '${base}' \\\n -H 'X-API-Key: ${k}' -H 'Content-Type: application/json' \\\n -d '${JSON.stringify(r.body || {})}'` : `curl '${r.url}' -H 'X-API-Key: ${k}'`; if (l === "py") return r.method === "POST" ? `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)` : `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)`; 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);`; 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)`; } /* ------------------------------------------------------------------ recettes */ const RECIPES = [ { t: "Premier appel : compter les projets", d: "Vérifier la clé et lire les compteurs globaux.", ep: "/v1/stats", v: {}, code: { curl: `curl https://www.pdb-api.co/v1/stats -H 'X-API-Key: ${KEY_PH}' | python3 -m json.tool | head -30`, 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")`, 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");`, 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` } }, { 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: { 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}'`, 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']}")`, 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$"));`, 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)))` } }, { 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: { 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`, 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)`, 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");`, 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")])))` } }, { 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: { curl: `curl 'https://www.pdb-api.co/v1/projects/export.csv?type=data_center' -H 'X-API-Key: ${KEY_PH}' -o data_centers.csv`, 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()`, js: `const csv = await (await fetch(\`\${BASE}/projects/export.csv?type=data_center\`, { headers: H })).text();\nrequire("fs").writeFileSync("data_centers.csv", csv);`, 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)` } }, { 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: { 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}'`, 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]])`, 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));`, 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))` } }, { 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: { curl: `curl https://www.pdb-api.co/v1/companies/DUK -H 'X-API-Key: ${KEY_PH}'`, 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")`, 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 })));`, 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)` } }, { 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: { curl: `curl 'https://www.pdb-api.co/v1/search/semantic?q=battery+cell+factory&k=10' -H 'X-API-Key: ${KEY_PH}'`, 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']}")`, 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));`, 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))` } }, { 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: { 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}'`, 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")`, 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]]))));`, 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` } }, { 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: { curl: `curl 'https://www.pdb-api.co/v1/graph/node/L:texas?limit=100' -H 'X-API-Key: ${KEY_PH}'`, 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])`, js: `const node = await (await fetch(\`\${BASE}/graph/node/L:texas?limit=500\`, { headers: H })).json();\nconsole.log(node.degree, "projets rattachés à texas");`, 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` } }, { 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: { curl: `curl -s -o /dev/null -w '%{http_code}\\n' https://www.pdb-api.co/v1/stats -H 'X-API-Key: mauvaise' # 401`, 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")`, 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}`, 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()` } }, ]; /* ------------------------------------------------------------------ référence : dictionnaire des champs */ const FIELDS = { 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)"]], 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)"]], 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"]], }; 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"]]; /* ------------------------------------------------------------------ rendu */ let ctx, meta = { types: [], sectors: [] }, cur = 7, tab = "start", exampleIds = {}; function render(c) { ctx = c; const view = c.view; const hp = new URLSearchParams((location.hash.split("?")[1] || "")); if (hp.get("tab")) tab = hp.get("tab"); if (hp.get("ep")) { const i = ENDPOINTS.findIndex((e) => e.p === hp.get("ep")); if (i >= 0) { cur = i; tab = "play"; } } view.innerHTML = `

API & playground

API REST JSON de la base SPID. Base : ${esc(location.origin)}/v1 · authentification par en-tête X-API-Key · 240 requêtes/min · pagination limit/offset (≤ 500).

Langage des exemples : ${LANGS.map(([k, l]) => ``).join("")}
${[["start", "Démarrage rapide"], ["play", "Playground"], ["recipes", "Recettes"], ["ref", "Référence"], ["sdk", "SDK Python"]].map(([k, l]) => ``).join("")}
`; view.querySelectorAll("[data-lang]").forEach((b) => b.addEventListener("click", () => { localStorage.setItem("pdb_lang", b.dataset.lang); render(ctx); })); view.querySelector("#api-tabs").addEventListener("click", (e) => { if (e.target.dataset.tab) { tab = e.target.dataset.tab; render(ctx); } }); Promise.all([ctx.api("/taxonomy"), ctx.api("/sectors")]).then(([t, s]) => { meta = { types: t, sectors: s }; if (tab === "play") renderPlay(); }).catch(() => { }); ({ start: renderStart, play: renderPlay, recipes: renderRecipes, ref: renderRef, sdk: renderSdk }[tab] || renderStart)(); resolveExampleIds(); } async function resolveExampleIds() { if (exampleIds.TSLA_ENERGY) return; try { const p = await ctx.api("/projects", { ticker: "TSLA", q: "energy storage", limit: 1 }); exampleIds.TSLA_ENERGY = p.items[0]?.project_id; 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; } catch { } } const sub = (v) => exampleIds[v] || v; const keyBox = () => `
La clé n'est pas publiée sur ce site. Elle est fournie par l'équipe UQO (simon-pierre.boucher@uqo.ca). Elle est conservée en localStorage et envoyée uniquement à cette API.
`; function bindKey(root) { const inp = root.querySelector("#key"); if (!inp) return; inp.addEventListener("input", () => { localStorage.setItem("pdb_key", inp.value); root.querySelectorAll("pre.code[data-req]").forEach(() => { }); }); root.querySelector("#key-show").addEventListener("click", () => (inp.type = inp.type === "password" ? "text" : "password")); root.querySelector("#key-test").addEventListener("click", async () => { const st = root.querySelector("#key-st"); st.textContent = "…"; const r = await fetch("/v1/taxonomy", { headers: { "X-API-Key": key() } }); st.innerHTML = r.ok ? `✓ clé valide (HTTP 200)` : `✗ HTTP ${r.status} — ${esc((await r.json()).detail || "")}`; }); } const codeBlock = (code, id = "") => `
${esc(code)}
`; 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); })); } /* ---- Démarrage rapide */ function renderStart() { const l = lang(); const body = ctx.view.querySelector("#api-body"); 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 }); 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]; body.innerHTML = `
${keyBox()}

1 Installer

${codeBlock(install)}

2 Vérifier le service (sans clé)

${codeBlock(snippet(l, r1, KEY_PH, true))}

3 Premier appel authentifié

${codeBlock(snippet(l, r2, key() || KEY_PH))}

Réponse : {overview:{projects, mentions, companies…}, by_type:[…], by_sector:[…], mentions_by_year:[…]}

4 Filtrer des projets

${codeBlock(snippet(l, r3, key() || KEY_PH))}

Réponse paginée : {total, limit, offset, items:[{project_id, ticker, project_name, project_type, status, total_amount_usd, …}]}. Pour tout récupérer, incrémentez offset de limit jusqu'à total (recette « pagination »).

Modèle de données en 30 secondes

Projet (19 227) — unité principale. Un projet = mentions fusionnées par entreprise × type × ancre (lieu ou premier mot du nom). /v1/projects, /v1/projects/{id}.
Mention (39 930) — l'extraction brute : un projet cité dans une section d'un filing, avec date, formulaire, montant, confiance. /v1/mentions. La chronologie d'un projet = ses mentions triées par date.
Graphe (32 226 nœuds) et embeddings (384-d) — navigation par lieu/technologie/partenaire et recherche par le sens. /v1/graph/*, /v1/search/semantic, /v1/projects/{id}/similar.

Point d'attention : total_amount_usd est le montant maximal divulgué pour le projet, auto-déclaré et hétérogène ; first_seen/last_seen datent l'observation, first_year/last_year sont les années citées dans le texte (parfois projetées).

Codes de réponse

${ERRORS.map(([c, d]) => ``).join("")}
${c}${esc(d)}
`; bindKey(body); bindCopy(body); } /* ---- Playground */ const HIST_KEY = "pdb_hist"; function renderPlay() { const body = ctx.view.querySelector("#api-body"); const e = ENDPOINTS[cur]; body.innerHTML = `

Points d'accès

    ${GROUPS.map((g) => `
  • ${esc(g)}
  • ` + ENDPOINTS.map((x, i) => x.g === g ? `
  • ${x.m}${esc(x.p.replace("/v1", ""))}
  • ` : "").join("")).join("")}

Historique

${keyBox()}
`; body.querySelectorAll(".endpoints li[data-i]").forEach((li) => li.addEventListener("click", () => { cur = +li.dataset.i; renderPlay(); })); bindKey(body); renderHist(body); const pg = body.querySelector("#pg"); const ex0 = e.ex?.[0]?.v || {}; pg.innerHTML = `

${e.m} ${esc(e.p)}${e.key === false ? ' sans clé' : ""}

${esc(e.d)}

${e.ex?.length ? `
Exemples :${e.ex.map((x, i) => ``).join("")}
` : ""}
${e.params.map((p) => p.textarea ? `` : ``).join("") || `Aucun paramètre.`}
${meta.types.map((t) => ``).join("")}${meta.sectors.map((s) => `
${LANGS.map(([k, l]) => ``).join("")}
${codeBlock("", "req")}

Réponse

—
`; const vals = () => { const o = {}; pg.querySelectorAll("[data-p]").forEach((i) => { if (i.value !== "") o[i.dataset.p] = i.value; }); return o; }; 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; }; pg.querySelectorAll("[data-p]").forEach((i) => i.addEventListener("input", showReq)); showReq(); 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(); })); 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(); } }); bindCopy(pg); let last = null, lastRaw = ""; const renderResp = () => { const v = pg.querySelector("#resptabs .active").dataset.v; const box = pg.querySelector("#resp"); if (!last) { box.innerHTML = `
${esc(lastRaw.slice(0, 60000))}
`; return; } if (v === "table") { const rows = tabular(last); box.innerHTML = rows ? rows : `
Réponse non tabulaire — voir JSON.
`; } else { const txt = JSON.stringify(last, null, 2); box.innerHTML = `
${hl(txt.length > 80000 ? txt.slice(0, 80000) + "\n… (tronqué)" : txt)}
`; bindCopy(box); } }; 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(); } }); async function send() { const r = buildRequest(e, vals()); const st = pg.querySelector("#st"); st.textContent = "…"; const t0 = performance.now(); try { 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 }); lastRaw = await resp.text(); last = null; try { last = JSON.parse(lastRaw); } catch { } const ms = Math.round(performance.now() - t0); st.innerHTML = `HTTP ${resp.status} ${ms} ms · ${(lastRaw.length / 1024).toFixed(1)} ko${last?.total != null ? ` · total ${fmtN(last.total)}` : ""}`; 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(); }; pushHist({ ep: e.p, vals: vals(), status: resp.status, ms, t: Date.now() }); renderHist(body); renderResp(); } catch (err) { st.textContent = ""; pg.querySelector("#resp").innerHTML = `
${esc(String(err))}
`; } } pg.querySelector("#send").addEventListener("click", send); } function exploreLink(e, vals, r) { 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)))); if (e.p.startsWith("/v1/projects/{") && vals.project_id) return "#/project/" + encodeURIComponent(vals.project_id); if (e.p === "/v1/companies/{ident}" && vals.ident) return "#/company/" + encodeURIComponent(vals.ident); if (e.p === "/v1/mentions") return "#/mentions?" + qs(r.query); if (e.p === "/v1/mentions/{mention_id}" && vals.mention_id) return "#/mention/" + encodeURIComponent(vals.mention_id); if (e.p === "/v1/graph/node/{node_id}" && vals.node_id) return "#/graph?node=" + encodeURIComponent(vals.node_id); if (e.p === "/v1/graph/search" && vals.q) return "#/graph?q=" + encodeURIComponent(vals.q); if (e.p === "/v1/search/semantic" && vals.q) return "#/semantic?q=" + encodeURIComponent(vals.q); if (e.p === "/v1/sql") return "#/sql"; return null; } function tabular(d) { let rows = Array.isArray(d) ? d : d?.items || d?.edges || d?.projects || d?.timeline || null; if (d?.columns && d?.rows) rows = d.rows.map((r) => Object.fromEntries(d.columns.map((c, i) => [c, r[i]]))); if (!rows || !rows.length || typeof rows[0] !== "object") return null; const cols = Object.keys(rows[0]).filter((c) => !["description", "props", "neighbor_props", "vector"].includes(c)).slice(0, 14); return `
${cols.map((c) => ``).join("")}${rows.slice(0, 500).map((r) => `${cols.map((c) => { const v = r[c]; return ``; }).join("")}`).join("")}
${esc(c)}
${esc(Array.isArray(v) ? v.join(" | ") : v && typeof v === "object" ? JSON.stringify(v) : v)}
${fmtN(rows.length)} lignes${rows.length > 500 ? " (500 affichées)" : ""}
`; } const hl = (txt) => esc(txt).replace(/("(?:\\.|[^"\\])*")(\s*:)?/g, (m, s, c) => c ? `${s}${c}` : `${s}`).replace(/\b(-?\d+(?:\.\d+)?(?:e[+-]?\d+)?)\b/g, `$1`).replace(/\b(true|false|null)\b/g, `$1`); function pushHist(h) { const a = JSON.parse(localStorage.getItem(HIST_KEY) || "[]"); a.unshift(h); localStorage.setItem(HIST_KEY, JSON.stringify(a.slice(0, 12))); } function renderHist(root) { const a = JSON.parse(localStorage.getItem(HIST_KEY) || "[]"); const box = root.querySelector("#hist"); if (!box) return; box.innerHTML = a.length ? a.map((h, i) => `
${h.status} ${esc(h.ep.replace("/v1", ""))}
${esc(Object.entries(h.vals).map(([k, v]) => `${k}=${String(v).slice(0, 18)}`).join(" ")) || "—"} · ${h.ms} ms
`).join("") + `` : `Aucune requête envoyée.`; 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")); })); box.querySelector("#hist-clear")?.addEventListener("click", () => { localStorage.removeItem(HIST_KEY); renderHist(root); }); } /* ---- Recettes */ function renderRecipes() { const l = lang(); const body = ctx.view.querySelector("#api-body"); 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]; body.innerHTML = `
${codeBlock(pre)}
${RECIPES.map((r, i) => `

${i + 1}. ${esc(r.t)}

${esc(r.d)}

${codeBlock(r.code[l].replace(new RegExp(KEY_PH, "g"), key() || KEY_PH))}
`).join("")}
`; bindCopy(body); 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); })); } /* ---- Référence */ function renderRef() { const body = ctx.view.querySelector("#api-body"); body.innerHTML = `

Conventions

  • Authentification : en-tête X-API-Key: <clé>, ou Authorization: Bearer <clé>, ou paramètre ?api_key= (déconseillé). /v1/health est libre.
  • Pagination : limit (≤ 500) et offset ; réponse {total, limit, offset, items}.
  • Filtres texte : insensibles à la casse, correspondance « contient » (sauf ticker et cik, exacts). Les paramètres type, status, form acceptent des listes séparées par des virgules.
  • Montants en US$ (5e8 accepté). Dates au format ISO AAAA-MM-JJ. Listes renvoyées comme tableaux JSON ; en CSV, jointes par |.
  • Limite : 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.
  • OpenAPI : openapi.json · Swagger UI · ReDoc.

Routes

${ENDPOINTS.map((e) => ``).join("")}
MéthodeRouteDescriptionParamètres
${e.m}${esc(e.p)}${esc(e.d)}${e.params.map((p) => `${esc(p.name)}`).join(", ") || "—"}
${Object.entries(FIELDS).map(([t, f]) => `

Champs — ${t}

${f.map(([k, d]) => ``).join("")}
${esc(k)}${esc(d)}
`).join("")}

Types de projets

${meta.types.map((t) => ``).join("") || ""}
project_typeLibelléProjets
${t.project_type}${esc(t.label)}${fmtN(t.n)}
chargement…

Codes de réponse

${ERRORS.map(([c, d]) => ``).join("")}
${c}${esc(d)}
`; if (!meta.types.length) ctx.api("/taxonomy").then((t) => { meta.types = t; if (tab === "ref") renderRef(); }).catch(() => { }); } /* ---- SDK */ function renderSdk() { const body = ctx.view.querySelector("#api-body"); const k = key() || KEY_PH; body.innerHTML = `

pdb_client.py — client Python sans dépendance

Un fichier à déposer à côté de votre script ou notebook. Pagination automatique, réessais sur 429, conversion pandas/CSV, toutes les routes.

⬇ Télécharger pdb_client.py Voir le source   ⬇ Script R prêt à l'emploi (pdb_api.R)   ⬇ MATLAB (pdb_api.m) ⬇ Stata (pdb_api.do)

R — pdb_api.R

Fichier R complet (httr2 + dplyr) : fonctions pdb_get(), pdb_sql(), pdb_all() (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.

${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")`)}

MATLAB — pdb_api.m

webread / webwrite natifs (R2018a+), fonctions pdb_get, pdb_sql, pdb_all et dix sections exécutables. Non testé sur MATLAB ici : signalez toute erreur.

${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);`)}

Stata — pdb_api.do

Stata ne peut pas envoyer d'en-tête HTTP : le .do définit pdb_projects, pdb_mentions, pdb_sql via Python intégré (Stata 16+) et une voie shell curl + import delimited pour toute version. Non testé sur Stata ici.

${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)`)}

Prise en main

${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"])`)}

Tout un jeu de données en DataFrame

${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")`)}

Fiche, similaires, entreprise, graphe

${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]`)}

Sémantique et SQL

${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")`)}

Dans un notebook Jupyter / Colab

${codeBlock(`!curl -sO ${location.origin}/sdk/pdb_client.py\nimport os; os.environ["PDB_API_KEY"] = "${k}"\nfrom pdb_client import PDB\npdb = PDB()`)}

JavaScript / TypeScript (sans SDK)

${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}`)}
`; bindCopy(body); } return { render }; })();