dynamical-catalog 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. dynamical_catalog-0.2.0/.github/workflows/integration.yml +17 -0
  2. dynamical_catalog-0.2.0/.github/workflows/publish.yml +62 -0
  3. dynamical_catalog-0.2.0/.github/workflows/test.yml +32 -0
  4. dynamical_catalog-0.2.0/.gitignore +10 -0
  5. dynamical_catalog-0.2.0/PKG-INFO +65 -0
  6. dynamical_catalog-0.2.0/README.md +33 -0
  7. dynamical_catalog-0.2.0/examples/01_quickstart.py +22 -0
  8. dynamical_catalog-0.2.0/examples/02_list_datasets.py +17 -0
  9. dynamical_catalog-0.2.0/examples/03_get_store.py +20 -0
  10. dynamical_catalog-0.2.0/examples/04_forecast.py +24 -0
  11. dynamical_catalog-0.2.0/examples/05_icechunk.py +23 -0
  12. dynamical_catalog-0.2.0/examples/06_ensemble.py +24 -0
  13. dynamical_catalog-0.2.0/examples/07_precipitation.py +19 -0
  14. dynamical_catalog-0.2.0/pyproject.toml +65 -0
  15. dynamical_catalog-0.2.0/src/dynamical_catalog/__init__.py +87 -0
  16. dynamical_catalog-0.2.0/src/dynamical_catalog/_catalog.py +47 -0
  17. dynamical_catalog-0.2.0/src/dynamical_catalog/_dataset_entry.py +68 -0
  18. dynamical_catalog-0.2.0/src/dynamical_catalog/_open.py +48 -0
  19. dynamical_catalog-0.2.0/src/dynamical_catalog/_stac.py +108 -0
  20. dynamical_catalog-0.2.0/src/dynamical_catalog/py.typed +0 -0
  21. dynamical_catalog-0.2.0/tests/__init__.py +0 -0
  22. dynamical_catalog-0.2.0/tests/conftest.py +41 -0
  23. dynamical_catalog-0.2.0/tests/test_catalog.py +139 -0
  24. dynamical_catalog-0.2.0/tests/test_integration.py +63 -0
  25. dynamical_catalog-0.2.0/tests/test_open.py +84 -0
  26. dynamical_catalog-0.2.0/tests/test_stac.py +209 -0
  27. dynamical_catalog-0.2.0/uv.lock +1284 -0
@@ -0,0 +1,17 @@
1
+ name: Integration
2
+
3
+ on:
4
+ schedule:
5
+ - cron: "0 6 * * *" # daily at 06:00 UTC
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ integration:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: actions/setup-python@v5
14
+ with:
15
+ python-version: "3.13"
16
+ - run: pip install -e ".[dev]"
17
+ - run: pytest tests/test_integration.py -v -m slow
@@ -0,0 +1,62 @@
1
+ name: Release & Publish
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ inputs:
6
+ version:
7
+ description: "Version to release (e.g. 0.2.0)"
8
+ required: true
9
+ type: string
10
+
11
+ jobs:
12
+ release:
13
+ runs-on: ubuntu-latest
14
+ permissions:
15
+ contents: write
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ - name: Validate version format
19
+ run: |
20
+ if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then
21
+ echo "::error::Invalid version format '$VERSION'. Use semver (e.g. 0.2.0)"
22
+ exit 1
23
+ fi
24
+ env:
25
+ VERSION: ${{ inputs.version }}
26
+ - name: Bump version in pyproject.toml
27
+ run: sed -i "s/^version = \".*\"/version = \"$VERSION\"/" pyproject.toml
28
+ env:
29
+ VERSION: ${{ inputs.version }}
30
+ - name: Commit, tag, and push
31
+ run: |
32
+ git config user.name "github-actions[bot]"
33
+ git config user.email "github-actions[bot]@users.noreply.github.com"
34
+ git add pyproject.toml
35
+ git diff --cached --quiet || git commit -m "Release v$VERSION"
36
+ git tag "v$VERSION"
37
+ git push origin main "v$VERSION"
38
+ env:
39
+ VERSION: ${{ inputs.version }}
40
+ - name: Create GitHub Release
41
+ run: gh release create "v$VERSION" --generate-notes
42
+ env:
43
+ VERSION: ${{ inputs.version }}
44
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
45
+
46
+ publish:
47
+ needs: release
48
+ runs-on: ubuntu-latest
49
+ environment: pypi
50
+ permissions:
51
+ contents: read
52
+ id-token: write
53
+ steps:
54
+ - uses: actions/checkout@v4
55
+ with:
56
+ ref: v${{ inputs.version }}
57
+ - uses: actions/setup-python@v5
58
+ with:
59
+ python-version: "3.12"
60
+ - run: pip install build
61
+ - run: python -m build
62
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,32 @@
1
+ name: Test
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ lint:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: actions/setup-python@v5
14
+ with:
15
+ python-version: "3.13"
16
+ - run: pip install -e ".[dev]"
17
+ - run: ruff check src/ tests/
18
+ - run: ruff format --check src/ tests/
19
+ - run: mypy src/dynamical_catalog/
20
+
21
+ test:
22
+ runs-on: ubuntu-latest
23
+ strategy:
24
+ matrix:
25
+ python-version: ["3.11", "3.12", "3.13", "3.14"]
26
+ steps:
27
+ - uses: actions/checkout@v4
28
+ - uses: actions/setup-python@v5
29
+ with:
30
+ python-version: ${{ matrix.python-version }}
31
+ - run: pip install -e ".[dev]"
32
+ - run: pytest tests/ -v
@@ -0,0 +1,10 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .venv/
7
+ .pytest_cache/
8
+ .ruff_cache/
9
+ *.egg
10
+ .DS_Store
@@ -0,0 +1,65 @@
1
+ Metadata-Version: 2.4
2
+ Name: dynamical-catalog
3
+ Version: 0.2.0
4
+ Summary: Load dynamical.org weather datasets in one line
5
+ Project-URL: Homepage, https://dynamical.org
6
+ Project-URL: Repository, https://github.com/dynamical-org/dynamical-catalog
7
+ Project-URL: Catalog, https://dynamical.org/catalog/
8
+ Project-URL: Issues, https://github.com/dynamical-org/dynamical-catalog/issues
9
+ Author-email: "dynamical.org" <feedback@dynamical.org>
10
+ License-Expression: MIT
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Scientific/Engineering :: Atmospheric Science
20
+ Requires-Python: >=3.11
21
+ Requires-Dist: aiohttp
22
+ Requires-Dist: fsspec
23
+ Requires-Dist: icechunk>=1.0.0
24
+ Requires-Dist: xarray>=2025.1.2
25
+ Requires-Dist: zarr>=3.0.8
26
+ Provides-Extra: dev
27
+ Requires-Dist: mypy; extra == 'dev'
28
+ Requires-Dist: pytest; extra == 'dev'
29
+ Requires-Dist: pytest-mock; extra == 'dev'
30
+ Requires-Dist: ruff; extra == 'dev'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # dynamical-catalog
34
+
35
+ Load [dynamical.org](https://dynamical.org) weather datasets in one line.
36
+
37
+ ## Install
38
+
39
+ ```bash
40
+ pip install dynamical-catalog
41
+ ```
42
+
43
+ ## Usage
44
+
45
+ ```python
46
+ import dynamical_catalog
47
+
48
+ # Optional: let us know who you are so we can improve the catalog!
49
+ dynamical_catalog.identify("you@example.com")
50
+
51
+ # Open a dataset as xarray (zarr v3)
52
+ ds = dynamical_catalog.open("noaa-gfs-forecast")
53
+
54
+ # Open via icechunk
55
+ ds = dynamical_catalog.open("noaa-gfs-forecast", engine="icechunk")
56
+
57
+ # Get the underlying zarr store
58
+ store = dynamical_catalog.get_store("noaa-gfs-forecast")
59
+
60
+ # Tab-completable catalog
61
+ ds = dynamical_catalog.catalog.noaa_gfs_forecast.open()
62
+
63
+ # List all available datasets
64
+ dynamical_catalog.list()
65
+ ```
@@ -0,0 +1,33 @@
1
+ # dynamical-catalog
2
+
3
+ Load [dynamical.org](https://dynamical.org) weather datasets in one line.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install dynamical-catalog
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```python
14
+ import dynamical_catalog
15
+
16
+ # Optional: let us know who you are so we can improve the catalog!
17
+ dynamical_catalog.identify("you@example.com")
18
+
19
+ # Open a dataset as xarray (zarr v3)
20
+ ds = dynamical_catalog.open("noaa-gfs-forecast")
21
+
22
+ # Open via icechunk
23
+ ds = dynamical_catalog.open("noaa-gfs-forecast", engine="icechunk")
24
+
25
+ # Get the underlying zarr store
26
+ store = dynamical_catalog.get_store("noaa-gfs-forecast")
27
+
28
+ # Tab-completable catalog
29
+ ds = dynamical_catalog.catalog.noaa_gfs_forecast.open()
30
+
31
+ # List all available datasets
32
+ dynamical_catalog.list()
33
+ ```
@@ -0,0 +1,22 @@
1
+ """Quickstart: open a dataset and read a single value."""
2
+
3
+ import dynamical_catalog
4
+
5
+ dynamical_catalog.identify("dynamical-catalog example")
6
+
7
+ # Open the GFS analysis dataset (lazily — no data downloaded yet)
8
+ ds = dynamical_catalog.open("noaa-gfs-analysis")
9
+
10
+ # Read 2m temperature at a specific place and time
11
+ temp = (
12
+ ds["temperature_2m"]
13
+ .sel(
14
+ time="2025-06-01T12",
15
+ latitude=40.7, # New York City
16
+ longitude=-74.0,
17
+ method="nearest",
18
+ )
19
+ .compute()
20
+ )
21
+
22
+ print(f"Temperature in NYC on 2025-06-01 12Z: {temp.values:.1f} K")
@@ -0,0 +1,17 @@
1
+ """Browse the catalog and inspect dataset metadata."""
2
+
3
+ import dynamical_catalog
4
+
5
+ dynamical_catalog.identify("dynamical-catalog example")
6
+
7
+ # List all available dataset IDs
8
+ print("Available datasets:")
9
+ for dataset_id in dynamical_catalog.list():
10
+ print(f" {dataset_id}")
11
+
12
+ # Use tab-completable catalog to inspect a dataset
13
+ entry = dynamical_catalog.catalog.noaa_gfs_forecast
14
+ print(f"\nDataset: {entry.name}")
15
+ print(f"Description: {entry.description}")
16
+ print(f"Zarr URL: {entry.zarr_url}")
17
+ print(f"Icechunk available: {entry.icechunk_config is not None}")
@@ -0,0 +1,20 @@
1
+ """Get the underlying zarr Store for direct access."""
2
+
3
+ import zarr
4
+
5
+ import dynamical_catalog
6
+
7
+ dynamical_catalog.identify("dynamical-catalog example")
8
+
9
+ # get_store returns a zarr.abc.Store — the primitive underlying open()
10
+ store = dynamical_catalog.get_store("noaa-gfs-forecast")
11
+
12
+ # Open it with zarr directly
13
+ group = zarr.open_group(store)
14
+ print(f"Variables: {list(group.keys())}")
15
+
16
+ # Or pass it to xarray yourself with custom options
17
+ import xarray as xr # noqa: E402
18
+
19
+ ds = xr.open_zarr(store, chunks={"latitude": 100, "longitude": 100})
20
+ print(ds)
@@ -0,0 +1,24 @@
1
+ """Work with forecast data: init_time and lead_time dimensions."""
2
+
3
+ import dynamical_catalog
4
+
5
+ dynamical_catalog.identify("dynamical-catalog example")
6
+
7
+ # Open the GFS forecast dataset
8
+ ds = dynamical_catalog.open("noaa-gfs-forecast")
9
+
10
+ # Select one forecast initialization and a location
11
+ forecast = ds["temperature_2m"].sel(
12
+ init_time="2025-06-01T00",
13
+ latitude=51.5, # London
14
+ longitude=-0.1,
15
+ method="nearest",
16
+ )
17
+
18
+ # Get the max temperature across all lead times in this forecast
19
+ max_temp = forecast.max().compute()
20
+ print(f"Max forecast temperature: {max_temp.values:.1f} K")
21
+
22
+ # Get the full time series for this forecast (lazy until .compute())
23
+ print(f"\nForecast shape: {forecast.sizes}")
24
+ print(f"Lead times: {forecast.lead_time.values[:5]} ...")
@@ -0,0 +1,23 @@
1
+ """Open a dataset via Icechunk for transactional, versioned access."""
2
+
3
+ import dynamical_catalog
4
+
5
+ dynamical_catalog.identify("dynamical-catalog example")
6
+
7
+ # Open the same dataset through Icechunk instead of Zarr v3
8
+ ds = dynamical_catalog.open("noaa-gfs-forecast", engine="icechunk")
9
+
10
+ # Usage is identical — it's still an xarray Dataset
11
+ temp = ds["temperature_2m"].sel(
12
+ init_time="2025-06-01T00",
13
+ latitude=35.7, # Tokyo
14
+ longitude=139.7,
15
+ method="nearest",
16
+ )
17
+
18
+ max_temp = temp.max().compute()
19
+ print(f"Max forecast temperature in Tokyo: {max_temp.values:.1f} K")
20
+
21
+ # Or get the raw icechunk store
22
+ store = dynamical_catalog.get_store("noaa-gfs-forecast", engine="icechunk")
23
+ print(f"Store type: {type(store).__name__}")
@@ -0,0 +1,24 @@
1
+ """Work with ensemble forecast data (GEFS and ECMWF IFS ENS)."""
2
+
3
+ import dynamical_catalog
4
+
5
+ dynamical_catalog.identify("dynamical-catalog example")
6
+
7
+ # Open the GEFS 35-day ensemble forecast
8
+ ds = dynamical_catalog.open("noaa-gefs-forecast-35-day")
9
+
10
+ # Select one initialization and location
11
+ forecast = ds["temperature_2m"].sel(
12
+ init_time="2025-06-01T00",
13
+ latitude=48.9, # Paris
14
+ longitude=2.3,
15
+ method="nearest",
16
+ )
17
+
18
+ # Compute the ensemble spread at lead_time = 5 days
19
+ five_day = forecast.sel(lead_time="120h")
20
+ ensemble_mean = five_day.mean(dim="ensemble_member").compute()
21
+ ensemble_std = five_day.std(dim="ensemble_member").compute()
22
+
23
+ print(f"5-day forecast ensemble mean: {ensemble_mean.values:.1f} K")
24
+ print(f"5-day forecast ensemble std: {ensemble_std.values:.1f} K")
@@ -0,0 +1,19 @@
1
+ """Query high-resolution precipitation data from MRMS."""
2
+
3
+ import dynamical_catalog
4
+
5
+ dynamical_catalog.identify("dynamical-catalog example")
6
+
7
+ # Open the MRMS hourly precipitation analysis (~1km resolution, CONUS only)
8
+ ds = dynamical_catalog.open("noaa-mrms-conus-analysis-hourly")
9
+
10
+ # Get precipitation at a point over a time range
11
+ precip = ds["precipitation_surface"].sel(
12
+ latitude=39.1, # Kansas City, MO
13
+ longitude=-94.6,
14
+ method="nearest",
15
+ time=slice("2025-06-01", "2025-06-07"),
16
+ )
17
+
18
+ total = precip.sum().compute()
19
+ print(f"Total precipitation in Kansas City, June 1-7 2025: {total.values:.1f} mm")
@@ -0,0 +1,65 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "dynamical-catalog"
7
+ version = "0.2.0"
8
+ description = "Load dynamical.org weather datasets in one line"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.11"
12
+ authors = [
13
+ { name = "dynamical.org", email = "feedback@dynamical.org" },
14
+ ]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Science/Research",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Programming Language :: Python :: 3.14",
24
+ "Topic :: Scientific/Engineering :: Atmospheric Science",
25
+ ]
26
+ dependencies = [
27
+ "xarray>=2025.1.2",
28
+ "zarr>=3.0.8",
29
+ "fsspec",
30
+ "aiohttp",
31
+ "icechunk>=1.0.0",
32
+ ]
33
+
34
+ [project.urls]
35
+ Homepage = "https://dynamical.org"
36
+ Repository = "https://github.com/dynamical-org/dynamical-catalog"
37
+ Catalog = "https://dynamical.org/catalog/"
38
+ Issues = "https://github.com/dynamical-org/dynamical-catalog/issues"
39
+
40
+ [project.optional-dependencies]
41
+ dev = ["pytest", "pytest-mock", "ruff", "mypy"]
42
+
43
+ [tool.hatch.build.targets.wheel]
44
+ packages = ["src/dynamical_catalog"]
45
+
46
+ [tool.pytest.ini_options]
47
+ addopts = "-m 'not slow'"
48
+ markers = [
49
+ "slow: marks tests as slow (hit real network, run with pytest -m slow)",
50
+ ]
51
+
52
+ [tool.ruff]
53
+ target-version = "py311"
54
+
55
+ [tool.ruff.lint]
56
+ select = ["E", "F", "I", "UP"]
57
+
58
+ [tool.mypy]
59
+ python_version = "3.11"
60
+ warn_unused_configs = true
61
+ disallow_untyped_defs = true
62
+
63
+ [[tool.mypy.overrides]]
64
+ module = ["zarr.*", "icechunk.*", "fsspec.*"]
65
+ ignore_missing_imports = true
@@ -0,0 +1,87 @@
1
+ """dynamical_catalog - Load dynamical.org weather datasets in one line."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from importlib.metadata import version
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ if TYPE_CHECKING:
9
+ import xarray as xr
10
+ from zarr.abc.store import Store
11
+
12
+ from dynamical_catalog._catalog import Catalog
13
+ from dynamical_catalog._stac import clear_cache, load_catalog, set_identifier
14
+
15
+ __version__ = version("dynamical-catalog")
16
+
17
+ catalog = Catalog(load_catalog)
18
+
19
+
20
+ def identify(identifier: str) -> None:
21
+ """Set your identity for STAC catalog requests.
22
+
23
+ The identifier (typically an email or company name) is included in
24
+ the User-Agent header: ``dynamical-catalog/0.1.0 (identifier)``.
25
+
26
+ Args:
27
+ identifier: Email or company name, e.g. "marshall@dynamical.org".
28
+ """
29
+ set_identifier(identifier)
30
+
31
+
32
+ def get_store(dataset_id: str, engine: str = "zarr") -> Store:
33
+ """Get a zarr Store for a dynamical.org dataset.
34
+
35
+ Args:
36
+ dataset_id: Dataset identifier (e.g. "noaa-gfs-forecast").
37
+ Underscores are also accepted (e.g. "noaa_gfs_forecast").
38
+ engine: "zarr" (default) or "icechunk".
39
+
40
+ Returns:
41
+ zarr.abc.Store
42
+ """
43
+ from dynamical_catalog._open import _get_store
44
+
45
+ return _get_store(_resolve(dataset_id), engine=engine)
46
+
47
+
48
+ def open(dataset_id: str, engine: str = "zarr", **kwargs: Any) -> xr.Dataset:
49
+ """Open a dynamical.org dataset by ID.
50
+
51
+ Args:
52
+ dataset_id: Dataset identifier (e.g. "noaa-gfs-forecast").
53
+ Underscores are also accepted (e.g. "noaa_gfs_forecast").
54
+ engine: "zarr" (default) or "icechunk".
55
+ **kwargs: Passed through to xr.open_zarr().
56
+
57
+ Returns:
58
+ xarray.Dataset
59
+ """
60
+ from dynamical_catalog._open import _open_dataset
61
+
62
+ return _open_dataset(_resolve(dataset_id), engine=engine, **kwargs)
63
+
64
+
65
+ def list() -> list[str]: # type: ignore[valid-type]
66
+ """List available dataset IDs."""
67
+ return sorted(load_catalog().keys())
68
+
69
+
70
+ def _resolve(dataset_id: str) -> dict[str, Any]:
71
+ datasets = load_catalog()
72
+ normalized_id = dataset_id.replace("_", "-")
73
+ if normalized_id not in datasets:
74
+ available = ", ".join(sorted(datasets.keys()))
75
+ raise ValueError(f"Unknown dataset {dataset_id!r}. Available: {available}")
76
+ return datasets[normalized_id]
77
+
78
+
79
+ __all__ = [
80
+ "open",
81
+ "get_store",
82
+ "list",
83
+ "identify",
84
+ "catalog",
85
+ "clear_cache",
86
+ "__version__",
87
+ ]
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from typing import Any
5
+
6
+ from dynamical_catalog._dataset_entry import DatasetEntry
7
+
8
+
9
+ class Catalog:
10
+ """Provides attribute-style access to dynamical.org datasets.
11
+
12
+ Supports tab-completion in IPython/Jupyter::
13
+
14
+ >>> import dynamical_catalog
15
+ >>> dynamical_catalog.catalog.<TAB>
16
+ >>> dynamical_catalog.catalog.noaa_gfs_forecast.open()
17
+ """
18
+
19
+ def __init__(self, loader: Callable[[], dict[str, dict[str, Any]]]) -> None:
20
+ self._loader = loader
21
+ self._entries: dict[str, DatasetEntry] = {}
22
+
23
+ @property
24
+ def _datasets(self) -> dict[str, dict[str, Any]]:
25
+ return self._loader()
26
+
27
+ def __getattr__(self, name: str) -> DatasetEntry:
28
+ if name.startswith("_"):
29
+ raise AttributeError(name)
30
+ dataset_id = name.replace("_", "-")
31
+ datasets = self._datasets
32
+ if dataset_id not in datasets:
33
+ available = ", ".join(self._dataset_attr_names())
34
+ raise AttributeError(f"No dataset named {name!r}. Available: {available}")
35
+ if dataset_id not in self._entries:
36
+ self._entries[dataset_id] = DatasetEntry(datasets[dataset_id])
37
+ return self._entries[dataset_id]
38
+
39
+ def __dir__(self) -> list[str]:
40
+ return list(self._dataset_attr_names()) + list(super().__dir__())
41
+
42
+ def _dataset_attr_names(self) -> list[str]:
43
+ return [did.replace("-", "_") for did in self._datasets]
44
+
45
+ def __repr__(self) -> str:
46
+ names = self._dataset_attr_names()
47
+ return f"Catalog({len(names)} datasets: {', '.join(names)})"
@@ -0,0 +1,68 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ if TYPE_CHECKING:
6
+ import xarray as xr
7
+ from zarr.abc.store import Store
8
+
9
+
10
+ class DatasetEntry:
11
+ """A single dataset from the dynamical.org catalog."""
12
+
13
+ def __init__(self, data: dict[str, Any]) -> None:
14
+ self._data = data
15
+
16
+ @property
17
+ def id(self) -> str:
18
+ return self._data["id"]
19
+
20
+ @property
21
+ def name(self) -> str:
22
+ return self._data["name"]
23
+
24
+ @property
25
+ def description(self) -> str:
26
+ return self._data["description"]
27
+
28
+ @property
29
+ def zarr_url(self) -> str:
30
+ return self._data["zarr_url"]
31
+
32
+ @property
33
+ def icechunk_config(self) -> dict[str, str] | None:
34
+ return self._data.get("icechunk")
35
+
36
+ @property
37
+ def status(self) -> str:
38
+ return self._data["status"]
39
+
40
+ def get_store(self, engine: str = "zarr") -> Store:
41
+ """Get a zarr Store for this dataset.
42
+
43
+ Args:
44
+ engine: "zarr" (default) or "icechunk".
45
+
46
+ Returns:
47
+ zarr.abc.Store
48
+ """
49
+ from dynamical_catalog._open import _get_store
50
+
51
+ return _get_store(self._data, engine=engine)
52
+
53
+ def open(self, engine: str = "zarr", **kwargs: Any) -> xr.Dataset:
54
+ """Open this dataset as an xarray Dataset.
55
+
56
+ Args:
57
+ engine: "zarr" (default) or "icechunk".
58
+ **kwargs: Passed through to xr.open_zarr().
59
+
60
+ Returns:
61
+ xarray.Dataset
62
+ """
63
+ from dynamical_catalog._open import _open_dataset
64
+
65
+ return _open_dataset(self._data, engine=engine, **kwargs)
66
+
67
+ def __repr__(self) -> str:
68
+ return f"DatasetEntry({self.id!r}, {self.name!r})"