mostlyright 1.5.1 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-OTJGBPL4.mjs → chunk-4RWHWFV3.mjs} +8 -4
- package/dist/chunk-4RWHWFV3.mjs.map +1 -0
- package/dist/{iem-asos-BPSHDERE.mjs → iem-asos-GEUKTQTM.mjs} +2 -2
- package/dist/index.bundle.mjs +61 -25
- package/dist/index.bundle.mjs.map +1 -1
- package/dist/index.global.js +74 -26
- package/dist/index.global.js.map +1 -1
- package/package.json +4 -4
- package/dist/chunk-OTJGBPL4.mjs.map +0 -1
- /package/dist/{iem-asos-BPSHDERE.mjs.map → iem-asos-GEUKTQTM.mjs.map} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mostlyright",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Public-data SDK for TypeScript — one import for quants, ML pipelines, and AI agents. Adapters ship weather (METAR, ASOS, GHCNh, NWS CLI) and prediction-market settlements (Kalshi NHIGH/NLOW, Polymarket) today; SEC filings, Federal Reserve series, court filings, FDA approvals, and equities are next. Local-first, no hosted backend.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"weather",
|
|
@@ -49,9 +49,9 @@
|
|
|
49
49
|
"dist"
|
|
50
50
|
],
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@mostlyrightmd/core": "1.
|
|
53
|
-
"@mostlyrightmd/
|
|
54
|
-
"@mostlyrightmd/
|
|
52
|
+
"@mostlyrightmd/core": "1.6.0",
|
|
53
|
+
"@mostlyrightmd/markets": "1.6.0",
|
|
54
|
+
"@mostlyrightmd/weather": "1.6.0"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"msw": "2.6.6",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../weather/src/_fetchers/_iem_chunks.ts","../../weather/src/_fetchers/iem-asos.ts"],"sourcesContent":["// Shared calendar-year chunkers for IEM fetchers.\n//\n// Byte-faithful TS port of\n// `packages/weather/src/mostlyright/weather/_fetchers/_iem_chunks.py::yearly_chunks_exclusive_end`\n// (mostlyright PR #85 commit cf9eb85, 2026-05-12; Pattern 1).\n//\n// Only the EXCLUSIVE-end variant is ported here — the IEM ASOS download\n// loop (`iem-asos.ts`) is the sole TS-side consumer, and IEM's `day2`\n// query parameter is exclusive. `yearly_chunks_inclusive` (used by the\n// Python forecast / climate paths) is NOT ported in TS-W2.\n//\n// Leap-year safety: the advance step uses calendar arithmetic\n// (`date(year+1, 1, 1)`), NOT `+365 days`. The latter silently drops Feb 29\n// in leap years and walks the calendar boundary by one day every leap year\n// — PR #85's primary anti-pattern.\n//\n// Date representation: ISO 8601 date strings (\"YYYY-MM-DD\"). Strings sort\n// lexicographically equivalent to calendar order and round-trip cleanly;\n// the JS `Date` constructor is deliberately avoided because its arithmetic\n// silently shifts to local-time, which would poison cache keys at midnight\n// boundaries for non-UTC hosts (Python's `_iem_chunks.py` module docstring\n// references this class of bug).\n\n/**\n * ISO 8601 date string, no time component. Format: `YYYY-MM-DD`.\n *\n * Brand-style alias — at runtime this is a plain `string`. Use\n * {@link parseIsoDate} when you need the parsed year/month/day integers.\n */\nexport type IsoDate = string;\n\nconst ISO_DATE_RE = /^(\\d{4})-(\\d{2})-(\\d{2})$/;\n\ninterface YMD {\n readonly year: number;\n readonly month: number;\n readonly day: number;\n}\n\n/**\n * Parse an ISO date string into year/month/day integers. Throws on\n * malformed input. The validation is intentionally minimal — chunker\n * inputs come from the fetcher's normalization layer, which is the\n * defensive boundary. Calendar-validity (e.g. Feb 30) is NOT checked\n * here; the caller has already validated.\n */\nfunction parseIsoDate(value: IsoDate): YMD {\n const m = ISO_DATE_RE.exec(value);\n if (m === null) {\n throw new Error(`Invalid ISO date: ${JSON.stringify(value)}; expected \"YYYY-MM-DD\"`);\n }\n // Indices 1-3 always present when the regex matches.\n return {\n year: Number.parseInt(m[1] as string, 10),\n month: Number.parseInt(m[2] as string, 10),\n day: Number.parseInt(m[3] as string, 10),\n };\n}\n\n/**\n * Format integer year/month/day as an ISO date string. Month and day are\n * zero-padded to 2 digits to preserve lexicographic order.\n */\nfunction formatIsoDate(year: number, month: number, day: number): IsoDate {\n const mm = month < 10 ? `0${month}` : String(month);\n const dd = day < 10 ? `0${day}` : String(day);\n return `${year}-${mm}-${dd}`;\n}\n\n/**\n * Range split into per-calendar-year EXCLUSIVE-end chunks\n * (Jan 1 of next year).\n *\n * Properties:\n * - The first chunk's start is `max(date(start.year, 1, 1), start)` — i.e.\n * the caller's actual start for the first chunk, NOT Jan 1. The\n * `current = date(start.year, 1, 1)` initialization ensures the loop\n * visits every calendar-year boundary regardless of the caller's start.\n * - Every chunk's end is `date(year+1, 1, 1)` — the IEM `day2`-exclusive\n * convention for \"include all of `year`\".\n * - Leap-year safe: advance via `date(year+1, 1, 1)`, NOT `+365 days`.\n * - Reversed range (`start > end` by lexicographic compare) returns `[]`\n * without throwing — higher layers iterate the list directly.\n *\n * Byte-faithful with the Python helper used by `iem_asos.download_iem_asos`.\n */\nexport function yearlyChunksExclusiveEnd(\n start: IsoDate,\n end: IsoDate,\n): ReadonlyArray<readonly [IsoDate, IsoDate]> {\n // Reversed-range short-circuit (lexicographic compare is equivalent to\n // calendar compare for YYYY-MM-DD strings of identical shape). Mirrors\n // Python L66-67 (`if start > end: return []`).\n if (start > end) {\n return [];\n }\n\n const { year: startYear } = parseIsoDate(start);\n const { year: endYear } = parseIsoDate(end);\n\n const chunks: Array<readonly [IsoDate, IsoDate]> = [];\n // Match Python `current = date(start.year, 1, 1)` initialization.\n let currentYear = startYear;\n while (currentYear <= endYear) {\n const currentYearStart = formatIsoDate(currentYear, 1, 1);\n // Match Python `chunk_start = max(current, start)`. For the first\n // iteration this clamps to the caller's `start`; for subsequent\n // iterations `currentYearStart` is always greater.\n const chunkStart: IsoDate = currentYearStart > start ? currentYearStart : start;\n // CRITICAL: leap-year safe via calendar arithmetic, NOT `+365 days`.\n const nextYearFirst: IsoDate = formatIsoDate(currentYear + 1, 1, 1);\n chunks.push([chunkStart, nextYearFirst]);\n currentYear += 1;\n }\n return chunks;\n}\n","// IEM ASOS historical METAR fetcher — yearly-chunked CSV downloads.\n//\n// Byte-faithful TS port of Python\n// `packages/weather/src/mostlyright/weather/_fetchers/iem_asos.py::download_iem_asos`,\n// with the following deliberate adaptations:\n//\n// 1. No disk cache. Python writes per-station CSVs under\n// `dest_dir/{station}/iem_*.csv`; the TS port returns the CSV body\n// in-memory via `{ chunkStart, chunkEnd, csv }`. The disk-cache layer\n// (PERF-02 partial-namespace + cache-poisoning paths) lands in TS-W3 —\n// it's intentionally out of scope here. Drop ALL filename / `skip_cache`\n// / `today_utc` partial-namespace logic.\n// 2. Station validation at the URL boundary uses an inline regex\n// (`^[A-Z]{3,4}$`) matching `iem-cli.ts`'s `validateIcao` pattern. The\n// Python `validate_icao_for_path` from `_internal/_bounds.py` is a\n// path-traversal guard — there's no path here in TS-W2, so the regex\n// check is the equivalent defense-in-depth measure.\n//\n// URL shape, start-normalization (caller's `start` clamped to\n// `date(start.year, 1, 1)` for cache idempotence under per-month callers),\n// reversed-range short-circuit (`start > end → []`), report_type guard\n// ({3, 4}), and 1-sec polite delay are byte-faithful.\n//\n// CORS posture: OPEN — IEM emits `Access-Control-Allow-Origin: *` per\n// `.planning/research/TS-CORS-MATRIX.md` §IEM-ASOS. Works in browsers,\n// Node 20+, Cloudflare Workers, Deno.\n\nimport { fetchWithRetry } from \"@mostlyrightmd/core\";\nimport type { FetchWithRetryOptions } from \"@mostlyrightmd/core\";\n\nimport { type IsoDate, yearlyChunksExclusiveEnd } from \"./_iem_chunks.js\";\n\n/** IEM ASOS request endpoint. Mirrors Python `IEM_BASE_URL`. */\nexport const IEM_BASE_URL = \"https://mesonet.agron.iastate.edu/cgi-bin/request/asos.py\";\n\n/**\n * Polite delay (ms) between consecutive IEM HTTP requests. Mirrors Python\n * `IEM_POLITE_DELAY = 1.0` (s) — IEM runs on a university server and\n * documents a 1-sec/IP throttle (see .planning/research/SOURCE-LIMITS.md).\n */\nexport const IEM_POLITE_DELAY_MS = 1000;\n\n/**\n * Station code regex (3-4 uppercase letters). Mirrors the inline pattern\n * used by `iem-cli.ts::validateIcao` and the shared `STATION_CODE_RE` from\n * `@mostlyrightmd/core/internal/bounds`. Inlined here so the fetcher does not\n * transitively pull in the validators barrel — keeps the per-fetcher\n * dep graph narrow per the TS-W1 review pattern.\n */\nconst STATION_CODE_RE = /^[A-Z]{3,4}$/;\n\n/** Permitted IEM report_type values: 3 = METAR, 4 = SPECI. */\nconst VALID_REPORT_TYPES = new Set<number>([3, 4]);\n\n/**\n * One downloaded yearly chunk. The CSV body is forwarded verbatim from\n * the upstream response so the parser can run a downstream pass over it.\n */\nexport interface IemChunkResult {\n /** First day of this chunk's range (caller's start for chunk 0; Jan 1 for subsequent). */\n readonly chunkStart: IsoDate;\n /** EXCLUSIVE end of this chunk's range — Jan 1 of the following calendar year. */\n readonly chunkEnd: IsoDate;\n /** Raw CSV body returned by IEM (text/plain, comma-separated). */\n readonly csv: string;\n}\n\nexport interface DownloadIemAsosOptions extends FetchWithRetryOptions {\n /** `3` (METAR, default) or `4` (SPECI). */\n reportType?: 3 | 4;\n /**\n * Delay (ms) between successive chunk requests. Defaults to\n * {@link IEM_POLITE_DELAY_MS}. Set to `0` in unit tests.\n */\n politenessMs?: number;\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction validateIcao(stationCode: string): void {\n if (typeof stationCode !== \"string\" || !STATION_CODE_RE.test(stationCode)) {\n throw new Error(\n `station_code=${JSON.stringify(\n stationCode,\n )} does not match STATION_CODE_RE (3-4 uppercase letters); refusing to use as URL component`,\n );\n }\n}\n\n/**\n * Build the IEM ASOS download URL for one (chunk, report_type) request.\n *\n * Param shape and ordering are byte-faithful to Python `_build_iem_url`:\n * station, data, tz, format, latlon, elev, missing, trace, direct,\n * report_type, year1, month1, day1, year2, month2, day2.\n *\n * Month/day are emitted WITHOUT zero-padding (Python uses bare `{start.month}`,\n * not `{start.month:02d}`) — preserve byte-equivalence on URL snapshots.\n *\n * `end` is the EXCLUSIVE end (already adjusted by the chunker to Jan 1 of\n * the following calendar year).\n */\nexport function buildIemUrl(\n stationCode: string,\n start: IsoDate,\n end: IsoDate,\n reportType: number,\n): string {\n // Parse ISO strings into ints (no Date — avoids local-TZ silent shifts).\n const [sy, sm, sd] = splitIso(start);\n const [ey, em, ed] = splitIso(end);\n const params = `station=${stationCode}&data=all&tz=Etc/UTC&format=comma&latlon=no&elev=no&missing=M&trace=T&direct=no&report_type=${reportType}&year1=${sy}&month1=${sm}&day1=${sd}&year2=${ey}&month2=${em}&day2=${ed}`;\n return `${IEM_BASE_URL}?${params}`;\n}\n\nfunction splitIso(iso: IsoDate): [number, number, number] {\n // The chunker has already validated; we just split.\n const parts = iso.split(\"-\");\n // Length-3 guaranteed if the chunker emitted this; defense in depth.\n if (parts.length !== 3) {\n throw new Error(`Invalid ISO date passed to buildIemUrl: ${JSON.stringify(iso)}`);\n }\n return [\n Number.parseInt(parts[0] as string, 10),\n Number.parseInt(parts[1] as string, 10),\n Number.parseInt(parts[2] as string, 10),\n ];\n}\n\n/**\n * Download yearly chunks of IEM ASOS data for one station, returning the\n * raw CSV bodies in chunker-natural order.\n *\n * The caller's inclusive `[start, end]` is normalized internally to\n * `[date(start.year, 1, 1), end]` and split into per-calendar-year\n * EXCLUSIVE-end chunks via {@link yearlyChunksExclusiveEnd}. Each chunk\n * fires one HTTP request to the IEM ASOS endpoint, with a polite delay\n * between successful responses (default {@link IEM_POLITE_DELAY_MS}).\n *\n * Errors propagate from `fetchWithRetry` — 4xx/5xx after retry exhaustion\n * are NOT swallowed (the IEM ASOS path is parity-critical; silent failure\n * would manifest as missing observation rows downstream).\n *\n * @throws If `reportType` is not `3` (METAR) or `4` (SPECI).\n * @throws If `stationCode` does not match `^[A-Z]{3,4}$` (path-traversal /\n * URL-injection defense).\n * @throws Whatever `fetchWithRetry` propagates on persistent network/HTTP\n * errors.\n */\nexport async function downloadIemAsos(\n stationCode: string,\n start: IsoDate,\n end: IsoDate,\n opts: DownloadIemAsosOptions = {},\n): Promise<ReadonlyArray<IemChunkResult>> {\n const reportType = opts.reportType ?? 3;\n if (!VALID_REPORT_TYPES.has(reportType)) {\n throw new Error(`report_type must be 3 (METAR) or 4 (SPECI), got ${reportType}`);\n }\n // Defense-in-depth: validate at the URL boundary BEFORE any HTTP.\n validateIcao(stationCode);\n\n // Reversed-range guard. Mirror Python L201-202: the chunker honors\n // `start > end → []`, but defensive short-circuit here avoids ever\n // computing the normalized year boundary for a malformed call.\n if (start > end) {\n return [];\n }\n\n // mostlyright-specific normalization: clamp caller's `start` to Jan 1 of\n // its year so per-month callers share a yearly cache key (parity-faithful\n // with Python's `normalized_start = date(start.year, 1, 1)`). Mirrored in\n // TS so the URL shape (and the future TS-W3 cache key) stays byte-stable\n // with Python on the same range.\n const normalizedStart: IsoDate = `${start.slice(0, 4)}-01-01`;\n const chunks = yearlyChunksExclusiveEnd(normalizedStart, end);\n\n const politenessMs = opts.politenessMs ?? IEM_POLITE_DELAY_MS;\n // Strip fetcher-specific opts from the bag forwarded to fetchWithRetry.\n const { reportType: _rtDrop, politenessMs: _pmDrop, ...fetchOpts } = opts;\n void _rtDrop;\n void _pmDrop;\n\n const out: IemChunkResult[] = [];\n for (const [chunkStart, chunkEnd] of chunks) {\n const url = buildIemUrl(stationCode, chunkStart, chunkEnd, reportType);\n const response = await fetchWithRetry(url, fetchOpts);\n const csv = await response.text();\n out.push({ chunkStart, chunkEnd, csv });\n // Polite delay AFTER each successful round-trip (mirror Python L224\n // which sleeps unconditionally). Skipping the trailing sleep on the\n // last chunk would be a micro-optimization not present in Python.\n if (politenessMs > 0) {\n await sleep(politenessMs);\n }\n }\n return out;\n}\n"],"mappings":"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","names":[]}
|
|
File without changes
|