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.
- dynamical_catalog-0.2.0/.github/workflows/integration.yml +17 -0
- dynamical_catalog-0.2.0/.github/workflows/publish.yml +62 -0
- dynamical_catalog-0.2.0/.github/workflows/test.yml +32 -0
- dynamical_catalog-0.2.0/.gitignore +10 -0
- dynamical_catalog-0.2.0/PKG-INFO +65 -0
- dynamical_catalog-0.2.0/README.md +33 -0
- dynamical_catalog-0.2.0/examples/01_quickstart.py +22 -0
- dynamical_catalog-0.2.0/examples/02_list_datasets.py +17 -0
- dynamical_catalog-0.2.0/examples/03_get_store.py +20 -0
- dynamical_catalog-0.2.0/examples/04_forecast.py +24 -0
- dynamical_catalog-0.2.0/examples/05_icechunk.py +23 -0
- dynamical_catalog-0.2.0/examples/06_ensemble.py +24 -0
- dynamical_catalog-0.2.0/examples/07_precipitation.py +19 -0
- dynamical_catalog-0.2.0/pyproject.toml +65 -0
- dynamical_catalog-0.2.0/src/dynamical_catalog/__init__.py +87 -0
- dynamical_catalog-0.2.0/src/dynamical_catalog/_catalog.py +47 -0
- dynamical_catalog-0.2.0/src/dynamical_catalog/_dataset_entry.py +68 -0
- dynamical_catalog-0.2.0/src/dynamical_catalog/_open.py +48 -0
- dynamical_catalog-0.2.0/src/dynamical_catalog/_stac.py +108 -0
- dynamical_catalog-0.2.0/src/dynamical_catalog/py.typed +0 -0
- dynamical_catalog-0.2.0/tests/__init__.py +0 -0
- dynamical_catalog-0.2.0/tests/conftest.py +41 -0
- dynamical_catalog-0.2.0/tests/test_catalog.py +139 -0
- dynamical_catalog-0.2.0/tests/test_integration.py +63 -0
- dynamical_catalog-0.2.0/tests/test_open.py +84 -0
- dynamical_catalog-0.2.0/tests/test_stac.py +209 -0
- 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,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})"
|