-
Notifications
You must be signed in to change notification settings - Fork 0
fix: initial dparquet (not done) #10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -73,6 +73,55 @@ const slice = await point.timeRange({ | |
| console.log(await slice.toRecords("precipitation")); | ||
| ``` | ||
|
|
||
| ### Station data usage | ||
|
|
||
| Gridded Zarr datasets come from `loadDataset`. Point-observation **station** | ||
| datasets (GHCND and friends) live under `client.stations`, and read the same way: | ||
| degrees, ISO timestamps, chained selections. | ||
|
|
||
| ```typescript | ||
| const stations = await client.stations.load({ cid: "bafyr4i..." }); | ||
|
|
||
| // Every station, with position and coverage window. | ||
| for (const s of stations.stations) { | ||
| console.log(s.stationId, s.latitude, s.longitude, s.start, s.end); | ||
| } | ||
|
|
||
| // Stations within 50 km of a point, over one week. | ||
| const records = await stations | ||
| .circle(40.75, -73.99, 50) | ||
| .timeRange({ start: "2023-01-01", end: "2023-01-07" }) | ||
| .toRecords("TMAX"); | ||
| ``` | ||
|
|
||
| Selections return new instances, so a partial selection can be branched: | ||
|
|
||
| ```typescript | ||
| const week = stations.timeRange({ start: "2023-01-01", end: "2023-01-07" }); | ||
| const nyc = await week.select("USW00094728").rows(); | ||
| const lax = await week.select("USW00023174").rows(); | ||
| ``` | ||
|
|
||
| Two things differ from `GeoTemporalDataset`, because the data model differs: | ||
|
|
||
| - **`nearest(lat, lon, { maxKm })` instead of `point()`.** A grid always has a | ||
| cell under any coordinate; stations are irregular, so the nearest one may be | ||
| far away. Pass `maxKm` to make that a hard bound rather than a surprise. | ||
| - **`where(...)` has no gridded counterpart.** Row-level predicates are pushed | ||
| down to fragment statistics, so most fragments are skipped without being read: | ||
|
|
||
| ```typescript | ||
| const hotDays = await stations | ||
| .nearest(29.98, -95.36) | ||
| .timeRange({ start: "2025-01-01", end: "2025-12-31" }) | ||
| .where({ element: "TMAX", op: "gt", value: 350 }) // tenths of °C, so 35 °C | ||
| .rows(); | ||
|
Comment on lines
+110
to
+118
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
rg -n -C 8 '\bwhere\s*\(|fragment|statistics|predicate|skip' src testsRepository: dClimate/dclimate-client-js Length of output: 24478 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== locate files likely to define where/rows/fetch paths =="
git ls-files | rg '(^|/)(README\.md|.*\.ts)$' | rg 'src |package.json|tests' | sed -n '1,200p'
echo
echo "== find all identifiers that export/assign 'where' or related query builder methods =="
rg -n "where:|method:.*where|\\.where\\(|function where|const where|rows\\(|nearest\\(|timeRange\\(" src tests --glob '*.ts' --glob '!src/shapes/circle.ts' --glob '!src/actions/concatenate-variants.ts' | sed -n '1,240p'Repository: dClimate/dclimate-client-js Length of output: 4384 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== src/geotemporal-dataset.ts outline =="
ast-grep outline src/geotemporal-dataset.ts || true
echo
echo "== src/geotemporal-dataset.ts relevant section =="
sed -n '1,340p' src/geotemporal-dataset.ts | cat -n
echo
echo "== occurrences of API query predicates in src and tests =="
rg -n "where|op:|value:|element|statistic|min|max|stats|bounds|bbox|spatialExtent|fragment|skip" src tests --glob '*.ts' | sed -n '1,320p'Repository: dClimate/dclimate-client-js Length of output: 28615 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== README.md around documented API usage =="
sed -n '90,130p' README.md | cat -n
echo
echo "== all source/test references to rows, station, nearest, and predicate shapes =="
rg -n "\.rows\(|stations|nearest\(|where\(|{ element:|element.*op:|op: .*value:|\.where\(" . --glob '*.ts' --glob '*.md' --glob '!package-lock.json' --glob '!node_modules/**' | sed -n '1,320p'
echo
echo "== git tracked ts files under src with top-level exports/classes likely query API =="
git ls-files src/*.ts src/**/*.ts | sed -n '1,200p'Repository: dClimate/dclimate-client-js Length of output: 2284 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== all source/test references to stations, nearest, rows, and where =="
python3 - <<'PY'
import subprocess
patterns = [r"\.rows\(", r"stations", r"\.nearest\(", r"\.where\(", r"\brows\(", r"\bnearest\("]
for pattern in patterns:
print(f"\n-- pattern: {pattern} --")
try:
subprocess.run([
"rg", "-n", "-C", "4", pattern, ".",
"--glob", "*.ts", "--glob", "*.md",
"--glob", "!package-lock.json",
"--glob", "!node_modules/**"
], check=False)
except subprocess.CalledProcessError as e:
if e.returncode != 1: raise
PYRepository: dClimate/dclimate-client-js Length of output: 23875 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== files under src/stations =="
git ls-files src/stations | sort
echo
echo "== inspect relevant station files excluding data files =="
for f in $(git ls-files 'scripts/inspect-stations.ts' 'src/stations/**/*.ts' | grep -v '\.parquet$' | grep -v '\.json$'); do
echo "--- $f ($(wc -l < "$f") lines) ---"
ast-grep outline "$f" || true
done
echo
echo "== search for where/predicate/statistics implementation in src scripts =="
rg -n -C 6 "where|predicate|element.*op:|op:.*element|fragment.*statistic|statistics|min|max|bounds|selectFragments|planFragments|toQuery|nearest|rows|rowCount|min|max|min_value|max_value|byteLength" scripts src --glob '*.ts' --glob '!src/shapes/circle.ts' | sed -n '1,500p'Repository: dClimate/dclimate-client-js Length of output: 31577 Document The SDK exports 🤖 Prompt for AI Agents |
||
| ``` | ||
|
|
||
| Reads go over the IPFS HTTP gateway, so no local daemon is required and the same | ||
| code runs in a browser. Resolution is by CID for now; STAC catalog support will | ||
| follow. | ||
|
|
||
| ### Siren REST API usage | ||
|
|
||
| Use Siren methods by configuring `siren` in the client options. | ||
|
|
||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,7 +31,8 @@ | |
| "test:ci": "vitest run", | ||
| "test:coverage": "vitest run --coverage", | ||
| "prepare": "npm run build", | ||
| "prepublishOnly": "npm run build" | ||
| "prepublishOnly": "npm run build", | ||
| "inspect:stations": "tsx scripts/inspect-stations.ts" | ||
| }, | ||
| "keywords": [ | ||
| "dclimate", | ||
|
|
@@ -52,7 +53,9 @@ | |
| "homepage": "https://github.com/dClimate/dclimate-client-js#readme", | ||
| "dependencies": { | ||
| "@dclimate/jaxray": "^0.7.0", | ||
| "@opentelemetry/api": "^1.9.1" | ||
| "@opentelemetry/api": "^1.9.1", | ||
| "@dclimate/dparquet": "file:../dparquet", | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. HIGH There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. HIGH |
||
| "multiformats": "^14.0.4" | ||
| }, | ||
| "devDependencies": { | ||
| "@eslint/js": "10.0.1", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,218 @@ | ||
| /** | ||
| * Load and inspect a station dataset through `client.stations`. | ||
| * | ||
| * The station counterpart to `inspect-dataset.ts`: that one walks a gridded | ||
| * Zarr dataset, this one walks point observations. Both go through the same | ||
| * client, which is the thing worth seeing -- station data is not a separate | ||
| * SDK, just a second namespace. | ||
| * | ||
| * Usage: | ||
| * npx tsx scripts/inspect-stations.ts <cid> | ||
| * npx tsx scripts/inspect-stations.ts <cid> --station USW00094728 --element TMAX | ||
| * npx tsx scripts/inspect-stations.ts <cid> --near 40.78,-73.97 --from 2025-07-01 --to 2025-07-08 | ||
| * | ||
| * Options: | ||
| * --gateway <url> IPFS HTTP gateway (default http://127.0.0.1:8080) | ||
| * --station <id> Restrict to one station id (repeatable) | ||
| * --near <lat,lon> Use the single nearest station to a point instead | ||
| * --element <name> Restrict to one element, e.g. TMAX, TMIN, PRCP (repeatable) | ||
| * --from <date> ISO date, inclusive | ||
| * --to <date> ISO date, inclusive | ||
| * --limit <n> Rows to print (default 10; 0 prints all) | ||
| * --plan Show what would be fetched, then stop | ||
| * | ||
| * Requires an IPFS gateway that can serve the dataset's blocks. A local Kubo | ||
| * daemon (`ipfs daemon`) is the usual answer; any gateway works. | ||
| */ | ||
|
|
||
| import { DClimateClient } from "../src/index.js"; | ||
| import type { StationDataset, StationInfo } from "@dclimate/dparquet/reader"; | ||
|
|
||
| const DEFAULT_GATEWAY = "http://127.0.0.1:8080"; | ||
|
|
||
| interface Args { | ||
| cid: string; | ||
| gateway: string; | ||
| stations: string[]; | ||
| near: [number, number] | null; | ||
| elements: string[]; | ||
| from: string | null; | ||
| to: string | null; | ||
| limit: number; | ||
| plan: boolean; | ||
| } | ||
|
|
||
| const USAGE = `Usage: | ||
| npx tsx scripts/inspect-stations.ts <cid> [options] | ||
|
|
||
| Options: | ||
| --gateway <url> IPFS HTTP gateway (default ${DEFAULT_GATEWAY}) | ||
| --station <id> Restrict to one station id (repeatable) | ||
| --near <lat,lon> Use the single nearest station to a point | ||
| --element <name> Restrict to one element, e.g. TMAX (repeatable) | ||
| --from <date> ISO date, inclusive | ||
| --to <date> ISO date, inclusive | ||
| --limit <n> Rows to print (default 10; 0 prints all) | ||
| --plan Show what would be fetched, then stop`; | ||
|
|
||
| function parseArgs(argv: string[]): Args { | ||
| const args: Args = { | ||
| cid: "", | ||
| gateway: process.env.IPFS_GATEWAY_URL ?? DEFAULT_GATEWAY, | ||
| stations: [], | ||
| near: null, | ||
| elements: [], | ||
| from: null, | ||
| to: null, | ||
| limit: 10, | ||
| plan: false, | ||
| }; | ||
|
|
||
| for (let i = 0; i < argv.length; i += 1) { | ||
| const arg = argv[i]!; | ||
| // A flag whose value is missing is a typo, not a request for the default: | ||
| // silently falling back would produce a plausible-looking wrong answer. | ||
| const value = (): string => { | ||
| const next = argv[i + 1]; | ||
| if (next === undefined || next.startsWith("--")) { | ||
| throw new Error(`${arg} needs a value`); | ||
| } | ||
| i += 1; | ||
| return next; | ||
| }; | ||
|
|
||
| switch (arg) { | ||
| case "--gateway": args.gateway = value(); break; | ||
| case "--station": args.stations.push(value()); break; | ||
| case "--element": args.elements.push(value().toUpperCase()); break; | ||
| case "--from": args.from = value(); break; | ||
| case "--to": args.to = value(); break; | ||
| case "--limit": args.limit = Number(value()); break; | ||
| case "--plan": args.plan = true; break; | ||
| case "--near": { | ||
| const parts = value().split(","); | ||
| const lat = Number(parts[0]); | ||
| const lon = Number(parts[1]); | ||
| if (parts.length !== 2 || !Number.isFinite(lat) || !Number.isFinite(lon)) { | ||
| throw new Error("--near expects <lat,lon>, e.g. --near 40.78,-73.97"); | ||
| } | ||
| args.near = [lat, lon]; | ||
|
Comment on lines
+90
to
+99
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Validate numeric arguments against their documented domains. Fractional Proposed fix- case "--limit": args.limit = Number(value()); break;
+ case "--limit": {
+ const limit = Number(value());
+ if (!Number.isInteger(limit) || limit < 0) {
+ throw new Error("--limit must be a non-negative integer");
+ }
+ args.limit = limit;
+ break;
+ }
case "--plan": args.plan = true; break;
case "--near": {
const parts = value().split(",");
const lat = Number(parts[0]);
const lon = Number(parts[1]);
- if (parts.length !== 2 || !Number.isFinite(lat) || !Number.isFinite(lon)) {
+ if (
+ parts.length !== 2 ||
+ !Number.isFinite(lat) ||
+ !Number.isFinite(lon) ||
+ lat < -90 || lat > 90 ||
+ lon < -180 || lon > 180
+ ) {
throw new Error("--near expects <lat,lon>, e.g. --near 40.78,-73.97");
}Also applies to: 109-112 🤖 Prompt for AI Agents |
||
| break; | ||
| } | ||
| default: | ||
| if (arg.startsWith("--")) throw new Error(`Unknown option: ${arg}`); | ||
| if (args.cid) throw new Error(`Unexpected argument: ${arg}`); | ||
| args.cid = arg; | ||
| } | ||
| } | ||
|
|
||
| if (!args.cid) throw new Error("A root CID is required"); | ||
| if (!Number.isFinite(args.limit) || args.limit < 0) { | ||
| throw new Error("--limit must be a non-negative number"); | ||
| } | ||
| return args; | ||
| } | ||
|
|
||
| const day = (date: Date): string => date.toISOString().slice(0, 10); | ||
|
|
||
| function describeStation(station: StationInfo): string { | ||
| const where = | ||
| station.latitude === null || station.longitude === null | ||
| ? "no position" | ||
| : `${station.latitude.toFixed(4)}, ${station.longitude.toFixed(4)}`; | ||
| return `${station.stationId} ${where} ${day(station.start)} .. ${day(station.end)}`; | ||
| } | ||
|
|
||
| function applySelection(dataset: StationDataset, args: Args): StationDataset { | ||
| let selected = dataset; | ||
|
|
||
| if (args.near) { | ||
| const [lat, lon] = args.near; | ||
| selected = selected.nearest(lat, lon); | ||
| console.log(`\nNearest station to ${lat}, ${lon}: ${selected.toQuery().stations?.[0]}`); | ||
| } else if (args.stations.length > 0) { | ||
| selected = selected.select(...args.stations); | ||
| } | ||
|
Comment on lines
+129
to
+135
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Reject conflicting station selectors. When callers pass both Proposed fix function applySelection(dataset: StationDataset, args: Args): StationDataset {
+ if (args.near && args.stations.length > 0) {
+ throw new Error("--near cannot be used with --station");
+ }
+
let selected = dataset;🤖 Prompt for AI Agents |
||
|
|
||
| if (args.elements.length > 0) selected = selected.elements(...args.elements); | ||
|
|
||
| if (args.from || args.to) { | ||
| // Either bound alone is meaningful, so the missing side widens to the | ||
| // dataset's own extent rather than forcing the caller to pass both. | ||
| const covered = dataset.stations; | ||
| const earliest = Math.min(...covered.map((s) => s.start.getTime())); | ||
| const latest = Math.max(...covered.map((s) => s.end.getTime())); | ||
| selected = selected.timeRange({ | ||
| start: args.from ?? new Date(earliest), | ||
| end: args.to ?? new Date(latest), | ||
| }); | ||
| } | ||
|
|
||
| return selected; | ||
| } | ||
|
|
||
| async function main(): Promise<void> { | ||
| const argv = process.argv.slice(2); | ||
| if (argv.length === 0 || argv.includes("--help") || argv.includes("-h")) { | ||
| console.log(USAGE); | ||
| process.exit(argv.length === 0 ? 1 : 0); | ||
| } | ||
|
|
||
| const args = parseArgs(argv); | ||
| const client = new DClimateClient({ gatewayUrl: args.gateway, stacServerUrl: null }); | ||
|
|
||
| console.log(`Loading station dataset ${args.cid}`); | ||
| console.log(`Gateway: ${args.gateway}\n`); | ||
|
|
||
| const dataset = await client.stations.load({ cid: args.cid }); | ||
|
|
||
| console.log(`Stations (${dataset.stations.length}):`); | ||
| for (const station of dataset.stations) console.log(` ${describeStation(station)}`); | ||
|
|
||
| const selected = applySelection(dataset, args); | ||
| console.log(`\nQuery (wire units): ${JSON.stringify(selected.toQuery())}`); | ||
|
|
||
| const plan = await selected.plan(); | ||
| const bytes = plan.fragments.reduce((sum, f) => sum + f.byteLength, 0); | ||
| const rows = plan.fragments.reduce((sum, f) => sum + f.rowCount, 0); | ||
| console.log( | ||
| `Plan: ${plan.fragments.length} fragment(s), ` + | ||
| `${plan.stations.length} station(s), ` + | ||
| `${rows} row(s), ${(bytes / 1024).toFixed(1)} KiB` | ||
| ); | ||
| // The number that shows predicate pushdown doing something: fragments ruled | ||
| // out by column statistics are never fetched at all. | ||
| console.log(` ${plan.stats.fragmentsPruned} fragment(s) pruned by statistics`); | ||
|
|
||
| if (args.plan) return; | ||
|
|
||
| const started = Date.now(); | ||
| const element = args.elements.length === 1 ? args.elements[0] : undefined; | ||
| const records = await selected.toRecords(element); | ||
| const elapsed = Date.now() - started; | ||
|
|
||
| console.log(`\n${records.length} record(s) in ${elapsed} ms`); | ||
|
|
||
| const shown = args.limit === 0 ? records : records.slice(0, args.limit); | ||
| for (const record of shown) { | ||
| const value = | ||
| element === undefined | ||
| ? JSON.stringify(record.values) | ||
| : String(record.value ?? "—"); | ||
| console.log(` ${day(record.time as Date)} ${record.stationId} ${value}`); | ||
| } | ||
| if (shown.length < records.length) { | ||
| console.log(` ... ${records.length - shown.length} more (--limit 0 for all)`); | ||
| } | ||
|
|
||
| // Stored in NOAA's own scaling rather than converted, so the archive's exact | ||
| // integers survive. Saying so beats letting a reader assume whole °C. | ||
| if (element) { | ||
| console.log(`\nValues are integers in tenths (TMAX 317 = 31.7 °C).`); | ||
| } | ||
| } | ||
|
|
||
| main().catch((error: unknown) => { | ||
| console.error(`\nError: ${error instanceof Error ? error.message : String(error)}`); | ||
| process.exit(1); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Use a valid CID in the loading example.
StationsClient.loadparsesrequest.cidwithCID.parse. The"bafyr4i..."value is invalid, so this example throwsDatasetNotFoundErrorwhen copied. Use a full valid CID or mark the value as a non-runnable placeholder.Proposed documentation fix
📝 Committable suggestion
🤖 Prompt for AI Agents