Four new data sources and fetch scripts via bundesAPI community project: - DS-00011 Lobbyregister: 6,799 registrants, €0.86–0.91B declared lobbying (FY2024) - DS-00012 SMARD Strommarkt: 60.2% renewable 2024, Wind Onshore #1 (107 TWh) - DS-00013 Bundeshaushalt: €474.75B Ist-Wert 2024, 38.2% Soziales, 10.6% Verteidigung - DS-00014 DIP Bundestag: 7,605 Drucksachen WP21, 12,507 Vorgänge, 83 Plenarprotokolle Each integration: live-data fetch script (bun/TypeScript) + DATASET-TEMPLATE markdown + CSV outputs. Scripts idempotent — re-run for current data. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
130 lines
3.9 KiB
Plaintext
Executable File
130 lines
3.9 KiB
Plaintext
Executable File
#!/usr/bin/env bun
|
|
|
|
/**
|
|
* Get DE Bundeshaushalt Data
|
|
*
|
|
* Fetches federal budget data from the Bundeshaushalt Digital API and produces:
|
|
* - Data/DE-Federal-Budget/haushalt-expenses-2024.csv
|
|
* - Data/DE-Federal-Budget/haushalt-income-2024.csv
|
|
*
|
|
* API: https://bundeshaushalt.de/internalapi/budgetData
|
|
* Source: https://www.bundeshaushalt.de/DE/Bundeshaushalt-digital/bundeshaushalt-digital.html
|
|
*/
|
|
|
|
// Bun on macOS may lack the Bundeshaushalt CA in its bundled cert store.
|
|
// This flag allows the fetch to proceed against the known-good government endpoint.
|
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
|
|
import { writeFileSync, mkdirSync } from "fs";
|
|
import { join } from "path";
|
|
|
|
const BASE_URL = "https://bundeshaushalt.de/internalapi/budgetData";
|
|
const OUT_DIR = join(__dirname, "Data/DE-Federal-Budget");
|
|
const YEAR = 2024;
|
|
|
|
interface BudgetMeta {
|
|
year: number;
|
|
unit: string;
|
|
quota: string;
|
|
account: string;
|
|
timestamp: number;
|
|
modifyDate: string;
|
|
entity: string;
|
|
levelCur: number;
|
|
levelMax: number;
|
|
}
|
|
|
|
interface BudgetDetail {
|
|
label: string;
|
|
value: number;
|
|
relativeToParentValue: number;
|
|
relativeValue: number;
|
|
}
|
|
|
|
interface BudgetChild {
|
|
id: string;
|
|
budgetNumber: string;
|
|
label: string;
|
|
value: number;
|
|
relativeToParentValue: number;
|
|
relativeValue: number;
|
|
}
|
|
|
|
interface BudgetResponse {
|
|
meta: BudgetMeta;
|
|
detail: BudgetDetail;
|
|
children: BudgetChild[];
|
|
}
|
|
|
|
function csvEscape(value: string | number | undefined | null): string {
|
|
if (value === null || value === undefined) return "";
|
|
const s = String(value);
|
|
if (s.includes(",") || s.includes('"') || s.includes("\n")) {
|
|
return `"${s.replace(/"/g, '""')}"`;
|
|
}
|
|
return s;
|
|
}
|
|
|
|
async function fetchBudget(account: "expenses" | "income"): Promise<BudgetResponse> {
|
|
const url = `${BASE_URL}?year=${YEAR}&account=${account}"a=actual`;
|
|
console.log(`Fetching ${account} data from ${url}…`);
|
|
const res = await fetch(url);
|
|
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText} (${url})`);
|
|
return (await res.json()) as BudgetResponse;
|
|
}
|
|
|
|
function writeCsv(data: BudgetResponse, filename: string): void {
|
|
const sorted = [...data.children].sort((a, b) => b.value - a.value);
|
|
|
|
const header = "rank,id,label,value_eur,share_pct";
|
|
const rows = sorted.map((child, i) =>
|
|
[
|
|
i + 1,
|
|
csvEscape(child.id),
|
|
csvEscape(child.label),
|
|
child.value.toFixed(2),
|
|
child.relativeToParentValue.toFixed(2),
|
|
].join(",")
|
|
);
|
|
|
|
const path = join(OUT_DIR, filename);
|
|
writeFileSync(path, [header, ...rows].join("\n") + "\n");
|
|
console.log(`Wrote ${path} (${rows.length} Einzelpläne)`);
|
|
}
|
|
|
|
async function main() {
|
|
mkdirSync(OUT_DIR, { recursive: true });
|
|
|
|
const [expenses, income] = await Promise.all([
|
|
fetchBudget("expenses"),
|
|
fetchBudget("income"),
|
|
]);
|
|
|
|
writeCsv(expenses, `haushalt-expenses-${YEAR}.csv`);
|
|
writeCsv(income, `haushalt-income-${YEAR}.csv`);
|
|
|
|
const totalExpenses = expenses.detail.value;
|
|
const totalIncome = income.detail.value;
|
|
const balance = totalIncome - totalExpenses;
|
|
|
|
const sortedExpenses = [...expenses.children].sort((a, b) => b.value - a.value);
|
|
const top5 = sortedExpenses.slice(0, 5);
|
|
|
|
console.log(`\n── Bundeshaushalt ${YEAR} Summary (Ist-Werte) ────────────────────`);
|
|
console.log(`Total Ausgaben: €${(totalExpenses / 1e9).toFixed(2)}B`);
|
|
console.log(`Total Einnahmen: €${(totalIncome / 1e9).toFixed(2)}B`);
|
|
console.log(`Balance: €${(balance / 1e9).toFixed(2)}B (${balance >= 0 ? "Überschuss" : "Defizit"})`);
|
|
console.log(`\nTop 5 Ausgaben-Einzelpläne:`);
|
|
for (const [i, ep] of top5.entries()) {
|
|
console.log(
|
|
` ${i + 1}. ${ep.label} — €${(ep.value / 1e9).toFixed(2)}B (${ep.relativeToParentValue.toFixed(1)}%)`
|
|
);
|
|
}
|
|
console.log(`\nModify date (expenses): ${expenses.meta.modifyDate}`);
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error("Error:", err.message);
|
|
process.exit(1);
|
|
});
|