Skip to content

Comprehensive audit: 12 proven bugs fixed, 160x spatial selection, test suite restored from 14 to 166 local tests - #14

Merged
TheGreatAlgo merged 49 commits into
mainfrom
fix/review-findings
Jul 21, 2026
Merged

Comprehensive audit: 12 proven bugs fixed, 160x spatial selection, test suite restored from 14 to 166 local tests#14
TheGreatAlgo merged 49 commits into
mainfrom
fix/review-findings

Conversation

@Faolain

@Faolain Faolain commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator

This PR is the output of a full library audit followed by a strict TDD pipeline (the same process as dClimate/py-hamt#88). Every item below was implemented as a pair of commits — a red commit with a failing test that reproduces the defect, then a green commit with the fix — so reviewers can check out any red commit and watch the test fail. Every fix was then reviewed by two independent reviewers (Codex implemented under a verifying harness; Grok and an adversarial Claude reviewer audited each diff, with blocking findings driving remediation commits).

Results: 166 tests pass locally (up from 14 on main — the old autouse IPFS gate silently skipped 144 tests including ~85 fully-offline unit tests), ruff lint + format clean. Two HIGH bugs that made shipped features completely unusable (polygons(), rolling_aggregation() on any grid with a masked cell) are fixed, all three previously-unproven suspicions from the audit were confirmed real and fixed, and the client now tracks py-hamt PR #88 (review-fixes-integration) — validated against the live gateway: the network suite passes on that branch with no upstream issues found.

Benchmarks (same machine, measured by the red tests / adversarial reviewers)

Path main This PR
rectangle() tiny selection on 41 MB dataset (peak alloc) 39–40 MB (full deep copy) 0.02–0.10 MB (zero-copy .sel view)
circle() small radius (peak alloc) 40.1 MB 0.17 MB (bbox pre-crop)
concatenate_datasets() 40 datasets × 200k float64 1.65 s, 3.04× final-size peak 0.06 s, 2.06×
Event-loop stall per STAC resolve inside async load_dataset() ~256 ms (blocks py-hamt chunk fetches) <10 ms (asyncio.to_thread)
points() coordinate extraction per-Point Python loop (16× slower at 50k pts) vectorized GeoSeries.y/.x
EncryptionCodec 16 KB chunk thread hop every op (+79%) inline < 128 KiB (1000×16 KB decodes: 222 ms → 84 ms)
import dclimate_client_py 973 ms 484 ms (lazy s3fs/geopandas/pystac)

1. Geotemporal core (geotemporal_data.py)

Fix Issue What was wrong → what changed
4f6fd46 G1 (feature dead) polygons() was broken three independent ways: rioxarray was never imported so self.data.rio raised AttributeError in a fresh interpreter; except errors.NoDataInBounds referenced an exception class that doesn't exist; and the fallback called reduce_polygon_to_point on the xarray Dataset instead of the wrapper. Every path crashed. Now: function-level rioxarray import (keeps the dep optional), catches rioxarray.exceptions.NoDataInBounds, correct receiver — and the inplace rio mutations of the caller's dataset are gone (ed0b248 adds inplace=False + a non-mutation regression pin after review caught rioxarray's surprising inplace=True default).
4f6fd46 G2 (feature dead) rolling_aggregation() used .dropna("time") (how="any"), so a single masked ocean/coast cell — ubiquitous in climate grids — emptied the entire time dimension. Now trims only the incomplete leading windows (isel(time=slice(window_size-1, None))); the adversarial reviewer verified the new semantics match the docstring and are identical() to the old output on dense data.
3c7586c G3 check_dataset_size() crashed with TypeError on scalar/zero-dim datasets (reduce with no initializer). Initializer 1; a scalar counts as one point (pinned both directions).
3c7586c G4 to_netcdf() permanently deleted date range, bbox, etc. from the caller's dataset attrs — attrs that as_dict() and s3 retrieval later read. Serializes from a copy; byte-identical output verified.
3c7586c G5 temporal_aggregation("quarter") used the pandas alias 'Q' — deprecated on installed pandas 2.2.3, removed in pandas 3. Now 'QE'; resample output verified identical, warning-free.
ab22db1 G6 tolerance=10e-5 (= 1e-4 ≈ 11 m) in point()/points() snap_to_grid=False silently snapped points documented to raise NoDataFoundError. Now 1e-5; the adversarial reviewer's float-representation attack (worst-case float32 half-ulp 7.6e-6) confirmed the tighter bound is safe for real grids.
929ee05 P1 (160×) rectangle()'s .where(mask, drop=True) deep-copied the full dataset via xr.align before cropping — 89% of a profiled end-to-end query. Replaced with .sel coordinate slices (ascending and descending coords); circle() pre-crops to a haversine bounding box (pole crossings widen to the full longitude band, antimeridian spans fall back to the old path). Output pinned identical by 6 semantics tests; 97f832d adds a mask-path fallback for exotic layouts (non-monotonic/NaN/non-index coords) after the adversarial reviewer's 400-case differential fuzzer + mutation testing found silent row-drops there and two missing pins (descending-longitude, high-latitude cos-widening).
76b4e7e P5 points() iterated shapely points in Python ([p.y for p in mask]). Vectorized GeoSeries.y/.x; 8e75c68 adds a guard rejecting missing (None) geometries, which the vectorized path would otherwise silently snap to an arbitrary grid cell.

2. STAC resolution, catalog and listing (stac_server.py, stac_catalog.py)

Fix Issue What was wrong → what changed
3c7586c S1 STAC-server variant resolution had no item-id fallback (parity break with the catalog resolver — silently forced the slow IPFS walk) and raised KeyError on features lacking properties. Variant now derives from the item id when properties are absent; all property access is None-safe.
4f7f7b2 S2 (confirmed suspect) The two resolvers parsed hyphenated ids differently: the catalog reparsed chirps-precip-daily-final-p05 as dataset precip. Both resolvers and both listers now share one dataset-aware, hyphen-aware grammar (_dataset_and_variant_from_item_id).
4f7f7b2 S3 (confirmed suspect) /search used a fixed limit: 100 and ignored rel=next — datasets beyond the first page were unresolvable. Pagination now follows next links (POST/GET, bounded 50 pages, repeat-page guard); da83c25 also honors the STAC merge: true body contract after the adversarial reviewer demonstrated page-2 requests dropping the collections filter could resolve a poisoned foreign-collection CID.
d1b23a2 + da83c25 S4 The listing APIs report bare items (no variant segment/property) as variant "default", but resolve(variant="default") failed on them — breaking list→resolve round-trips, with server and catalog disagreeing on the documented fallback path. Bare items now count as "default" uniformly across both resolvers and both listers.
ab29a23 + 12bf2e5 S5 The IPFS catalog lister ignored dclimate:dataset_id/dclimate:variant properties (inventing wrong names for hyphenated property-bearing items) — and the first fix's hint-parsing dropped items whose ids don't encode their properties (caught by the final adversarial gate). Both listers now parse unhinted and overlay explicit properties; both match paths gained the same fallback.
364842a P3 IPFSStacIO issued a bare sessionless requests.get per ipfs:// read — a fresh TCP+TLS handshake each time, with no timeout. One pooled requests.Session with a 30 s timeout (8040d54 extends the timeout to get_root_catalog_cid, the first call of every catalog load).

3. Client, async and I/O (dclimate_client.py, concatenate.py, ipfs_retrieval.py, s3_retrieval.py, encryption_codec.py)

Fix Issue What was wrong → what changed
ab22db1 + 8996335 C1 __aexit__ leaked the KuboCAS session if Siren aclose() raised. Now try/finally with full dual-failure semantics per review: AsyncExitStack convention (later error propagates with the earlier as __context__), CancelledError from either cleanup outranks ordinary errors, _kubo_cas cleared on every path — the whole exception matrix is pinned by tests.
364842a P2 Blocking requests calls ran directly on the event loop inside async load_dataset(), freezing py-hamt's concurrent chunk fetches (~204–256 ms per STAC call). All blocking resolution now runs via asyncio.to_thread, with an asyncio.Lock-guarded catalog lazy-init (double-checked locking verified race-free with a widened race window); 8040d54 adds alist_datasets() since the sync list_datasets() had the same disease.
364842a P4 concatenate_datasets() did iterative xr.concat — O(n²) time, ~2× peak memory. Slices are collected and concatenated once; the adversarial reviewer's 13-case differential harness (including a dataset overlapping its grand-predecessor) found zero output differences.
4f7f7b2 + da83c25 S6 (confirmed suspect) Connection errors were classified by substring matching on str(exc) — unrelated Zarr metadata text containing "timeout" was treated as a gateway failure and skipped the HAMT fallback. Now classified by exception type. The first typed version used blanket OSError, which grok proved was a new BLOCKER (FileNotFoundError, requests.HTTPError — an IOError subclass — also skipped the fallback); the final classifier matches only connection/timeout/DNS families + requests/urllib3/httpx-transport/aiohttp types, pinned positive and negative.
3c7586c C2 get_dataset_from_s3() crashed with AttributeError when the optional update_in_progress attr was absent (the same attr is guarded everywhere else). All optional update flags guarded; all three update-flag branches now covered.
76b4e7e P6 EncryptionCodec hopped through asyncio.to_thread (~60 µs) for every chunk — +79% overhead at 16 KB. Crypto runs inline under 128 KiB, still offloads above; roundtrips, exception parity and the dispatch boundary itself are pinned.
76b4e7e P7 Importing the package eagerly pulled s3fs, geopandas and pystac (~34% of import time). Lazy via module __getattr__ + function-level imports; all 29 public names, pickling, isinstance stability and star-imports verified; __dir__ added per PEP 562.

4. Test infrastructure (the biggest process risk)

Fix Issue What was wrong → what changed
2e594dc T1 One autouse session fixture skipped every test when no local IPFS node answered on 127.0.0.1:8080 — including ~85 fully-offline mocked unit tests. Only 14 tests actually ran locally. The gate is now marker-based (ipfs for gateway/daemon tests, existing integration gating kept); the adversarial reviewer verified marker triage empirically with a socket-blocking harness (109 unit tests pass fully offline, zero over- or under-marking).
2e594dc T2 Async tests silently skipped (PytestUnhandledCoroutineWarning; a hard error on pytest 9). asyncio_mode = "auto" — and after both reviewers independently caught the meta-test being tautological, it now generates an unmarked async def test to prove the config is load-bearing.
2e594dc T3 Bare pytest collected the stray root-level test_stac_integration.py and examples/; tests/test_debug.py could not fail (every check wrapped in try/except, zero assertions). testpaths = ["tests"], script relocated to scripts/, test_debug.py deleted.

5. Build, deps, docs

Fix What changed
81e6f8b py_hamt pinned to PR dClimate/py-hamt#88 (review-fixes-integration) via [tool.uv.sources]drop this pin once #88 merges/releases. Validated against the live gateway: 24 network tests pass on that branch; the single failure needs a local Kubo daemon (RPC upload), not a py-hamt defect.
09666d7 All runtime deps floored; aiohttp/urllib3/multiformats (direct imports) declared; pyarrow removed, python-dotenv moved to an examples extra. The audit's "scipy unused" claim was wrong — xarray's in-memory to_netcdf() needs the scipy netCDF backend (removal made a test fail); kept with a comment.
571ee1a README documented a geo_utils.py that no longer exists (→ geotemporal_data.py); codecov badge pointed at the old dClimate-Zarr-Client repo. query()/forecast() accept ISO strings (mypy arg-type error at the only call site).

Compatibility notes

  • P1: rectangle() on regular grids now returns a zero-copy view of the parent dataset (documented), and integer data variables keep their dtype instead of being upcast to float64 by .where — both intentional; values are pinned identical.
  • G2: rolling_aggregation() on grids with NaN cells now returns NaN-holding rows instead of a silently truncated (typically empty) time axis — this is the documented behavior; dense-data output is unchanged.
  • S6: gateway 5xx after retries now takes the HAMT fallback path instead of surfacing as IpfsConnectionError (the old behavior matched only when the status text happened to contain "timeout").
  • The py-hamt git-branch pin is intentional for this branch and should be replaced with a release floor when Comprehensive audit: 20 proven bugs fixed, HTTP/2 enabled, 3-10x performance gains py-hamt#88 lands.

How this was verified

  • Each red commit's tests fail on its parent and pass after the green commit; reviewers re-verified red tests were not weakened through green (the one edited red test — the async-mode meta-test — was flagged by both reviewers as tautological and made stronger).
  • Every work package was independently reviewed twice (Grok + adversarial Claude). Adversarial passes ran differential harnesses old-vs-new (400 fuzz cases + mutation testing on the spatial rewrite; 13-case concat differential; full __aexit__ exception matrix; live probing of api.stac.dclimate.net for the pagination contract). Reviewer findings drove 10 remediation commits, including one BLOCKER (over-broad OSError classification) and one FIX-FIRST (server/catalog default-variant divergence).
  • Full gates on the merged tree: pytest 166 passed / 47 skipped (network-marked; the ipfs-marked set passes against the live gateway except the one test requiring a local Kubo daemon), ruff check and ruff format --check clean.

Depends on dClimate/py-hamt#88 (tracked via the uv source pin).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added alist_datasets() for async dataset discovery without blocking.
    • Enhanced client configuration (concurrency, retry/backoff, custom headers/auth, and HTTP client factory); improved public gateway default behavior.
  • Bug Fixes
    • Improved geospatial selection/tolerance (snap-to-grid behavior, stricter non-snap matching, and stronger circle/rectangle/polygon reductions), forecast/query input flexibility (ISO strings), quarter aggregation compatibility, rolling-window alignment, and NetCDF serialization.
    • More robust dataset retrieval across STAC, IPFS, and S3 update states, including clearer variant/CID resolution.
  • Performance
    • Reduced event-loop stalls via background IO, optimized single-call concatenation, and improved chunk encryption offloading.
  • Documentation
    • Updated README references and refreshed STAC integration usage; adjusted dataset-catalog examples for structured CID/variant resolution.

Faolain and others added 29 commits July 18, 2026 01:50
…source pin

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…async skips, stray collection, assertion-free test_debug)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ction to tests/

- conftest: replace the autouse skip-everything IPFS gate with
  pytest_collection_modifyitems gating only @pytest.mark.ipfs tests
- pyproject: testpaths=["tests"], asyncio_mode=auto
- mark genuinely gateway/daemon-dependent tests with ipfs/integration
- relocate root-level test_stac_integration.py to scripts/ (manual script)
- delete assertion-free tests/test_debug.py (coverage exists elsewhere)
- meta-test hardening per grok + adversarial review: probe asyncio_mode
  with a generated unmarked async test; drop dead fixture overrides

Locally-executed tests: 14 -> 109 (47 network-dependent skip cleanly).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nce typo

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ttrs, s3 attr guard, stac-server variants, quarter alias)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…aggregation NaN wipe

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…0e-5 -> 1e-5

__aexit__ now closes KuboCAS via try/finally even when the Siren client's
aclose() raises, then re-raises the original error. The snap_to_grid=False
tolerance in point()/points() was 10e-5 (= 1e-4, ~11 m), silently snapping
points documented to raise NoDataFoundError.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ted variants, STAC pagination, substring error classification)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ceiver); stop rolling_aggregation wiping NaN-celled grids

polygons(): import rioxarray in-function (registers the .rio accessor and
keeps the dependency optional), catch rioxarray.exceptions.NoDataInBounds
instead of the nonexistent errors.NoDataInBounds, call
reduce_polygon_to_point on the GeotemporalData wrapper rather than the
Dataset, and drop the inplace rio mutations of the caller's dataset.

rolling_aggregation(): dropna(time, how=any) emptied the whole time axis
whenever any grid cell was NaN (ubiquitous in masked climate grids); trim
only the incomplete leading windows instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ention, CancelledError priority)

Follow-up to grok + adversarial review of WP-low: on dual cleanup failure
propagate the later (Kubo) error with the Siren error as __context__
instead of a false __cause__ chain; a CancelledError from either cleanup
always outranks an ordinary error. Adds dual-failure, cancellation, and
with-block-forwarding tests; drops dead no-op fixtures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…or rectangle()/circle()

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… and O(n^2) concatenate

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ll chunks, eager heavy imports

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s3 attrs, variant item-id fallback, QE quarter alias

- check_dataset_size: reduce initializer 1 (zero-dim datasets = 1 point)
- to_netcdf: serialize from a copy; caller attrs no longer stripped
- get_dataset_from_s3: optional update_in_progress/initial_parse/
  update_is_append_only attrs no longer raise AttributeError
- stac_server: variant matching falls back to the item id when
  properties lack dclimate:variant, and tolerates missing properties
- temporal_aggregation: pandas quarter alias Q -> QE

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ession test, dead fixture cleanup)

Per grok + adversarial review: rioxarray set_spatial_dims defaults to
inplace=True, so pass inplace=False explicitly; replace the post-fix
duplicate fallback test with a caller-dataset non-mutation pin; drop the
no-op check_ipfs_connection overrides copied across the new test files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
rectangle(): .where(mask, drop=True) deep-copied the full dataset via
xr.align before cropping (measured 160x slower, 379 MB peak for a 4 KB
result); replaced with .sel coordinate slices handling ascending and
descending coord order, with empty selections normalized to the old
drop=True shape. circle(): pre-crop to the circle's bounding box (pole
crossings widen to the full longitude band; antimeridian-spanning boxes
fall back to the uncropped path) before the haversine mask, output
unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…pe-based connection-error classification

- stac_server/stac_catalog: dataset/variant parsing out of item ids is now
  shared and dataset-aware, so hyphenated dataset ids and hyphenated
  variants resolve identically in both resolvers
- stac_server: /search follows rel=next links (bounded at 50 pages, loop
  guard) instead of silently truncating at the first 100 items
- ipfs_retrieval: connection errors classified by exception type
  (requests/httpx/OSError families, incl. __cause__ chain) instead of
  substring matching on str(exc); existing substring-pinned test updated
  to real exception types

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…calar/variant test pins

Per grok + adversarial review of WP-med: the listing API reports items
without a variant segment as 'default', but resolve derived None for the
same items, breaking list->resolve round-trips and skipping unnamed base
items in the no-variant preference order. Bare items now count as the
'default' variant in both the page-scan early-exit and final selection.
Adds reviewer-requested pins: default round-trip, unnamed-over-latest
preference, s3 update-flag branch coverage, scalar=1-point semantics.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ssion; single-pass concatenate

- load_dataset/list fallback paths run resolve_cid_from_stac_server,
  load_stac_catalog, list_available_datasets and
  resolve_dataset_cid_from_stac via asyncio.to_thread (measured 204 ms
  event-loop stall per STAC call previously froze py-hamt's concurrent
  chunk fetches); catalog lazy-init guarded by an asyncio.Lock
- IPFSStacIO uses one pooled requests.Session with a 30 s timeout
  instead of a fresh TCP+TLS handshake per ipfs:// read
- concatenate_datasets collects deduplicated slices and concatenates
  once (iterative xr.concat was O(n^2) time and ~2x peak memory)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… imports

- points() uses GeoSeries .y/.x vectorized accessors (16x at 50k points
  vs the per-Point python loop)
- EncryptionCodec runs encrypt/decrypt inline for buffers < 128 KiB and
  keeps the asyncio.to_thread offload for larger ones (+79% overhead
  measured at 16 KB)
- s3fs, geopandas and pystac import lazily (function-level / module
  __getattr__); package import time 973 ms -> 484 ms
- red tests gain a key-restoration fixture (they leaked the process-wide
  encryption key into later tests); assertions unchanged

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…circle()

Per grok + adversarial (differential/mutation) review of the zero-copy
rewrite: .sel slicing silently drops rows on non-monotonic coords and
errors on non-index/NaN/scalar coords that the old mask handled. Both
selectors now verify coords are 1-D NaN-free monotonic dimension indexes
and fall back to the legacy .where path otherwise. Adds the two pins
mutation testing showed missing (descending-longitude rectangle,
high-latitude cos-widened circle bbox) plus non-monotonic/NaN fallback
pins; documents the zero-copy view and int-dtype preservation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…, STAC merge next-links, unified lister parsing

Review follow-ups for the suspects package (grok BLOCKER + adversarial
FIX FIRST both addressed):
- _is_connection_error no longer treats every OSError as a gateway
  failure: FileNotFoundError/PermissionError (and requests.HTTPError,
  which subclasses IOError) previously skipped the HAMT fallback; the
  classifier now matches only connection/timeout/DNS families plus
  requests/urllib3/httpx-transport/aiohttp types, with positive and
  negative parametrized pins. Note: gateway 504s (HTTPStatusError) now
  intentionally take the HAMT fallback instead of IpfsConnectionError.
- resolve_dataset_cid_from_stac treats bare items as variant 'default',
  mirroring the STAC-server resolver, so the documented server->catalog
  fallback resolves identically (adversarial repro: bare 'chirps-temp'
  succeeded on the server path and raised on the catalog path)
- _search_pages honors the STAC next-link 'merge: true' contract so
  page-2 requests keep the collections filter (poisoning repro fixed)
- the IPFS catalog lister now uses the shared hyphen-aware id parsing
  and reports bare items as 'default', matching the server lister

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…param, thread-safety notes)

Per grok + adversarial review: async alist_datasets() offloads the
blocking STAC/catalog work and shares the catalog lazy-init lock (the
sync list_datasets now documents that it blocks); get_root_catalog_cid
gains the missing 30 s timeout (it is the first call of every catalog
load); find_split_index drops its never-read combined_coords parameter;
session/set_default thread-safety constraints documented in place.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…est, __dir__ for lazy modules, typed catalog)

Per grok review: large-chunk roundtrips now assert exactly two thread
offloads (encode + decode); the 128 KiB dispatch boundary itself is
pinned; lazy modules implement __dir__ so introspection matches __all__;
_stac_catalog regains its pystac.Catalog annotation via TYPE_CHECKING.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adversarial review of the vectorized points() found GeoSeries.y maps
missing (None) geometries to NaN, which .sel(method='nearest') silently
snaps to an arbitrary grid cell — now raises InvalidSelectionError.
Also applies ruff format across the repo (pre-existing violations were
mixed with new hunks; pre-commit runs ruff-format).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nused ones

Every runtime dep now has a minimum version. aiohttp, urllib3 and
multiformats are imported directly and are now declared instead of
arriving transitively. pyarrow (unused) and python-dotenv (used only by
examples/, now an 'examples' extra) removed from runtime deps. scipy
stays with a comment: the peer review flagged it unused, but xarray's
in-memory to_netcdf() path requires the scipy netCDF backend (removal
made test_to_netcdf_preserves_original_dataset_attributes fail).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…trings in query()

README documented a geo_utils.py that no longer exists (now
GeotemporalData in geotemporal_data.py) and the codecov badge pointed at
the old dClimate-Zarr-Client repo. query()'s forecast_reference_time is
widened to str | datetime — xarray .sel accepts ISO strings and
client.geo_temporal_query has always passed one (mypy arg-type error).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ge-link and HTTPError pins, forecast typing)

Per the final overall grok review: the IPFS catalog lister now prefers
dclimate:dataset_id/variant properties like the server lister (its
id-only split invented wrong names for hyphenated property-bearing
items); adds regression pins for the merge:true next-link collections
filter and for requests.HTTPError staying non-connection; drops the dead
None entry from the catalog preference order; forecast() accepts ISO
strings to match query().

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-parse fallbacks, dead code removal)

The catalog lister now parses ids unhinted and overlays dclimate:*
properties exactly like the server lister — the hint-parse approach
dropped items whose ids don't encode their properties (e.g. properties
variant 'default' on a bare id); both match paths gain the same
fallback. Deletes the ~30-line commented-out org-inference block,
fixes circle()'s return annotation/docstring typo, and repositions the
rolling_aggregation comment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The pull request adds lazy imports, asynchronous STAC and catalog access, structured dataset variant resolution, optimized geospatial selection, revised retrieval behavior, dependency updates, and expanded regression and pytest infrastructure coverage.

Changes

Runtime, retrieval, and data processing

Layer / File(s) Summary
Client runtime and asynchronous access
dclimate_client_py/__init__.py, dclimate_client_py/dclimate_client.py, dclimate_client_py/encryption_codec.py
Public symbols and optional dependencies load lazily; blocking operations run in threads; asynchronous listing, cleanup handling, and size-based encryption offloading are added.
Retrieval and concatenation
dclimate_client_py/s3_retrieval.py, dclimate_client_py/ipfs_retrieval.py, dclimate_client_py/concatenate.py
S3 and IPFS retrieval use updated metadata, error classification, explicit Zarr decoding, and single-call concatenation.
Geospatial selection and serialization
dclimate_client_py/geotemporal_data.py, dclimate_client_py/client.py
Spatial selection, tolerance validation, polygon fallback, aggregation, serialization, and public type annotations are updated.

STAC resolution

Layer / File(s) Summary
Server and catalog resolution
dclimate_client_py/stac_server.py, dclimate_client_py/stac_catalog.py
STAC resolution returns ResolvedDataset, follows bounded pagination, applies property-first dataset parsing, and manages catalog HTTP clients explicitly.

Project and test infrastructure

Layer / File(s) Summary
Regression coverage and project configuration
tests/*, pyproject.toml, README.md, DATASET_CATALOG_USAGE.md, scripts/*
Tests cover async cleanup, STAC matching, spatial behavior, retrieval, lazy imports, pytest boundaries, dependency constraints, documentation, and benchmark CLI behavior.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested reviewers: thegreatalgo

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the PR’s broad scope: bug fixes, spatial selection improvements, and expanded/restored tests.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/review-findings

Comment @coderabbitai help to get the list of available commands.

Faolain and others added 4 commits July 18, 2026 04:14
…way h2 benchmark script

dClimateClient now accepts concurrency, headers, auth, max_retries,
initial_delay, backoff_factor and client_factory, forwarded to KuboCAS
only when set so py-hamt's defaults stay authoritative — unblocking
authenticated gateways, tuned pools, and h2-on/off comparisons.
scripts/benchmark_gateway.py implements the py-hamt #58 methodology
(median wall time, --http2/--no-http2, --concurrency, --repetitions)
and documents the companion infra change: raising nginx
keepalive_requests on the gateway to stop routine GOAWAY churn.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ncy list, pooled client) + fallback non-regression pin

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…llib3

stac_server and stac_catalog use pooled sync httpx.Clients (IPFSStacIO
keeps a sync client because pystac's StacIO interface is synchronous);
fallback guards in dclimate_client catch httpx.HTTPError; the
connection-error classifier drops the requests/urllib3 families; the
nine transport-pinned test files migrate to an httpx.MockTransport seam
with every invariant preserved (request bodies, pagination sequences,
single-pooled-client reuse, the event-loop heartbeat bound); requests
and urllib3 leave [project] dependencies (still present transitively).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@0xSwego

0xSwego commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator

Review follow-up for db7444b:

All 12 inline review threads have been addressed, replied to, and resolved. I also fixed the substantive top-level pystac issue by binding IPFSStacIO per catalog load instead of mutating pystac process-global state.

Additional reproduced fixes included in the same commit:

  • explicit timedelta decoding for IPFS and S3 Zarr reads, preserving forecast behavior on current xarray;
  • stable bytes output from GeotemporalData.to_netcdf across xarray versions;
  • a compatible numcodecs constraint for the py_hamt/zarr 3.0 stack;
  • standard STAC data-asset CID fallback and next-link headers;
  • separate gateway and writable-RPC integration gates.

Validation completed:

  • locked suite: 175 passed, 47 skipped;
  • fresh wheel with current allowed dependencies (including xarray 2026.7): 175 passed, 47 skipped;
  • live public IPFS/STAC run: 33 passed, 1 RPC-only test skipped;
  • ruff, format, pre-commit, sdist/wheel build, and wheel METADATA inspection: clean.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@dclimate_client_py/stac_catalog.py`:
- Around line 508-515: Update the property_variant branch around
_dataset_and_variant_from_item_id so variant-only IDs that do not encode the
variant still produce a valid parsed dataset/variant result. Mirror the fallback
behavior already used by the resolver, using the item's dclimate:variant value
(including “default”) rather than allowing parsed_variant to remain None and be
omitted by the later listing logic.

In `@dclimate_client_py/stac_server.py`:
- Around line 376-386: Update the pre-scan around the search_features loop to
collect each valid dclimate:dataset_id in a separate per-collection hints map
without requiring collection_id to already exist in accumulators. When creating
accumulators for previously unseen collections in the later flow, seed their
known_datasets from the corresponding hints so legacy hyphenated IDs are parsed
correctly.
- Around line 426-435: Update the item parsing flow around
_dataset_and_variant_from_item_id so that when a variant-aware parse yields no
dataset for a bare item ID, it retries _dataset_and_variant_from_known_datasets
using entry["known_datasets"], matching _feature_matches_dataset behavior.
Preserve the existing property_dataset and variant fallback logic after the
retry.

In `@pyproject.toml`:
- Around line 41-43: Remove the PEP 508 git URL dependency for py_hamt from the
published dependency metadata in pyproject.toml. Replace it with a released
normal version constraint if available; otherwise keep the immutable git pin
only in a non-published development dependency configuration.

In `@tests/conftest.py`:
- Around line 188-194: Update is_ipfs_rpc_running to require a successful HTTP
status from the Kubo /api/v0/id request, rather than accepting any response
below 500. Validate the response using the appropriate successful-status check
and retain the existing RequestException fallback to False.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: aab216c8-8bd1-4401-9798-5ed5778cd038

📥 Commits

Reviewing files that changed from the base of the PR and between 12bf2e5 and db7444b.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (19)
  • dclimate_client_py/dclimate_client.py
  • dclimate_client_py/geotemporal_data.py
  • dclimate_client_py/ipfs_retrieval.py
  • dclimate_client_py/s3_retrieval.py
  • dclimate_client_py/stac_catalog.py
  • dclimate_client_py/stac_server.py
  • pyproject.toml
  • tests/conftest.py
  • tests/ipfs_config.py
  • tests/test_ipfs_retrieval.py
  • tests/test_list_datasets_parity.py
  • tests/test_pytest_infra.py
  • tests/test_review_bugs_client.py
  • tests/test_review_bugs_geotemporal.py
  • tests/test_review_suspects.py
  • tests/test_s3_retrieval.py
  • tests/test_stac_catalog.py
  • tests/test_stac_server_listing.py
  • tests/test_zarr_encryption_ipfs.py
🚧 Files skipped from review as they are similar to previous changes (8)
  • tests/test_zarr_encryption_ipfs.py
  • tests/test_review_bugs_geotemporal.py
  • tests/test_list_datasets_parity.py
  • dclimate_client_py/s3_retrieval.py
  • dclimate_client_py/ipfs_retrieval.py
  • tests/test_pytest_infra.py
  • dclimate_client_py/dclimate_client.py
  • dclimate_client_py/geotemporal_data.py

Comment thread dclimate_client_py/stac_catalog.py
Comment thread dclimate_client_py/stac_server.py Outdated
Comment thread dclimate_client_py/stac_server.py
Comment thread pyproject.toml Outdated
Comment thread tests/conftest.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
dclimate_client_py/stac_catalog.py (2)

129-132: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Define and close the reusable session. IPFSStacIO creates a requests.Session per load, but there’s no close path here or on the returned catalog, so repeated loads can leave pooled connections open for the lifetime of each instance. Make the session owned by the caller and close it explicitly, or add an IPFSStacIO.close() lifecycle and invoke it reliably.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dclimate_client_py/stac_catalog.py` around lines 129 - 132, The reusable
requests session created by IPFSStacIO must have an explicit lifecycle. Add an
IPFSStacIO.close() method that closes self.session, and ensure the
catalog-loading flow invokes it reliably after the returned catalog has finished
using the IO object, including failure paths.

188-196: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Pass the gateway into root-CID discovery get_root_catalog_cid() still hits https://ipfs-gateway.dclimate.net/stac, so load_stac_catalog(gateway_url, root_cid=None) can’t bootstrap private/local/offline gateways unless callers always supply root_cid explicitly. dclimate_client_py/stac_catalog.py:170-196

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dclimate_client_py/stac_catalog.py` around lines 188 - 196, Update
load_stac_catalog so the root-CID discovery call to get_root_catalog_cid
receives the gateway_url argument. Preserve the existing explicit root_cid path,
while ensuring gateway-specific discovery is used when root_cid is None.
tests/test_stac_catalog.py (1)

11-14: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Gate these tests against the endpoint they actually use.

@pytest.mark.ipfs is evaluated against IPFS_GATEWAY_URL, but get_root_catalog_cid() always calls https://ipfs-gateway.dclimate.net/stac. This can skip the tests when the configured gateway is down even though the public endpoint works, or run them when only the configured gateway is healthy. Add a separate gate for the public CID endpoint or mock this lookup.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_stac_catalog.py` around lines 11 - 14, Update the tests using
get_root_catalog_cid() so their availability gate targets the hard-coded public
CID endpoint at https://ipfs-gateway.dclimate.net/stac rather than
IPFS_GATEWAY_URL. Add a dedicated gate for that endpoint, or mock the CID lookup
to avoid endpoint-dependent gating, and apply it to the affected tests while
preserving the existing IPFS gate for tests that use the configured gateway.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@dclimate_client_py/stac_catalog.py`:
- Around line 129-132: The reusable requests session created by IPFSStacIO must
have an explicit lifecycle. Add an IPFSStacIO.close() method that closes
self.session, and ensure the catalog-loading flow invokes it reliably after the
returned catalog has finished using the IO object, including failure paths.
- Around line 188-196: Update load_stac_catalog so the root-CID discovery call
to get_root_catalog_cid receives the gateway_url argument. Preserve the existing
explicit root_cid path, while ensuring gateway-specific discovery is used when
root_cid is None.

In `@tests/test_stac_catalog.py`:
- Around line 11-14: Update the tests using get_root_catalog_cid() so their
availability gate targets the hard-coded public CID endpoint at
https://ipfs-gateway.dclimate.net/stac rather than IPFS_GATEWAY_URL. Add a
dedicated gate for that endpoint, or mock the CID lookup to avoid
endpoint-dependent gating, and apply it to the affected tests while preserving
the existing IPFS gate for tests that use the configured gateway.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 455463ee-f5b0-43c8-a17a-4d8ecd8a1b08

📥 Commits

Reviewing files that changed from the base of the PR and between db7444b and e1c09db.

📒 Files selected for processing (2)
  • dclimate_client_py/stac_catalog.py
  • tests/test_stac_catalog.py

@da-code-reviewer da-code-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex Automated Review

Found 5 actionable correctness and fallback regressions.
Posted 5 inline comment(s).

Comment thread dclimate_client_py/ipfs_retrieval.py Outdated
Comment thread dclimate_client_py/geotemporal_data.py
Comment thread dclimate_client_py/stac_server.py
Comment thread dclimate_client_py/stac_server.py
Comment thread dclimate_client_py/stac_catalog.py
@0xSwego

0xSwego commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator

Addressed the three outside-diff findings in 572f4aa:

  • IPFSStacIO now has an explicit close method. load_stac_catalog closes it on parse failures and registers cleanup when the returned catalog is released, while keeping the pool alive for lazy link resolution.
  • Root-CID discovery is configurable through a separate catalog_url argument (DCLIMATE_STAC_CATALOG_URL in tests). I intentionally did not derive /stac from gateway_url: the CI failure proved that a valid generic Kubo gateway does not necessarily expose dClimate’s control-plane /stac endpoint.
  • Pointer-dependent tests now have a dedicated stac_pointer marker and validate the configured pointer endpoint independently from the IPFS data gateway and writable RPC endpoint.

Validation on this commit: 191 passed / 45 skipped in the fast suite; 26/26 live catalog tests passed; fresh built wheel passed the same 191-test suite; pre-commit, ruff, format, sdist/wheel build, and publishable METADATA checks are clean.

@0xSwego

0xSwego commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator

Final review-pong status for 179dd78:

  • Addressed and resolved all 22 inline review threads, plus the 3 substantive outside-diff findings.
  • Locked-environment suite: 195 passed, 45 skipped.
  • Fresh built-wheel suite with released dependencies: 195 passed, 45 skipped.
  • Live catalog suite: 26 passed.
  • Ruff, formatting, pre-commit, package build, and metadata checks pass.
  • GitHub Actions sync-check, build, and pre-commit are green.

There are no unresolved review threads. The remaining REVIEW_REQUIRED/BLOCKED state is the required human approval, not a failing check.

@da-code-reviewer da-code-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex Automated Review

Found 3 actionable correctness issues.
Posted 3 inline comment(s).

Comment thread dclimate_client_py/ipfs_retrieval.py Outdated
Comment thread dclimate_client_py/geotemporal_data.py
Comment thread dclimate_client_py/stac_server.py

@da-code-reviewer da-code-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex Automated Review

Found two pagination and STAC fallback correctness issues.
Posted 2 inline comment(s).

Comment thread dclimate_client_py/stac_catalog.py
Comment thread dclimate_client_py/stac_server.py
Faolain and others added 2 commits July 18, 2026 13:38
…ttpx/ResolvedDataset line

Reconciliation decisions:
- upstream's requests-era additions translated to httpx: headers-aware
  _search_pages (pooled module client), get_root_catalog_cid(catalog_url),
  conftest RPC/pointer probes
- upstream's known_datasets longest-match parsing and collect-all-pages
  resolve adopted wholesale, fused with ResolvedDataset returns and
  _effective_variant selection; the 'default on page 1 stops paging'
  optimization is superseded by upstream's authoritative full walk
  (call-count pin updated 1 -> 2 accordingly)
- upstream's per-catalog stac_io binding + weakref close lifecycle
  adopted; IPFSStacIO now owns a per-instance httpx.Client (closable)
  while one-shot pointer reads keep the module-pooled client
- upstream's urllib3 MaxRetryError refinements dropped as moot on the
  httpx stack (HTTPStatusError already classifies non-connection)
- upstream's new tests ported from requests monkeypatching to the httpx
  MockTransport seams with all assertions preserved

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- IPFSStacIO.read_text implements the http(s) fallback for real (the old
  super().read_text was abstract and raised NotImplementedError behind a
  type-ignore); unsupported schemes raise a clear ValueError
- query() kwargs validation is value-aware (required keys must be
  non-None) and consistent: empty polygon/multiple_points kwargs now
  raise like the other selectors
- dClimateClient: eager ValueError for client_factory combined with
  headers/auth (KuboCAS rejected it only at context entry); gateway-None
  split semantics documented and the fallback ternary hoisted; auth
  forwarding pinned in tests
- mypy meta-test runs the project-pinned mypy and asserts the checked
  file count; py.typed presence pinned inside a built wheel
- benchmark: one-line failures to stderr, follow_redirects=True factory,
  JSON-only stdout
- probe redirect parity, managed mock clients in test helpers, stale
  requests.HTTPError reference in DATASET_CATALOG_USAGE.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/test_stac_catalog.py`:
- Around line 139-140: Add the pytest.mark.ipfs decorator alongside
pytest.mark.stac_pointer on TestLoadStacCatalog, TestResolveDatasetCidFromStac,
TestListAvailableDatasets, and TestIntegrationEndToEnd in
tests/test_stac_catalog.py, so all catalog-loading tests require both services.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 21b534e7-f868-4a76-b6a8-e513e42ce662

📥 Commits

Reviewing files that changed from the base of the PR and between e1c09db and 06e658b.

📒 Files selected for processing (14)
  • dclimate_client_py/geotemporal_data.py
  • dclimate_client_py/ipfs_retrieval.py
  • dclimate_client_py/stac_catalog.py
  • dclimate_client_py/stac_server.py
  • pyproject.toml
  • tests/conftest.py
  • tests/ipfs_config.py
  • tests/test_ipfs_retrieval.py
  • tests/test_list_datasets_parity.py
  • tests/test_pytest_infra.py
  • tests/test_review_perf_misc.py
  • tests/test_review_suspects.py
  • tests/test_stac_catalog.py
  • tests/test_stac_server_listing.py
🚧 Files skipped from review as they are similar to previous changes (7)
  • tests/ipfs_config.py
  • tests/test_review_perf_misc.py
  • tests/test_list_datasets_parity.py
  • pyproject.toml
  • dclimate_client_py/ipfs_retrieval.py
  • dclimate_client_py/stac_server.py
  • dclimate_client_py/geotemporal_data.py

Comment on lines +139 to 140
@pytest.mark.stac_pointer
class TestLoadStacCatalog:

Copy link
Copy Markdown

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

Gate catalog-loading tests on the IPFS gateway too.

stac_pointer only covers the root-CID endpoint; these classes also load catalog content through IPFS_GATEWAY_URL. Without @pytest.mark.ipfs, they run and fail when the pointer is reachable but the gateway is not.

  • tests/test_stac_catalog.py#L139-L140: add @pytest.mark.ipfs to TestLoadStacCatalog.
  • tests/test_stac_catalog.py#L202-L203: add @pytest.mark.ipfs to TestResolveDatasetCidFromStac.
  • tests/test_stac_catalog.py#L353-L354: add @pytest.mark.ipfs to TestListAvailableDatasets.
  • tests/test_stac_catalog.py#L444-L445: add @pytest.mark.ipfs to TestIntegrationEndToEnd.
📍 Affects 1 file
  • tests/test_stac_catalog.py#L139-L140 (this comment)
  • tests/test_stac_catalog.py#L202-L203
  • tests/test_stac_catalog.py#L353-L354
  • tests/test_stac_catalog.py#L444-L445
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_stac_catalog.py` around lines 139 - 140, Add the pytest.mark.ipfs
decorator alongside pytest.mark.stac_pointer on TestLoadStacCatalog,
TestResolveDatasetCidFromStac, TestListAvailableDatasets, and
TestIntegrationEndToEnd in tests/test_stac_catalog.py, so all catalog-loading
tests require both services.

@da-code-reviewer da-code-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex Automated Review

Found 2 actionable reliability issues.
Posted 2 inline comment(s).

dumps(request_headers, sort_keys=True, default=str),
)
if page_key in seen:
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MEDIUM
A repeated next request silently ends pagination as if the search completed. This can make listing return a partial catalog or make resolution select a first-page non-default variant, preventing the catalog fallback. Raise ValueError here, as the page-limit branch does, so callers detect truncation.

Comment thread pyproject.toml Outdated
[tool.uv.sources]
# Development integration pin for py-hamt PR #88. Published artifacts retain
# the released runtime floor because PyPI rejects direct-URL requirements.
py_hamt = { git = "https://github.com/dClimate/py-hamt", rev = "942580df089be13d6b8b803fc942932d65f2fc7f" }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MEDIUM
[tool.uv.sources] is ignored by wheels, sdists, pip, and non-uv installers. Since this PR explicitly depends on the pinned py-hamt revision, published installs will still resolve py_hamt>=3.4.1 and miss those required fixes. Wait for a release containing the revision and raise the runtime floor before publishing/merging.

Follow-ups: variant provenance, mypy 0 + py.typed, KuboCAS knobs, httpx consolidation (+ upstream review-commit merge)
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 80.03565% with 112 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.79%. Comparing base (2da6713) to head (d6e1657).
⚠️ Report is 45 commits behind head on main.

Files with missing lines Patch % Lines
dclimate_client_py/geotemporal_data.py 73.28% 21 Missing and 18 partials ⚠️
dclimate_client_py/stac_server.py 84.00% 13 Missing and 11 partials ⚠️
dclimate_client_py/dclimate_client.py 75.00% 11 Missing and 10 partials ⚠️
dclimate_client_py/stac_catalog.py 88.23% 5 Missing and 7 partials ⚠️
dclimate_client_py/s3_retrieval.py 77.77% 4 Missing and 2 partials ⚠️
dclimate_client_py/siren/siren_client.py 16.66% 5 Missing ⚠️
dclimate_client_py/__init__.py 75.00% 3 Missing ⚠️
dclimate_client_py/concatenate.py 87.50% 0 Missing and 1 partial ⚠️
dclimate_client_py/ipfs_retrieval.py 93.75% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #14      +/-   ##
==========================================
+ Coverage   65.42%   74.79%   +9.37%     
==========================================
  Files           8       16       +8     
  Lines         778     1956    +1178     
  Branches      111      363     +252     
==========================================
+ Hits          509     1463     +954     
- Misses        221      344     +123     
- Partials       48      149     +101     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@TheGreatAlgo
TheGreatAlgo merged commit eac31ef into main Jul 21, 2026
3 of 4 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
dclimate_client_py/stac_server.py (2)

453-506: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Parsed dataset names aren't fed back into known_datasets during listing.

Unlike stac_catalog.py's list_available_datasets (which does known_datasets.add(item_dataset) after each item so later items in the same collection benefit from siblings already resolved), this loop never updates entry["known_datasets"] after computing dataset_name (Lines 474-506). For a collection with no /collections metadata and no items carrying explicit dclimate:dataset_id/dclimate:variant properties, known_datasets stays empty for the whole pass, so every legacy hyphenated item id falls back to naive first-hyphen splitting (misparsing e.g. precip-daily-final as dataset precip) instead of self-reinforcing from siblings resolved earlier in the same loop.

♻️ Proposed fix
         dataset_variants = entry["datasets"].setdefault(dataset_name, {})
         dataset_variants[variant_name] = variant_entry
+        entry["known_datasets"].add(dataset_name)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dclimate_client_py/stac_server.py` around lines 453 - 506, After computing a
valid dataset_name in the item-processing loop, add it to
entry["known_datasets"] before resolving subsequent items. Update the logic
surrounding dataset_name and dataset_variants in the listing flow so sibling
items can reuse datasets discovered earlier, while preserving the existing skip
behavior when dataset_name is missing.

169-238: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Loop-detected pagination silently truncates instead of raising.

At Line 176-177, if page_key in seen: return ends pagination silently when a next link repeats an already-seen request. This is the exact failure mode the page-limit branch below (231-238) was fixed to reject — a still-open past review comment ("A repeated next request silently ends pagination as if the search completed... Raise ValueError here, as the page-limit branch does") appears unaddressed in the current code (no confirmation reply, unlike every sibling fix in this file).

Silent truncation here means resolve_cid_from_stac_server/list_available_datasets_from_stac_server can return an incomplete result (wrong variant selected, or a partial catalog) without triggering the caller's IPFS-catalog fallback, since no exception propagates.

🐛 Proposed fix
         if page_key in seen:
-            return
+            raise ValueError(
+                "STAC search next link repeated an already-seen request; "
+                "pagination cannot make progress"
+            )
         seen.add(page_key)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dclimate_client_py/stac_server.py` around lines 169 - 238, Update the
repeated-request branch in the pagination loop around the seen set to raise
ValueError instead of returning when page_key is already present. Keep the
existing page-limit failure behavior and provide a descriptive message
indicating that STAC pagination detected a repeated next request, so callers
receive the incomplete-result error rather than silent truncation.
🧹 Nitpick comments (1)
tests/test_review_fu_httpx.py (1)

75-144: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Prefer mocking the STAC transport over a real socket connection attempt.

stac_server_url="http://127.0.0.1:9" depends on the sandbox/CI network stack immediately refusing the connection; a mocked transport (as used in test_review_perf_async.py's install_httpx_mock) would make this deterministic and avoid depending on OS-level connection-refused semantics.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_review_fu_httpx.py` around lines 75 - 144, Update
test_load_dataset_falls_back_when_stac_transport_is_unreachable to mock the STAC
HTTP transport using the existing install_httpx_mock pattern from
test_review_perf_async.py instead of relying on stac_server_url port 9.
Configure the mock to produce the transport failure needed to exercise catalog
fallback, while preserving the existing assertions and dataset-loading setup.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/conftest.py`:
- Around line 194-197: Update the gateway availability predicate around the
httpx.head call in the IPFS collection gating logic to accept only successful
responses or the expected missing-CID 404; reject 401, 403, 429, and all other
statuses so inaccessible gateways cause the tests to be skipped.

---

Outside diff comments:
In `@dclimate_client_py/stac_server.py`:
- Around line 453-506: After computing a valid dataset_name in the
item-processing loop, add it to entry["known_datasets"] before resolving
subsequent items. Update the logic surrounding dataset_name and dataset_variants
in the listing flow so sibling items can reuse datasets discovered earlier,
while preserving the existing skip behavior when dataset_name is missing.
- Around line 169-238: Update the repeated-request branch in the pagination loop
around the seen set to raise ValueError instead of returning when page_key is
already present. Keep the existing page-limit failure behavior and provide a
descriptive message indicating that STAC pagination detected a repeated next
request, so callers receive the incomplete-result error rather than silent
truncation.

---

Nitpick comments:
In `@tests/test_review_fu_httpx.py`:
- Around line 75-144: Update
test_load_dataset_falls_back_when_stac_transport_is_unreachable to mock the STAC
HTTP transport using the existing install_httpx_mock pattern from
test_review_perf_async.py instead of relying on stac_server_url port 9.
Configure the mock to produce the transport failure needed to exercise catalog
fallback, while preserving the existing assertions and dataset-loading setup.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4eed9706-69ca-4dc7-9847-a9dfb6cc8755

📥 Commits

Reviewing files that changed from the base of the PR and between 06e658b and e3d2cae.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (29)
  • DATASET_CATALOG_USAGE.md
  • dclimate_client_py/__init__.py
  • dclimate_client_py/client.py
  • dclimate_client_py/dclimate_client.py
  • dclimate_client_py/encryption_codec.py
  • dclimate_client_py/geotemporal_data.py
  • dclimate_client_py/ipfs_retrieval.py
  • dclimate_client_py/py.typed
  • dclimate_client_py/siren/siren_client.py
  • dclimate_client_py/stac_catalog.py
  • dclimate_client_py/stac_server.py
  • pyproject.toml
  • scripts/benchmark_gateway.py
  • tests/conftest.py
  • tests/test_ipfs_retrieval.py
  • tests/test_list_datasets_parity.py
  • tests/test_pytest_infra.py
  • tests/test_review_bugs_stac_server.py
  • tests/test_review_fu_gateway_none.py
  • tests/test_review_fu_httpx.py
  • tests/test_review_fu_kubo_knobs.py
  • tests/test_review_fu_kwargs_validation.py
  • tests/test_review_fu_typing.py
  • tests/test_review_fu_variant.py
  • tests/test_review_perf_async.py
  • tests/test_review_suspects.py
  • tests/test_stac_catalog.py
  • tests/test_stac_server.py
  • tests/test_stac_server_listing.py
🚧 Files skipped from review as they are similar to previous changes (7)
  • dclimate_client_py/init.py
  • tests/test_pytest_infra.py
  • tests/test_ipfs_retrieval.py
  • pyproject.toml
  • tests/test_review_suspects.py
  • tests/test_stac_catalog.py
  • dclimate_client_py/geotemporal_data.py

Comment thread tests/conftest.py
Comment on lines +194 to +197
response = httpx.head(
f"{gateway_url.rstrip('/')}/ipfs/{known_cid}",
timeout=5,
follow_redirects=True,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not treat all sub-500 responses as a usable gateway.

The current predicate accepts 401, 403, 429, and other client-error responses, so collection gating can run IPFS tests against an inaccessible or rate-limited gateway instead of skipping them. Match the documented behavior by accepting successful responses or the expected missing-CID 404.

Proposed fix
-        if response.status_code < 500:
+        if response.is_success or response.status_code == 404:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
response = httpx.head(
f"{gateway_url.rstrip('/')}/ipfs/{known_cid}",
timeout=5,
follow_redirects=True,
if response.is_success or response.status_code == 404:
🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 193-197: Request-controlled URL passed to httpx; validate against an allowlist to prevent SSRF.
Context: httpx.head(
f"{gateway_url.rstrip('/')}/ipfs/{known_cid}",
timeout=5,
follow_redirects=True,
)
Note: [CWE-918] Server-Side Request Forgery (SSRF).

(avoid-ssrf)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/conftest.py` around lines 194 - 197, Update the gateway availability
predicate around the httpx.head call in the IPFS collection gating logic to
accept only successful responses or the expected missing-CID 404; reject 401,
403, 429, and all other statuses so inaccessible gateways cause the tests to be
skipped.

@da-code-reviewer da-code-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex Automated Review

Found five actionable issues: one credential-exposure risk, two STAC correctness bugs, and two deterministic test failures.
Posted 3 inline comment(s).

Findings that could not be placed inline:

  • dclimate_client_py/stac_catalog.py:215 [high] The client contains gateway headers/auth, but it is also used for arbitrary HTTP(S) links supplied by the catalog. A malicious catalog can point a child link at another origin and receive the gateway credentials. Restrict authenticated requests to the gateway origin or use an unauthenticated client for external links.
  • dclimate_client_py/stac_server.py:506 [medium] After inferring dataset_name, it is not added to entry["known_datasets"]. If a variant-only item establishes a hyphenated dataset such as precip-daily, a following property-less sibling is reparsed as dataset precip. Add each resolved dataset name to the hint set before processing subsequent items.

dumps(request_headers, sort_keys=True, default=str),
)
if page_key in seen:
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MEDIUM
A repeated next request silently terminates pagination and makes the accumulated features appear complete. Listing can therefore return partial results, and resolution can select the wrong fallback variant instead of invoking the catalog fallback. Raise ValueError here as the page-limit branch does.

Comment thread tests/test_review_suspects.py Outdated
}
)

monkeypatch.setattr(stac_server.requests, "post", post)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MEDIUM
stac_server no longer defines requests, so this test deterministically raises AttributeError before exercising pagination. Patch the new pooled _client() transport (or use the existing HTTPX helper) instead.

Comment thread tests/test_review_suspects.py Outdated
organization="org",
)

assert cid == "bafy-catalog-precip-daily-final"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MEDIUM
The resolver now returns ResolvedDataset, so comparing it directly with a CID string always fails. Assert cid.cid or compare against the complete ResolvedDataset; as written, this leaves the test suite red.

@da-code-reviewer da-code-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex Automated Review

Found 3 actionable issues: two credential-disclosure paths and one silent pagination truncation.
Posted 3 inline comment(s).

self._stac_catalog = await asyncio.to_thread(
load_stac_catalog,
gateway_url=self._catalog_gateway_base_url,
headers=self._headers,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH
Gateway credentials are forwarded to load_stac_catalog() without a catalog_url, so a client configured with a custom authenticated gateway sends its headers/basic-auth to the hard-coded public dClimate pointer whenever STAC-server fallback occurs. Derive or expose the custom gateway's pointer URL, or only forward credentials when the pointer origin matches the gateway.

return super().read_text(source, *args, **kwargs)
scheme = urlsplit(source_text).scheme.lower()
if scheme in {"http", "https"}:
response = self.client.get(source_text)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH
This client was constructed with the gateway's headers/basic-auth, but it is also used for arbitrary HTTP(S) links supplied by the remote catalog. A cross-origin catalog link therefore receives the user's gateway credentials. Restrict authenticated requests to the gateway origin and use an unauthenticated client for external links.

dumps(request_headers, sort_keys=True, default=str),
)
if page_key in seen:
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MEDIUM
A repeated next request silently ends pagination and treats the accumulated pages as complete. This can return an incomplete listing or select a lower-priority first-page variant without triggering catalog fallback. Raise ValueError here, as the page-limit branch does.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants