Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 | 1x 6x 6x 6x 1x 5x 5x 5x 12x 12x 12x 12x 12x 10x 10x 2x 5x 4x 4x 1x 3x 5x 1x 1x 1x 1x 1x 1x | #!/usr/bin/env tsx
/**
* @module scripts/statskontoret-fetch
* @description CLI wrapper around StatskontoretClient for agentic workflows.
*
* Usage:
* tsx scripts/statskontoret-fetch.ts list-sources
* tsx scripts/statskontoret-fetch.ts discover --source myndighetsforteckning
* tsx scripts/statskontoret-fetch.ts headcount --url <xlsx-url> [--persist]
* tsx scripts/statskontoret-fetch.ts budget-outturn --url <xlsx-url> --source arsutfall [--doc-type Inkomst] [--persist]
*/
import path from 'node:path';
import { pathToFileURL } from 'node:url';
import {
buildBudgetTimeSeries,
buildHeadcountTimeSeries,
getStatskontoretSource,
STATSKONTORET_SOURCES,
StatskontoretClient,
StatskontoretError,
type StatskontoretSourceKey,
} from './statskontoret-client.js';
import { persistStatskontoretData } from './parliamentary-data/data-persistence.js';
interface ParsedArgs {
readonly command: 'list-sources' | 'discover' | 'headcount' | 'budget-outturn' | 'help';
readonly flags: ReadonlyMap<string, string>;
readonly booleans: ReadonlySet<string>;
}
const HELP = `tsx scripts/statskontoret-fetch.ts <command> [flags]
Commands:
list-sources Print the built-in Statskontoret source catalogue
discover Extract downloadable Excel/CSV-ZIP links from a source page
headcount Fetch an authority-register workbook and aggregate headcount by department/year
budget-outturn Fetch a budget-outturn workbook (årsutfall / månadsutfall / tidsserier) and parse rows
help Show this message
Flags:
--source <KEY> Source key: myndighetsforteckning | budget-time-series | arsutfall | manadsutfall
--url <URL> Direct Excel workbook URL for headcount / budget-outturn commands
--doc-type <TYPE> Override documentType label for budget-outturn (e.g. Inkomst | Utgift)
--persist Write raw/derived output under analysis/data/statskontoret/
`;
export function parseStatskontoretArgs(argv: readonly string[]): ParsedArgs {
const command = (argv[0] ?? 'help') as ParsedArgs['command'];
const validCommands: readonly ParsedArgs['command'][] = [
'list-sources', 'discover', 'headcount', 'budget-outturn', 'help',
];
if (!validCommands.includes(command)) {
throw new StatskontoretError(`unknown command ${command}`, 'cli');
}
const flags = new Map<string, string>();
const booleans = new Set<string>();
for (let i = 1; i < argv.length; i++) {
const token = argv[i];
Iif (!token.startsWith('--')) {
throw new StatskontoretError(`unexpected positional argument ${token}`, 'cli');
}
const key = token.slice(2);
const next = argv[i + 1];
if (next !== undefined && !next.startsWith('--')) {
flags.set(key, next);
i++;
} else {
booleans.add(key);
}
}
return { command, flags, booleans };
}
export function requireStatskontoretFlag(flags: ReadonlyMap<string, string>, key: string): string {
const value = flags.get(key);
if (!value) {
throw new StatskontoretError(`missing required flag --${key}`, 'cli');
}
return value;
}
export function parseStatskontoretSource(value: string): StatskontoretSourceKey {
if (STATSKONTORET_SOURCES.some((source) => source.key === value)) return value as StatskontoretSourceKey;
throw new StatskontoretError(`unknown source ${value}`, 'cli');
}
async function runDiscover(flags: ReadonlyMap<string, string>, booleans: ReadonlySet<string>): Promise<void> {
const source = parseStatskontoretSource(requireStatskontoretFlag(flags, 'source'));
const client = new StatskontoretClient();
const links = await client.discoverDownloads(source);
const payload = { source: getStatskontoretSource(source), links };
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
if (booleans.has('persist')) {
persistStatskontoretData(source, 'downloads', payload);
}
}
async function runHeadcount(flags: ReadonlyMap<string, string>, booleans: ReadonlySet<string>): Promise<void> {
const url = requireStatskontoretFlag(flags, 'url');
const client = new StatskontoretClient();
const workbook = await client.fetchWorkbook(url);
const headcount = buildHeadcountTimeSeries(workbook, { sheetNamePattern: /förteckning|forteckning/i });
const payload = { source: 'myndighetsforteckning', url, headcount };
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
if (booleans.has('persist')) {
persistStatskontoretData('myndighetsforteckning', 'headcount-by-department', payload);
}
}
async function runBudgetOutturn(flags: ReadonlyMap<string, string>, booleans: ReadonlySet<string>): Promise<void> {
const url = requireStatskontoretFlag(flags, 'url');
const source = parseStatskontoretSource(requireStatskontoretFlag(flags, 'source'));
if (source === 'myndighetsforteckning') {
throw new StatskontoretError(
'budget-outturn command is for arsutfall | manadsutfall | budget-time-series, not myndighetsforteckning',
'cli',
);
}
const docType = flags.get('doc-type');
const client = new StatskontoretClient();
const workbook = await client.fetchWorkbook(url);
const rows = buildBudgetTimeSeries(workbook, { ...(docType ? { documentType: docType } : {}) });
const payload = { source, url, ...(docType ? { documentType: docType } : {}), rows };
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
if (booleans.has('persist')) {
const artifact = docType
? `budget-outturn-${docType.toLowerCase()}`
: 'budget-outturn';
persistStatskontoretData(source, artifact, payload);
}
}
async function main(): Promise<void> {
const { command, flags, booleans } = parseStatskontoretArgs(process.argv.slice(2));
switch (command) {
case 'list-sources':
process.stdout.write(`${JSON.stringify({ sources: STATSKONTORET_SOURCES }, null, 2)}\n`);
return;
case 'discover':
await runDiscover(flags, booleans);
return;
case 'headcount':
await runHeadcount(flags, booleans);
return;
case 'budget-outturn':
await runBudgetOutturn(flags, booleans);
return;
case 'help':
default:
process.stdout.write(HELP);
}
}
function isDirectExecution(): boolean {
const entry = process.argv[1];
Iif (!entry) return false;
try {
return import.meta.url === pathToFileURL(path.resolve(entry)).href;
} catch {
// `pathToFileURL` throws on malformed paths; `path.resolve` is used to
// normalise the entry first so most runners reach the comparison, and the
// catch keeps the module import-safe across exotic launchers.
return false;
}
}
Iif (isDirectExecution()) {
main().catch((error: unknown) => {
const message = error instanceof Error ? error.message : String(error);
process.stderr.write(`statskontoret-fetch: ${message}\n`);
process.exit(error instanceof StatskontoretError && error.kind === 'cli' ? 2 : 1);
});
}
|