From 2dac0415d7dc263851b4214ab87b9e8631b5d365 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 May 2026 08:16:56 +0000 Subject: [PATCH 01/15] Initial plan From 8226a6c7e101f062169101f0d4f6003671663b1e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 May 2026 08:23:21 +0000 Subject: [PATCH 02/15] fix: store dataset properties in netcdf-compatible attrs Agent-Logs-Url: https://github.com/mlwp-tools/mxalign/sessions/2645ad1c-3573-48c9-88f0-5150eb23b01f Co-authored-by: observingClouds <43613877+observingClouds@users.noreply.github.com> --- src/mxalign/interpolations/delaunay.py | 4 +- src/mxalign/loaders/base.py | 4 +- src/mxalign/properties/utils.py | 45 +++++++++++++----- tests/test_properties_attrs.py | 64 ++++++++++++++++++++++++++ 4 files changed, 102 insertions(+), 15 deletions(-) create mode 100644 tests/test_properties_attrs.py diff --git a/src/mxalign/interpolations/delaunay.py b/src/mxalign/interpolations/delaunay.py index 164c255..88f2507 100644 --- a/src/mxalign/interpolations/delaunay.py +++ b/src/mxalign/interpolations/delaunay.py @@ -10,6 +10,7 @@ from .base import BaseInterpolator from .registry import register_interpolator from ..properties.properties import Space +from ..properties.utils import properties_from_attrs, set_properties_attrs @register_interpolator @@ -81,8 +82,7 @@ def _interpolate(self, source_dataset): latitude=self.target_dataset["latitude"], longitude=self.target_dataset["longitude"], ) - ds_out.attrs["properties"] = source_dataset.attrs["properties"] - return ds_out + return set_properties_attrs(ds_out, properties_from_attrs(source_dataset)) def _build_weight_matrix( diff --git a/src/mxalign/loaders/base.py b/src/mxalign/loaders/base.py index 3ab8bd9..c7020b5 100644 --- a/src/mxalign/loaders/base.py +++ b/src/mxalign/loaders/base.py @@ -3,7 +3,7 @@ from .registry import register_loader from ..properties.properties import Properties, Space, Time, Uncertainty from ..properties.validation import validate_dataset -from ..properties.utils import properties_to_attrs +from ..properties.utils import set_properties_attrs class BaseLoader(ABC): @@ -29,7 +29,7 @@ def load(self): properties = self._get_properties(ds) validate_dataset(ds, properties) - ds.attrs["properties"] = properties_to_attrs(properties) + ds = set_properties_attrs(ds, properties) if self.grid_mapping: ds = self._add_grid_mapping(ds) diff --git a/src/mxalign/properties/utils.py b/src/mxalign/properties/utils.py index 53555d3..42cdce6 100644 --- a/src/mxalign/properties/utils.py +++ b/src/mxalign/properties/utils.py @@ -1,24 +1,49 @@ +import json + from .properties import Properties, Space, Time, Uncertainty from .validation import validate_time_dataset, validate_space_dataset +SPACE_ATTR = "properties_space" +TIME_ATTR = "properties_time" +UNCERTAINTY_ATTR = "properties_uncertainty" + def properties_to_attrs(prop: Properties) -> dict: return { - "space": prop.space.value, - "time": prop.time.value, - "uncertainty": prop.uncertainty.value, + SPACE_ATTR: prop.space.value, + TIME_ATTR: prop.time.value, + UNCERTAINTY_ATTR: prop.uncertainty.value, } def properties_from_attrs(ds) -> Properties: - attrs = ds.attrs.get("properties", {}) + attrs = ds.attrs + old_attrs = attrs.get("properties", {}) + if isinstance(old_attrs, str): + try: + old_attrs = json.loads(old_attrs) + except json.JSONDecodeError: + old_attrs = {} + if not isinstance(old_attrs, dict): + old_attrs = {} + + space = attrs.get(SPACE_ATTR, old_attrs.get("space")) + time = attrs.get(TIME_ATTR, old_attrs.get("time")) + uncertainty = attrs.get(UNCERTAINTY_ATTR, old_attrs.get("uncertainty")) + return Properties( - space=Space(attrs["space"]), - time=Time(attrs["time"]), - uncertainty=Uncertainty(attrs.get("uncertainty", Uncertainty.DETERMINISTIC)), + space=Space(space), + time=Time(time), + uncertainty=Uncertainty(uncertainty or Uncertainty.DETERMINISTIC), ) +def set_properties_attrs(ds, prop: Properties): + ds.attrs.update(properties_to_attrs(prop)) + ds.attrs.pop("properties", None) + return ds + + def update_space_property(ds, prop: Space): old_props = properties_from_attrs(ds) new_props = Properties( @@ -27,8 +52,7 @@ def update_space_property(ds, prop: Space): uncertainty=old_props.uncertainty, ) validate_space_dataset(ds, new_props) - ds.attrs["properties"] = properties_to_attrs(new_props) - return ds + return set_properties_attrs(ds, new_props) def update_time_property(ds, prop: Time): @@ -39,5 +63,4 @@ def update_time_property(ds, prop: Time): uncertainty=old_props.uncertainty, ) validate_time_dataset(ds, new_props) - ds.attrs["properties"] = properties_to_attrs(new_props) - return ds + return set_properties_attrs(ds, new_props) diff --git a/tests/test_properties_attrs.py b/tests/test_properties_attrs.py new file mode 100644 index 0000000..baf344d --- /dev/null +++ b/tests/test_properties_attrs.py @@ -0,0 +1,64 @@ +import json +import tempfile +import unittest + +import xarray as xr + +from mxalign.properties.properties import Properties, Space, Time, Uncertainty +from mxalign.properties.utils import properties_from_attrs, set_properties_attrs + + +class TestPropertiesAttrs(unittest.TestCase): + def test_properties_are_stored_in_netcdf_compatible_attrs(self): + ds = xr.Dataset() + props = Properties( + space=Space.POINT, + time=Time.OBSERVATION, + uncertainty=Uncertainty.DETERMINISTIC, + ) + + ds = set_properties_attrs(ds, props) + + self.assertNotIn("properties", ds.attrs) + self.assertEqual(ds.attrs["properties_space"], "point") + self.assertEqual(ds.attrs["properties_time"], "observation") + self.assertEqual(ds.attrs["properties_uncertainty"], "deterministic") + + with tempfile.NamedTemporaryFile(suffix=".nc") as tmp: + ds.to_netcdf(tmp.name) + ds_loaded = xr.open_dataset(tmp.name) + self.assertEqual(properties_from_attrs(ds_loaded), props) + + def test_properties_can_still_be_read_from_legacy_format(self): + ds = xr.Dataset() + ds.attrs["properties"] = { + "space": "point", + "time": "observation", + "uncertainty": "deterministic", + } + self.assertEqual( + properties_from_attrs(ds), + Properties( + space=Space.POINT, + time=Time.OBSERVATION, + uncertainty=Uncertainty.DETERMINISTIC, + ), + ) + + def test_properties_can_be_read_from_legacy_json_string(self): + ds = xr.Dataset() + ds.attrs["properties"] = json.dumps( + {"space": "point", "time": "observation", "uncertainty": "deterministic"} + ) + self.assertEqual( + properties_from_attrs(ds), + Properties( + space=Space.POINT, + time=Time.OBSERVATION, + uncertainty=Uncertainty.DETERMINISTIC, + ), + ) + + +if __name__ == "__main__": + unittest.main() From 7885b590d11e810bb118ce247f1b8197f15fe20d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 May 2026 08:26:18 +0000 Subject: [PATCH 03/15] test: close netcdf dataset handle in properties attr test Agent-Logs-Url: https://github.com/mlwp-tools/mxalign/sessions/2645ad1c-3573-48c9-88f0-5150eb23b01f Co-authored-by: observingClouds <43613877+observingClouds@users.noreply.github.com> --- tests/test_properties_attrs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_properties_attrs.py b/tests/test_properties_attrs.py index baf344d..0774d1c 100644 --- a/tests/test_properties_attrs.py +++ b/tests/test_properties_attrs.py @@ -26,8 +26,8 @@ def test_properties_are_stored_in_netcdf_compatible_attrs(self): with tempfile.NamedTemporaryFile(suffix=".nc") as tmp: ds.to_netcdf(tmp.name) - ds_loaded = xr.open_dataset(tmp.name) - self.assertEqual(properties_from_attrs(ds_loaded), props) + with xr.open_dataset(tmp.name) as ds_loaded: + self.assertEqual(properties_from_attrs(ds_loaded), props) def test_properties_can_still_be_read_from_legacy_format(self): ds = xr.Dataset() From 605b283cf004cf3bc9ba17b163491fbbd3e8ad0f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 May 2026 08:44:04 +0000 Subject: [PATCH 04/15] refactor: use dotted properties attr keys Agent-Logs-Url: https://github.com/mlwp-tools/mxalign/sessions/77c180d8-40ee-44d4-b245-85a382fff6a4 Co-authored-by: observingClouds <43613877+observingClouds@users.noreply.github.com> --- src/mxalign/properties/utils.py | 20 ++++++++++++++------ tests/test_properties_attrs.py | 20 +++++++++++++++++--- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/src/mxalign/properties/utils.py b/src/mxalign/properties/utils.py index 42cdce6..377b717 100644 --- a/src/mxalign/properties/utils.py +++ b/src/mxalign/properties/utils.py @@ -3,9 +3,12 @@ from .properties import Properties, Space, Time, Uncertainty from .validation import validate_time_dataset, validate_space_dataset -SPACE_ATTR = "properties_space" -TIME_ATTR = "properties_time" -UNCERTAINTY_ATTR = "properties_uncertainty" +SPACE_ATTR = "properties.space" +TIME_ATTR = "properties.time" +UNCERTAINTY_ATTR = "properties.uncertainty" +LEGACY_SPACE_ATTR = "properties_space" +LEGACY_TIME_ATTR = "properties_time" +LEGACY_UNCERTAINTY_ATTR = "properties_uncertainty" def properties_to_attrs(prop: Properties) -> dict: @@ -27,9 +30,14 @@ def properties_from_attrs(ds) -> Properties: if not isinstance(old_attrs, dict): old_attrs = {} - space = attrs.get(SPACE_ATTR, old_attrs.get("space")) - time = attrs.get(TIME_ATTR, old_attrs.get("time")) - uncertainty = attrs.get(UNCERTAINTY_ATTR, old_attrs.get("uncertainty")) + space = attrs.get( + SPACE_ATTR, attrs.get(LEGACY_SPACE_ATTR, old_attrs.get("space")) + ) + time = attrs.get(TIME_ATTR, attrs.get(LEGACY_TIME_ATTR, old_attrs.get("time"))) + uncertainty = attrs.get( + UNCERTAINTY_ATTR, + attrs.get(LEGACY_UNCERTAINTY_ATTR, old_attrs.get("uncertainty")), + ) return Properties( space=Space(space), diff --git a/tests/test_properties_attrs.py b/tests/test_properties_attrs.py index 0774d1c..3bf5a56 100644 --- a/tests/test_properties_attrs.py +++ b/tests/test_properties_attrs.py @@ -20,9 +20,9 @@ def test_properties_are_stored_in_netcdf_compatible_attrs(self): ds = set_properties_attrs(ds, props) self.assertNotIn("properties", ds.attrs) - self.assertEqual(ds.attrs["properties_space"], "point") - self.assertEqual(ds.attrs["properties_time"], "observation") - self.assertEqual(ds.attrs["properties_uncertainty"], "deterministic") + self.assertEqual(ds.attrs["properties.space"], "point") + self.assertEqual(ds.attrs["properties.time"], "observation") + self.assertEqual(ds.attrs["properties.uncertainty"], "deterministic") with tempfile.NamedTemporaryFile(suffix=".nc") as tmp: ds.to_netcdf(tmp.name) @@ -45,6 +45,20 @@ def test_properties_can_still_be_read_from_legacy_format(self): ), ) + def test_properties_can_still_be_read_from_legacy_flat_underscore_keys(self): + ds = xr.Dataset() + ds.attrs["properties_space"] = "point" + ds.attrs["properties_time"] = "observation" + ds.attrs["properties_uncertainty"] = "deterministic" + self.assertEqual( + properties_from_attrs(ds), + Properties( + space=Space.POINT, + time=Time.OBSERVATION, + uncertainty=Uncertainty.DETERMINISTIC, + ), + ) + def test_properties_can_be_read_from_legacy_json_string(self): ds = xr.Dataset() ds.attrs["properties"] = json.dumps( From 9ece8b5f4b84acc3d47565e57b5647b446d8b0e0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 May 2026 09:00:06 +0000 Subject: [PATCH 05/15] refactor: drop underscore properties attr fallback Agent-Logs-Url: https://github.com/mlwp-tools/mxalign/sessions/7ea2a6c3-a3f2-4be2-8210-cabc0af65ae2 Co-authored-by: observingClouds <43613877+observingClouds@users.noreply.github.com> --- src/mxalign/properties/utils.py | 15 +++------------ tests/test_properties_attrs.py | 14 -------------- 2 files changed, 3 insertions(+), 26 deletions(-) diff --git a/src/mxalign/properties/utils.py b/src/mxalign/properties/utils.py index 377b717..6ebf796 100644 --- a/src/mxalign/properties/utils.py +++ b/src/mxalign/properties/utils.py @@ -6,10 +6,6 @@ SPACE_ATTR = "properties.space" TIME_ATTR = "properties.time" UNCERTAINTY_ATTR = "properties.uncertainty" -LEGACY_SPACE_ATTR = "properties_space" -LEGACY_TIME_ATTR = "properties_time" -LEGACY_UNCERTAINTY_ATTR = "properties_uncertainty" - def properties_to_attrs(prop: Properties) -> dict: return { @@ -30,14 +26,9 @@ def properties_from_attrs(ds) -> Properties: if not isinstance(old_attrs, dict): old_attrs = {} - space = attrs.get( - SPACE_ATTR, attrs.get(LEGACY_SPACE_ATTR, old_attrs.get("space")) - ) - time = attrs.get(TIME_ATTR, attrs.get(LEGACY_TIME_ATTR, old_attrs.get("time"))) - uncertainty = attrs.get( - UNCERTAINTY_ATTR, - attrs.get(LEGACY_UNCERTAINTY_ATTR, old_attrs.get("uncertainty")), - ) + space = attrs.get(SPACE_ATTR, old_attrs.get("space")) + time = attrs.get(TIME_ATTR, old_attrs.get("time")) + uncertainty = attrs.get(UNCERTAINTY_ATTR, old_attrs.get("uncertainty")) return Properties( space=Space(space), diff --git a/tests/test_properties_attrs.py b/tests/test_properties_attrs.py index 3bf5a56..910cd72 100644 --- a/tests/test_properties_attrs.py +++ b/tests/test_properties_attrs.py @@ -45,20 +45,6 @@ def test_properties_can_still_be_read_from_legacy_format(self): ), ) - def test_properties_can_still_be_read_from_legacy_flat_underscore_keys(self): - ds = xr.Dataset() - ds.attrs["properties_space"] = "point" - ds.attrs["properties_time"] = "observation" - ds.attrs["properties_uncertainty"] = "deterministic" - self.assertEqual( - properties_from_attrs(ds), - Properties( - space=Space.POINT, - time=Time.OBSERVATION, - uncertainty=Uncertainty.DETERMINISTIC, - ), - ) - def test_properties_can_be_read_from_legacy_json_string(self): ds = xr.Dataset() ds.attrs["properties"] = json.dumps( From 3bdf9bf68214362fdd2c6da36e413f3ec0959591 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 May 2026 09:08:50 +0000 Subject: [PATCH 06/15] style: apply ruff format fix Agent-Logs-Url: https://github.com/mlwp-tools/mxalign/sessions/3e3fab28-7e29-4f5e-b20d-c0ca122da9bd Co-authored-by: observingClouds <43613877+observingClouds@users.noreply.github.com> --- src/mxalign/properties/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/mxalign/properties/utils.py b/src/mxalign/properties/utils.py index 6ebf796..78e2997 100644 --- a/src/mxalign/properties/utils.py +++ b/src/mxalign/properties/utils.py @@ -7,6 +7,7 @@ TIME_ATTR = "properties.time" UNCERTAINTY_ATTR = "properties.uncertainty" + def properties_to_attrs(prop: Properties) -> dict: return { SPACE_ATTR: prop.space.value, From f596fe1363878674596d212a25f4367368ce93ef Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 May 2026 09:14:50 +0000 Subject: [PATCH 07/15] ci: add workflow to run unit tests Agent-Logs-Url: https://github.com/mlwp-tools/mxalign/sessions/470afd36-0845-4b9b-864f-6e1a0e536c32 Co-authored-by: observingClouds <43613877+observingClouds@users.noreply.github.com> --- .github/workflows/tests.yaml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .github/workflows/tests.yaml diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml new file mode 100644 index 0000000..4e46cbc --- /dev/null +++ b/.github/workflows/tests.yaml @@ -0,0 +1,24 @@ +name: Test Suite + +on: + pull_request: + push: + branches: [main, develop] + workflow_dispatch: + +permissions: + contents: read + +jobs: + unit-tests: + name: Run Unit Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + with: + python-version: "3.12" + - name: Install package + run: python -m pip install . + - name: Run tests + run: python -m unittest discover -s tests -v From 37518b4967a0d733d54685700caaee117997dfb0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 May 2026 09:25:08 +0000 Subject: [PATCH 08/15] ci: use uv lockfile for test workflow Agent-Logs-Url: https://github.com/mlwp-tools/mxalign/sessions/cf8763b9-c94e-495f-bf12-5939302ed1ab Co-authored-by: observingClouds <43613877+observingClouds@users.noreply.github.com> --- .github/workflows/tests.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 4e46cbc..1dcc417 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -15,10 +15,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - - uses: actions/setup-python@v6 + - uses: astral-sh/setup-uv@v6 with: python-version: "3.12" - - name: Install package - run: python -m pip install . + - name: Install dependencies + run: uv sync --frozen - name: Run tests - run: python -m unittest discover -s tests -v + run: uv run python -m unittest discover -s tests -v From bf7c3b69f9467dd292971778e89a879af554a99c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 May 2026 09:44:53 +0000 Subject: [PATCH 09/15] build: add ifs optional deps and install all extras in CI Agent-Logs-Url: https://github.com/mlwp-tools/mxalign/sessions/00dc6225-1a66-4308-8824-e7fa4522d672 Co-authored-by: observingClouds <43613877+observingClouds@users.noreply.github.com> --- .github/workflows/tests.yaml | 2 +- pyproject.toml | 3 +++ uv.lock | 6 +++++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 1dcc417..02b466d 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -19,6 +19,6 @@ jobs: with: python-version: "3.12" - name: Install dependencies - run: uv sync --frozen + run: uv sync --frozen --all-extras - name: Run tests run: uv run python -m unittest discover -s tests -v diff --git a/pyproject.toml b/pyproject.toml index 1016cdd..80ffe81 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,9 @@ mxalign = "mxalign.cli:main" earthkit = [ "earthkit-meteo>=0.6.1", ] +ifs = [ + "cfgrib>=0.9.15.1", +] verification = [ "xskillscore>=0.0.29", ] diff --git a/uv.lock b/uv.lock index 7e23d67..d87620c 100644 --- a/uv.lock +++ b/uv.lock @@ -1251,6 +1251,9 @@ dependencies = [ earthkit = [ { name = "earthkit-meteo" }, ] +ifs = [ + { name = "cfgrib" }, +] jobqueue = [ { name = "dask-jobqueue" }, ] @@ -1267,6 +1270,7 @@ dev = [ requires-dist = [ { name = "bokeh", specifier = ">=3.8.2" }, { name = "cartopy", specifier = ">=0.25.0" }, + { name = "cfgrib", marker = "extra == 'ifs'", specifier = ">=0.9.15.1" }, { name = "dask", specifier = ">=2026.1.2" }, { name = "dask-jobqueue", marker = "extra == 'jobqueue'", specifier = ">=0.9.0" }, { name = "distributed", specifier = ">=2026.1.2" }, @@ -1281,7 +1285,7 @@ requires-dist = [ { name = "xskillscore", marker = "extra == 'verification'", specifier = ">=0.0.29" }, { name = "zarr", specifier = "<3.0" }, ] -provides-extras = ["earthkit", "verification", "jobqueue"] +provides-extras = ["earthkit", "ifs", "verification", "jobqueue"] [package.metadata.requires-dev] dev = [{ name = "ipykernel", specifier = ">=7.2.0" }] From 399df3187c85cf05ef2a795e991a2cadbc13b6b4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 May 2026 09:58:32 +0000 Subject: [PATCH 10/15] build: add eccodes to ifs optional dependency group Agent-Logs-Url: https://github.com/mlwp-tools/mxalign/sessions/7b3c68be-ad8c-467b-a5d2-d93514fda6c0 Co-authored-by: observingClouds <43613877+observingClouds@users.noreply.github.com> --- pyproject.toml | 1 + uv.lock | 2 ++ 2 files changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 80ffe81..c520905 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ earthkit = [ ] ifs = [ "cfgrib>=0.9.15.1", + "eccodes>=2.45.0", ] verification = [ "xskillscore>=0.0.29", diff --git a/uv.lock b/uv.lock index d87620c..4fbe7d3 100644 --- a/uv.lock +++ b/uv.lock @@ -1253,6 +1253,7 @@ earthkit = [ ] ifs = [ { name = "cfgrib" }, + { name = "eccodes" }, ] jobqueue = [ { name = "dask-jobqueue" }, @@ -1276,6 +1277,7 @@ requires-dist = [ { name = "distributed", specifier = ">=2026.1.2" }, { name = "earthkit-data", specifier = ">=0.19.0" }, { name = "earthkit-meteo", marker = "extra == 'earthkit'", specifier = ">=0.6.1" }, + { name = "eccodes", marker = "extra == 'ifs'", specifier = ">=2.45.0" }, { name = "h5netcdf", specifier = ">=1.8.1" }, { name = "h5py", specifier = ">=3.15.1" }, { name = "netcdf4", specifier = ">=1.7.4" }, From 7216c8ef75c079e50190c7df0969994842f63221 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 May 2026 10:40:18 +0000 Subject: [PATCH 11/15] build: add eccodeslib to ifs optional dependencies Agent-Logs-Url: https://github.com/mlwp-tools/mxalign/sessions/e06d9499-d27d-47d7-b76f-4cb0470fc5d9 Co-authored-by: observingClouds <43613877+observingClouds@users.noreply.github.com> --- pyproject.toml | 1 + uv.lock | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index c520905..4a345f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,7 @@ earthkit = [ ifs = [ "cfgrib>=0.9.15.1", "eccodes>=2.45.0", + "eccodeslib>=2.46.2.19", ] verification = [ "xskillscore>=0.0.29", diff --git a/uv.lock b/uv.lock index 4fbe7d3..eeafaed 100644 --- a/uv.lock +++ b/uv.lock @@ -577,6 +577,47 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7f/40/0a42c7441d76c373a7bef7ec1f535f26910a8c82a37e745ffcd3ea0cdf79/eccodes-2.45.0-py3-none-any.whl", hash = "sha256:0ba61dbd2844843f1fd466c8ca24107932cc40088338f6176428cf38c533c08c", size = 91433, upload-time = "2026-01-15T16:27:06.601Z" }, ] +[[package]] +name = "eccodeslib" +version = "2.46.2.19" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "eckitlib" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/ab/eb96e7b9a69521e438f2a562e029e0452e80be93b8f4c8b32be2766913f9/eccodeslib-2.46.2.19-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:5cb46d9bda6324935539457282195f8fc5df38dae4ea7ff545ba76a9cd50cf5f", size = 8899990, upload-time = "2026-04-01T12:08:38.156Z" }, + { url = "https://files.pythonhosted.org/packages/98/fe/7c3bec98984b4e946431d13fddf2ebdd5436106a9f85c1a3938e7339c26d/eccodeslib-2.46.2.19-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:73880e83765b78b149b747549674cf264b2e7bf64532ab4e99eb1225cb8bb485", size = 8731401, upload-time = "2026-04-01T12:14:47.677Z" }, + { url = "https://files.pythonhosted.org/packages/11/e4/1cdffaa4abd303726a06bc9630b7197360df35d11c297c702bf8b6e83fdd/eccodeslib-2.46.2.19-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:8174d1350e629a0a4e51fbf63703658b1ed2f7423e7cb083a2b539a7dcee80cb", size = 9093644, upload-time = "2026-04-01T12:15:16.138Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f4/2f83c12c9987040a1a165534e02e28a82acef9744552cba43e705a674689/eccodeslib-2.46.2.19-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:cab8151ac204a45088d7789236183e8ff7a51fafd168be1488a0c10be886f0f4", size = 8986646, upload-time = "2026-04-01T12:37:42.601Z" }, + { url = "https://files.pythonhosted.org/packages/d3/12/e48ea960e13791b033c08b25045eeb8221df8beb3a9e58d0753bf5bd42fe/eccodeslib-2.46.2.19-cp313-cp313-macosx_13_0_arm64.whl", hash = "sha256:f5fe6a0787381115929ba521d9dafae7cb9c0c9b72790e4f92db1a09a043625f", size = 8899996, upload-time = "2026-04-01T12:30:10.847Z" }, + { url = "https://files.pythonhosted.org/packages/01/8a/0826b05445c886e95fc46ac908afae28d28a06bb66f4f84dbbf710360a85/eccodeslib-2.46.2.19-cp313-cp313-macosx_13_0_x86_64.whl", hash = "sha256:a5ba7e609de866ed0f2d45b4785a8a870d21efb6e9e5a71e219d39e8fdeebdf0", size = 8731409, upload-time = "2026-04-01T12:14:59.447Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3b/758a073c4ed5b1c732f6a3b4a5ba7d8ca122d8408ebc7dd608e7e447c07c/eccodeslib-2.46.2.19-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:4500abf05428d94693dd29c589c6d208f2d48aff5efa145425c704fcd1f46925", size = 9093643, upload-time = "2026-04-01T12:15:20.43Z" }, + { url = "https://files.pythonhosted.org/packages/d9/48/5e0e31e3cb310fbe04b08040e96d3107639ceef8a94f8788691af77beb54/eccodeslib-2.46.2.19-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:0f14eb14f37af34bb79f43ba8ab8dff99d6d4876b6681614c6644a3cbbb89d6c", size = 8986651, upload-time = "2026-04-01T12:23:15.323Z" }, + { url = "https://files.pythonhosted.org/packages/d2/90/3a3180b227e6483e80e6631e8630d8942fc8bfc66b8e6acd9f5549c537c6/eccodeslib-2.46.2.19-cp314-cp314-macosx_13_0_arm64.whl", hash = "sha256:d93146a2770d3169f91129193e995a0bd58ac933937e26909c65c347f36b94a6", size = 8900007, upload-time = "2026-04-01T12:20:31.911Z" }, + { url = "https://files.pythonhosted.org/packages/f4/91/901cb12b04f988420b7e75c914f7361ce5c23bff8c19e8b1d9c56657d7bc/eccodeslib-2.46.2.19-cp314-cp314-macosx_13_0_x86_64.whl", hash = "sha256:9c2da433e1d87ee4554b684c625234cbca14fa51181256b78a9d646a90f2fe22", size = 8731400, upload-time = "2026-04-01T12:52:59.854Z" }, + { url = "https://files.pythonhosted.org/packages/ef/33/ab3c19ffc2675514259ff61a8ea757fb2a470ad6052371384f2a5021046c/eccodeslib-2.46.2.19-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:e62e4d893ec2939b6096c6aed21e31d38a979e33d50917611a1907b3ef0a2996", size = 9093646, upload-time = "2026-04-01T12:13:35.905Z" }, + { url = "https://files.pythonhosted.org/packages/d7/41/c8ab7aadf86d28149c636970704a923c161b81ce301bc0fc3da82ce3d737/eccodeslib-2.46.2.19-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:5daa5a8290d10accf308a94678ebc85c8fd843e20f2d5d644559d4a691dd4af1", size = 8986650, upload-time = "2026-04-01T12:32:33.346Z" }, +] + +[[package]] +name = "eckitlib" +version = "2.0.7.19" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/23/269c1271d00596c4e68177c9dda5350e98a3ba19c0058421da9c870380f1/eckitlib-2.0.7.19-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:cfba9bf706755e649afea97de7597242fd8f4ecff1b72a26c9ef58eafb67857e", size = 2480050, upload-time = "2026-04-01T12:08:43.95Z" }, + { url = "https://files.pythonhosted.org/packages/00/38/7adb54bdcbce6642230c122515dea472f131e0ccdb0c3cc0fb350440c6bc/eckitlib-2.0.7.19-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:66bdd16dd1cf1bbeeea9cbeccc0cc1e28822057d5f104e43868dd7011717b172", size = 2610836, upload-time = "2026-04-01T12:14:53.294Z" }, + { url = "https://files.pythonhosted.org/packages/9b/9f/9728cc577cbf84619f57da633e7ad3625fa5bef7ad05a990118530ed9f0e/eckitlib-2.0.7.19-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5639123c5a3c645d8bcd6bb2fd29eb321675c0af7624a369a8804798abc6364b", size = 7040518, upload-time = "2026-04-01T12:15:23.621Z" }, + { url = "https://files.pythonhosted.org/packages/4f/f5/4b2c6ac00ea46d1e7f6c680979720d51e05bce6182c61df24b8a2a75326d/eckitlib-2.0.7.19-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:725d8abc7c5eb039788fd0df28aea7d2c0739ff53861f6d00ca9c319dbd321b0", size = 7169837, upload-time = "2026-04-01T12:37:49.842Z" }, + { url = "https://files.pythonhosted.org/packages/1a/46/9ccdf223a33c07fe27426c01dcc931316a3531881e4873c24d40e4f09dfd/eckitlib-2.0.7.19-cp313-cp313-macosx_13_0_arm64.whl", hash = "sha256:5b6518c555f598fef15915c52d07797fe85aa9a09d89f6e9274c5f549c427fb1", size = 2480047, upload-time = "2026-04-01T12:30:15.936Z" }, + { url = "https://files.pythonhosted.org/packages/f2/49/31d03b6fe1270e055c521d4a350fed77afd81abe5711f254058e4c631579/eckitlib-2.0.7.19-cp313-cp313-macosx_13_0_x86_64.whl", hash = "sha256:04c4ec2b96b9da37406925b418e752195e4f782d77148c4654f105984525dd0f", size = 2610842, upload-time = "2026-04-01T12:15:05.025Z" }, + { url = "https://files.pythonhosted.org/packages/32/13/bf8bcf039f81c7e74f68808c6fe1eb0ba57812aaa0e16f0f5644ef7f81ce/eckitlib-2.0.7.19-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13c7acfe19da5d25e9c5f7e6fb806deca39ac7adea31958d118799a58e8739bd", size = 7040512, upload-time = "2026-04-01T12:15:29.127Z" }, + { url = "https://files.pythonhosted.org/packages/b4/64/9dc3e3a669f251707487308d0e5efaa0e6755a4da8e3566b52a509658a05/eckitlib-2.0.7.19-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f2876c3fd3b2c3140a4c5c3a95c63f6d04b874a6a2b62d0d06c8d6f22375c531", size = 7169835, upload-time = "2026-04-01T12:23:22.598Z" }, + { url = "https://files.pythonhosted.org/packages/b4/b9/fa7908d4b208d1635c13c06ea96183a25c89373ee536a746f709efb5275b/eckitlib-2.0.7.19-cp314-cp314-macosx_13_0_arm64.whl", hash = "sha256:f4605a53d939c24083af431e570466be14f42ff6ae7395a7fa5691768ea02e5b", size = 2480048, upload-time = "2026-04-01T12:20:37.276Z" }, + { url = "https://files.pythonhosted.org/packages/d9/d3/77779e785dfa6f90fea78ece275cbdd43fc19da39bb84c1b09f1db528881/eckitlib-2.0.7.19-cp314-cp314-macosx_13_0_x86_64.whl", hash = "sha256:a34d28f3b8903527f1693327982ad4539c228c360c24bcd2b3e357ed9b267d0a", size = 2610838, upload-time = "2026-04-01T12:53:05.693Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d5/0a8b7de8471eb8cda8d4c5680e983e24434391e587b48234f6e92461e790/eckitlib-2.0.7.19-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:54fefebd47b7cadef235b2d1082e34ad9aabaf1cbee97ca9f7e72f99ea69685c", size = 7040523, upload-time = "2026-04-01T12:13:40.089Z" }, + { url = "https://files.pythonhosted.org/packages/61/e2/af1b905dc937bbc44d92c3d6a15d2fdab4b2d228b99583952844a317f760/eckitlib-2.0.7.19-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2479b853bb7b1dc14d5c710a9803e08bc6b3784ed721deec9799dd51b59cce76", size = 7169842, upload-time = "2026-04-01T12:32:41.65Z" }, +] + [[package]] name = "entrypoints" version = "0.4" @@ -1254,6 +1295,7 @@ earthkit = [ ifs = [ { name = "cfgrib" }, { name = "eccodes" }, + { name = "eccodeslib" }, ] jobqueue = [ { name = "dask-jobqueue" }, @@ -1278,6 +1320,7 @@ requires-dist = [ { name = "earthkit-data", specifier = ">=0.19.0" }, { name = "earthkit-meteo", marker = "extra == 'earthkit'", specifier = ">=0.6.1" }, { name = "eccodes", marker = "extra == 'ifs'", specifier = ">=2.45.0" }, + { name = "eccodeslib", marker = "extra == 'ifs'", specifier = ">=2.46.2.19" }, { name = "h5netcdf", specifier = ">=1.8.1" }, { name = "h5py", specifier = ">=3.15.1" }, { name = "netcdf4", specifier = ">=1.7.4" }, From 90d14ba45b4fdd50841b6b457bce2dbe1a6365a7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 May 2026 10:47:45 +0000 Subject: [PATCH 12/15] Add changelog entry for PR 21 changes Agent-Logs-Url: https://github.com/mlwp-tools/mxalign/sessions/ce510f28-d96f-401e-a6d7-602ccc1e9ede Co-authored-by: observingClouds <43613877+observingClouds@users.noreply.github.com> --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c06e818..c9ee03e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Changed + +- Store dataset properties as netCDF-safe scalar attrs (`properties.space`, `properties.time`, `properties.uncertainty`) while keeping read compatibility with legacy `attrs["properties"]` dict/JSON data. +- Added CI test workflow using `uv` and the frozen `uv.lock`, installing all optional extras before running unit tests. +- Added optional `ifs` dependency group with `cfgrib`, `eccodes`, and `eccodeslib`. + ## [0.1.0](https://github.com/mlwp-tools/mxalign/releases/tag/v0.1.0) First release of `mxalign`, an xarray-based package for alignment of meteorological datasets, with the following functionality and configuration: From b8e96faad48b62bbf113ff26093ab86bf332459b Mon Sep 17 00:00:00 2001 From: Hauke Schulz <43613877+observingClouds@users.noreply.github.com> Date: Thu, 21 May 2026 12:55:39 +0200 Subject: [PATCH 13/15] Apply suggestion from @observingClouds --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9ee03e..7180e61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- Store dataset properties as netCDF-safe scalar attrs (`properties.space`, `properties.time`, `properties.uncertainty`) while keeping read compatibility with legacy `attrs["properties"]` dict/JSON data. -- Added CI test workflow using `uv` and the frozen `uv.lock`, installing all optional extras before running unit tests. -- Added optional `ifs` dependency group with `cfgrib`, `eccodes`, and `eccodeslib`. +- Store dataset properties as netCDF-safe individual attributes while keeping read compatibility with legacy `attrs["properties"]` dict/JSON data. [\#21](https://github.com/mlwp-tools/mxalign/pull/21) @observingClouds +- Added CI test workflow with first unit tests. [\#21](https://github.com/mlwp-tools/mxalign/pull/21) @observingClouds +- Added optional `ifs` dependency group with `cfgrib`, `eccodes`, and `eccodeslib`. [\#21](https://github.com/mlwp-tools/mxalign/pull/21) @observingClouds ## [0.1.0](https://github.com/mlwp-tools/mxalign/releases/tag/v0.1.0) From c4b90887a9ca14acc5fb7fc385fadfafd47e54d7 Mon Sep 17 00:00:00 2001 From: Hauke Schulz <43613877+observingClouds@users.noreply.github.com> Date: Fri, 22 May 2026 09:52:37 +0200 Subject: [PATCH 14/15] use pytest instead of unittest --- .github/workflows/tests.yaml | 2 +- pyproject.toml | 1 + uv.lock | 40 +++++++++++++++++++++++++++++++++++- 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 02b466d..6652d87 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -21,4 +21,4 @@ jobs: - name: Install dependencies run: uv sync --frozen --all-extras - name: Run tests - run: uv run python -m unittest discover -s tests -v + run: uv run pytest tests -v diff --git a/pyproject.toml b/pyproject.toml index 4a345f3..edaa73b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,4 +48,5 @@ build-backend = "hatchling.build" [dependency-groups] dev = [ "ipykernel>=7.2.0", + "pytest>=8.0.0", ] diff --git a/uv.lock b/uv.lock index eeafaed..7d12970 100644 --- a/uv.lock +++ b/uv.lock @@ -794,6 +794,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + [[package]] name = "ipykernel" version = "7.2.0" @@ -1307,6 +1316,7 @@ verification = [ [package.dev-dependencies] dev = [ { name = "ipykernel" }, + { name = "pytest" }, ] [package.metadata] @@ -1333,7 +1343,10 @@ requires-dist = [ provides-extras = ["earthkit", "ifs", "verification", "jobqueue"] [package.metadata.requires-dev] -dev = [{ name = "ipykernel", specifier = ">=7.2.0" }] +dev = [ + { name = "ipykernel", specifier = ">=7.2.0" }, + { name = "pytest", specifier = ">=8.0.0" }, +] [[package]] name = "narwhals" @@ -1676,6 +1689,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/e3/1eddccb2c39ecfbe09b3add42a04abcc3fa5b468aa4224998ffb8a7e9c8f/platformdirs-4.7.0-py3-none-any.whl", hash = "sha256:1ed8db354e344c5bb6039cd727f096af975194b508e37177719d562b2b540ee6", size = 18983, upload-time = "2026-02-12T22:21:52.237Z" }, ] +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + [[package]] name = "prompt-toolkit" version = "3.0.52" @@ -1839,6 +1861,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/82/06/cad54e8ce758bd836ee5411691cbd49efeb9cc611b374670fce299519334/pyshp-3.0.3-py3-none-any.whl", hash = "sha256:28c8fac8c0c25bb0fecbbfd10ead7f319c2ff2f3b0b44a94f22bd2c93510ad42", size = 58465, upload-time = "2025-11-28T17:47:30.328Z" }, ] +[[package]] +name = "pytest" +version = "9.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" From fdc412c196ebe3d48024bdc38170f4c06b34dbfb Mon Sep 17 00:00:00 2001 From: Hauke Schulz <43613877+observingClouds@users.noreply.github.com> Date: Fri, 22 May 2026 11:12:39 +0200 Subject: [PATCH 15/15] remove unittest --- tests/test_properties_attrs.py | 39 ++++++++++++---------------------- 1 file changed, 14 insertions(+), 25 deletions(-) diff --git a/tests/test_properties_attrs.py b/tests/test_properties_attrs.py index 910cd72..9a7e06f 100644 --- a/tests/test_properties_attrs.py +++ b/tests/test_properties_attrs.py @@ -1,6 +1,5 @@ import json import tempfile -import unittest import xarray as xr @@ -8,7 +7,7 @@ from mxalign.properties.utils import properties_from_attrs, set_properties_attrs -class TestPropertiesAttrs(unittest.TestCase): +class TestPropertiesAttrs: def test_properties_are_stored_in_netcdf_compatible_attrs(self): ds = xr.Dataset() props = Properties( @@ -19,15 +18,15 @@ def test_properties_are_stored_in_netcdf_compatible_attrs(self): ds = set_properties_attrs(ds, props) - self.assertNotIn("properties", ds.attrs) - self.assertEqual(ds.attrs["properties.space"], "point") - self.assertEqual(ds.attrs["properties.time"], "observation") - self.assertEqual(ds.attrs["properties.uncertainty"], "deterministic") + assert "properties" not in ds.attrs + assert ds.attrs["properties.space"] == "point" + assert ds.attrs["properties.time"] == "observation" + assert ds.attrs["properties.uncertainty"] == "deterministic" with tempfile.NamedTemporaryFile(suffix=".nc") as tmp: ds.to_netcdf(tmp.name) with xr.open_dataset(tmp.name) as ds_loaded: - self.assertEqual(properties_from_attrs(ds_loaded), props) + assert properties_from_attrs(ds_loaded) == props def test_properties_can_still_be_read_from_legacy_format(self): ds = xr.Dataset() @@ -36,13 +35,10 @@ def test_properties_can_still_be_read_from_legacy_format(self): "time": "observation", "uncertainty": "deterministic", } - self.assertEqual( - properties_from_attrs(ds), - Properties( - space=Space.POINT, - time=Time.OBSERVATION, - uncertainty=Uncertainty.DETERMINISTIC, - ), + assert properties_from_attrs(ds) == Properties( + space=Space.POINT, + time=Time.OBSERVATION, + uncertainty=Uncertainty.DETERMINISTIC, ) def test_properties_can_be_read_from_legacy_json_string(self): @@ -50,15 +46,8 @@ def test_properties_can_be_read_from_legacy_json_string(self): ds.attrs["properties"] = json.dumps( {"space": "point", "time": "observation", "uncertainty": "deterministic"} ) - self.assertEqual( - properties_from_attrs(ds), - Properties( - space=Space.POINT, - time=Time.OBSERVATION, - uncertainty=Uncertainty.DETERMINISTIC, - ), + assert properties_from_attrs(ds) == Properties( + space=Space.POINT, + time=Time.OBSERVATION, + uncertainty=Uncertainty.DETERMINISTIC, ) - - -if __name__ == "__main__": - unittest.main()