Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
81e6f8b
build: track py-hamt PR #88 branch (review-fixes-integration) via uv …
Faolain Jul 18, 2026
907b98e
test(red): meta-tests proving test-infra defects (autouse IPFS gate, …
Faolain Jul 18, 2026
2e594dc
test(green): marker-gate IPFS tests, asyncio_mode=auto, confine colle…
Faolain Jul 18, 2026
381255f
test(red): failing proofs for __aexit__ session leak and 10e-5 tolera…
Faolain Jul 18, 2026
e0b060b
test(red): failing proofs for 5 MEDIUM bugs (scalar size, to_netcdf a…
Faolain Jul 18, 2026
6a26740
test(red): failing proofs for polygons() triple breakage and rolling_…
Faolain Jul 18, 2026
ab22db1
fix: guarantee KuboCAS cleanup in __aexit__; correct snap tolerance 1…
Faolain Jul 18, 2026
bd42a23
test(red): failing proofs for 3 previously-unproven suspects (hyphena…
Faolain Jul 18, 2026
4f6fd46
fix: repair polygons() (rioxarray import, NoDataInBounds, fallback re…
Faolain Jul 18, 2026
8996335
fix: __aexit__ dual-failure semantics per review (AsyncExitStack conv…
Faolain Jul 18, 2026
838f461
test(red): spatial-selection semantics pins + failing memory guards f…
Faolain Jul 18, 2026
66730ef
test(red): failing proofs for event-loop stalls, sessionless STAC IO,…
Faolain Jul 18, 2026
397fc58
test(red): failing proofs for per-point loop, codec thread-hop on sma…
Faolain Jul 18, 2026
3c7586c
fix: scalar-safe check_dataset_size, non-mutating to_netcdf, guarded …
Faolain Jul 18, 2026
ed0b248
test/fix: WP-high review follow-ups (inplace=False, non-mutation regr…
Faolain Jul 18, 2026
929ee05
perf: zero-copy coordinate slicing for rectangle() and circle()
Faolain Jul 18, 2026
4f7f7b2
fix: unify hyphen-aware STAC id parsing, follow search pagination, ty…
Faolain Jul 18, 2026
d1b23a2
fix: resolve variant='default' matches bare item ids; strengthen s3/s…
Faolain Jul 18, 2026
364842a
perf: offload blocking STAC resolution from the event loop; pooled se…
Faolain Jul 18, 2026
76b4e7e
perf: vectorized points(), inline crypto for small chunks, lazy heavy…
Faolain Jul 18, 2026
97f832d
fix: mask-path fallback for exotic coordinate layouts in rectangle()/…
Faolain Jul 18, 2026
da83c25
fix: narrow connection-error typing, catalog default-variant symmetry…
Faolain Jul 18, 2026
8040d54
fix: perf2 review follow-ups (alist_datasets, root-CID timeout, dead …
Faolain Jul 18, 2026
1d696c4
fix: perf3 review follow-ups (offload count pin, threshold boundary t…
Faolain Jul 18, 2026
8e75c68
fix: reject missing geometries in points(); repo-wide ruff format pass
Faolain Jul 18, 2026
09666d7
build: floor all runtime dependencies, declare direct imports, drop u…
Faolain Jul 18, 2026
571ee1a
docs/types: fix README module section and codecov badge; accept ISO s…
Faolain Jul 18, 2026
ab29a23
fix: final review follow-ups (catalog lister property preference, mer…
Faolain Jul 18, 2026
12bf2e5
fix: final adversarial-review follow-ups (props-overlay parsing, hint…
Faolain Jul 18, 2026
43e962d
test(red): failing proofs for variant provenance in load_dataset meta…
Faolain Jul 18, 2026
a6f5483
test(red): failing proofs for mypy/py.typed gate, gateway_base_url=No…
Faolain Jul 18, 2026
342d684
fix: resolvers return ResolvedDataset(cid, variant); metadata reports…
Faolain Jul 18, 2026
2a4986c
types: mypy clean (40 -> 0), ship py.typed; fix gateway_base_url=None…
Faolain Jul 18, 2026
c959147
test(red): failing proofs for KuboCAS knob plumbing and benchmark scr…
Faolain Jul 18, 2026
96cba63
test/fix: FU-A review follow-ups (0.6.0 bump, preference-order pins, …
Faolain Jul 18, 2026
aae0e6b
feat: plumb KuboCAS connection knobs through dClimateClient; add gate…
Faolain Jul 18, 2026
de94db8
test(red): failing pins for httpx consolidation (source scan, depende…
Faolain Jul 18, 2026
3fd4213
refactor: consolidate all package HTTP on httpx; drop requests and ur…
Faolain Jul 18, 2026
db7444b
fix: address PR review findings
0xSwego Jul 18, 2026
e1c09db
fix: separate STAC pointer from data gateway
0xSwego Jul 18, 2026
8fd56e7
fix: address follow-up review findings
0xSwego Jul 18, 2026
572f4aa
fix: manage STAC catalog endpoint lifecycle
0xSwego Jul 18, 2026
179dd78
fix: handle final review edge cases
0xSwego Jul 18, 2026
a3fb3ca
Merge upstream fix/review-findings (maintainer review commits) into h…
Faolain Jul 18, 2026
1432333
fix: consolidated post-merge review follow-ups
Faolain Jul 18, 2026
06e658b
fix: avoid incomplete STAC fallback results
0xSwego Jul 20, 2026
5ad2f82
fix: handle stac and client
TheGreatAlgo Jul 21, 2026
e3d2cae
Merge pull request #15 from dClimate/fix/review-followups
TheGreatAlgo Jul 21, 2026
d6e1657
fix: tests
TheGreatAlgo Jul 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions DATASET_CATALOG_USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,15 +85,15 @@ datasets = list_available_datasets(catalog)
# }

# Resolve a specific dataset to its CID
cid = resolve_dataset_cid_from_stac(
resolved = resolve_dataset_cid_from_stac(
catalog=catalog,
collection="ecmwf_era5",
dataset="temperature_2m",
variant="finalized", # Optional, only needed if dataset has multiple variants
organization="ecmwf",
)

print(f"Dataset CID: {cid}")
print(f"Dataset CID: {resolved.cid} (variant: {resolved.variant})")
```

## STAC Catalog Structure
Expand Down Expand Up @@ -317,7 +317,7 @@ def resolve_dataset_cid_from_stac(
collection: str,
dataset: str,
variant: Optional[str] = None
) -> str
) -> ResolvedDataset
```

**Parameters:**
Expand All @@ -327,7 +327,7 @@ def resolve_dataset_cid_from_stac(
- `variant`: Optional variant name

**Returns:**
- `str`: IPFS CID (without "ipfs://" prefix)
- `ResolvedDataset`: IPFS CID (without "ipfs://" prefix) and selected variant

**Raises:**
- `ValueError`: If collection, dataset, or variant is not found
Expand All @@ -340,14 +340,14 @@ from dclimate_client_py import load_stac_catalog, resolve_dataset_cid_from_stac
catalog = load_stac_catalog("https://ipfs-gateway.dclimate.net")

# Resolve dataset to CID
cid = resolve_dataset_cid_from_stac(
resolved = resolve_dataset_cid_from_stac(
catalog=catalog,
collection="ifs",
dataset="temperature",
variant="single"
)

print(f"Dataset CID: {cid}")
print(f"Dataset CID: {resolved.cid} (variant: {resolved.variant})")
```

### `get_root_catalog_cid()`
Expand All @@ -362,7 +362,7 @@ def get_root_catalog_cid() -> str
- `str`: The IPFS CID of the latest root STAC catalog

**Raises:**
- `requests.HTTPError`: If the API request fails
- `httpx.HTTPError`: If the API request fails
- `KeyError`: If response doesn't contain expected 'cid' field

**Example:**
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
</p>

# dClimate-Client-Py
[![codecov](https://codecov.io/gh/dClimate/dClimate-Zarr-Client/graph/badge.svg?token=AovaMO6DX5)](https://codecov.io/gh/dClimate/dClimate-Zarr-Client)
[![codecov](https://codecov.io/gh/dClimate/dclimate-client-py/graph/badge.svg)](https://codecov.io/gh/dClimate/dclimate-client-py)

Retrieve dClimate GIS zarr datasets stored on IPFS

Expand Down Expand Up @@ -226,11 +226,11 @@ Various exceptions to be raised for bad or invalid user input.

---

### geo_utils.py
### geotemporal_data.py

Functions to manipulate `xarray` datasets. Contains polygon, rectangle, circle and point spatial
subsetting options, as well as temporal subsetting. Also allows for both spatial and temporal
aggregations.
`GeotemporalData`, a wrapper around `xarray` datasets. Contains polygon, rectangle, circle and
point spatial subsetting options, as well as temporal subsetting. Also allows for both spatial
and temporal aggregations.

---

Expand Down
39 changes: 29 additions & 10 deletions dclimate_client_py/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
# public API
from .client import (
load_s3,
geo_temporal_query,
)
from .dclimate_client import dClimateClient
from .geotemporal_data import GeotemporalData
from importlib import import_module

from .encryption_codec import (
EncryptionCodec,
)
Expand All @@ -16,11 +12,8 @@
SpatialExtent,
TemporalExtent,
)
from .stac_catalog import (
load_stac_catalog,
list_available_datasets,
)
from .stac_server import (
ResolvedDataset,
resolve_cid_from_stac_server,
list_available_datasets_from_stac_server,
STAC_SERVER_URL,
Expand All @@ -43,6 +36,31 @@
X402NotInstalledError,
)

_LAZY_IMPORTS = {
"load_s3": (".client", "load_s3"),
"geo_temporal_query": (".client", "geo_temporal_query"),
"dClimateClient": (".dclimate_client", "dClimateClient"),
"GeotemporalData": (".geotemporal_data", "GeotemporalData"),
"load_stac_catalog": (".stac_catalog", "load_stac_catalog"),
"list_available_datasets": (".stac_catalog", "list_available_datasets"),
}


def __getattr__(name: str):
try:
module_name, attribute_name = _LAZY_IMPORTS[name]
except KeyError:
raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from None

value = getattr(import_module(module_name, __name__), attribute_name)
globals()[name] = value
return value


def __dir__() -> list[str]:
return sorted(set(globals()) | set(_LAZY_IMPORTS))


__all__ = [
"dClimateClient",
"load_s3",
Expand All @@ -57,6 +75,7 @@
"TemporalExtent",
"load_stac_catalog",
"list_available_datasets",
"ResolvedDataset",
"resolve_cid_from_stac_server",
"list_available_datasets_from_stac_server",
"STAC_SERVER_URL",
Expand Down
24 changes: 12 additions & 12 deletions dclimate_client_py/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,21 +36,21 @@ def load_s3(
def geo_temporal_query(
dataset_name: str,
source: typing.Literal["s3"] = "s3",
bucket_name: str = None,
var_name: str = None,
bucket_name: typing.Optional[str] = None,
var_name: typing.Optional[str] = None,
gateway_uri_stem: str | None = None,
rpc_uri_stem: str | None = None,
forecast_reference_time: str = None,
point_kwargs: dict = None,
circle_kwargs: dict = None,
rectangle_kwargs: dict = None,
polygon_kwargs: dict = None,
multiple_points_kwargs: dict = None,
forecast_reference_time: typing.Optional[str] = None,
point_kwargs: typing.Optional[dict] = None,
circle_kwargs: typing.Optional[dict] = None,
rectangle_kwargs: typing.Optional[dict] = None,
polygon_kwargs: typing.Optional[dict] = None,
multiple_points_kwargs: typing.Optional[dict] = None,
bounds=None,
bounds_options: dict = None,
spatial_agg_kwargs: dict = None,
temporal_agg_kwargs: dict = None,
rolling_agg_kwargs: dict = None,
bounds_options: typing.Optional[dict] = None,
spatial_agg_kwargs: typing.Optional[dict] = None,
temporal_agg_kwargs: typing.Optional[dict] = None,
rolling_agg_kwargs: typing.Optional[dict] = None,
time_range: typing.Optional[typing.List[datetime.datetime]] = None,
# as_of: typing.Optional[datetime.datetime] = None, # Removed as_of
point_limit: int = DEFAULT_POINT_LIMIT,
Expand Down
29 changes: 14 additions & 15 deletions dclimate_client_py/concatenate.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@


def find_split_index(
combined_coords: typing.Any,
next_coords: typing.Any,
last_coord_value: typing.Any,
) -> int:
Expand All @@ -27,7 +26,6 @@ def find_split_index(
This prevents duplicate data when concatenating datasets.

Args:
combined_coords: Coordinates from the combined dataset (for reference)
next_coords: Coordinates from the next variant to concatenate
last_coord_value: The last coordinate value from the combined dataset

Expand Down Expand Up @@ -109,10 +107,11 @@ async def concatenate_datasets(
Implements smart concatenation logic:
1. Start with first dataset (highest priority)
2. For each subsequent dataset:
- Find the last coordinate value in combined dataset
- Find split index in next dataset where coords > last coord
- Find split index in next dataset where coords > the last coord
accepted so far
- Slice next dataset to only include new data
- Concatenate sliced dataset
3. Concatenate all accepted slices with a single ``xr.concat`` call
(an iterative per-dataset concat is O(n^2) time and ~2x peak memory)

Args:
datasets: List of xarray datasets to concatenate (in priority order)
Expand Down Expand Up @@ -144,16 +143,16 @@ async def concatenate_datasets(

# Start with the first dataset (highest priority)
combined = datasets[0]
datasets_to_concat = [combined]
last_coord_value = combined[dimension].values[-1]
total_coord_count = len(combined[dimension])
logger.debug(
f"Starting with dataset 1/{len(datasets)}, "
f"{dimension} range: {combined[dimension].values[0]} to {combined[dimension].values[-1]}"
)

# Concatenate each subsequent dataset
for i, next_ds in enumerate(datasets[1:], start=2):
# Get the last coordinate value from combined dataset
last_coord_value = combined[dimension].values[-1]

# Get coordinates from next dataset
next_coords = next_ds[dimension].values

Expand All @@ -165,7 +164,6 @@ async def concatenate_datasets(
# Find where to split the next dataset
try:
split_index = find_split_index(
combined[dimension].values,
next_coords,
last_coord_value,
)
Expand All @@ -177,25 +175,26 @@ async def concatenate_datasets(

# Slice the next dataset to only include new data
sliced_next = next_ds.isel({dimension: slice(split_index, None)})
datasets_to_concat.append(sliced_next)
last_coord_value = sliced_next[dimension].values[-1]
total_coord_count += len(sliced_next[dimension])

logger.debug(
f"Sliced dataset {i} to {len(sliced_next[dimension])} new coords"
)

# Concatenate with combined dataset
combined = xr.concat(
[combined, sliced_next],
dim=dimension,
)
logger.debug(
f"After concatenating dataset {i}, total {dimension} coords: {len(combined[dimension])}"
f"After concatenating dataset {i}, total {dimension} coords: {total_coord_count}"
)

except NoDataFoundError as e:
logger.warning(f"Skipping dataset {i} as it contains no new data: {e}")
# Continue to next dataset
continue

if len(datasets_to_concat) > 1:
combined = xr.concat(datasets_to_concat, dim=dimension)

logger.info(
f"Concatenation complete. Final dataset has {len(combined[dimension])} "
f"{dimension} coordinates ranging from {combined[dimension].values[0]} "
Expand Down
Loading
Loading