Skip to content

Commit 8c7947c

Browse files
Merge pull request #44 from xcube-dev/tejas-xxx-fix_list_data_ids_method
fix and update cmems store
2 parents 6acf6a2 + e329dec commit 8c7947c

File tree

9 files changed

+117
-88
lines changed

9 files changed

+117
-88
lines changed

.github/workflows/unitest-workflow.yml

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,34 @@ jobs:
1515
NUMBA_DISABLE_JIT: 1
1616
steps:
1717
- name: checkout xcube-cmems
18-
uses: actions/checkout@v3
18+
uses: actions/checkout@v4
1919

20-
- name: Set up MicroMamba
21-
uses: mamba-org/provision-with-micromamba@main
20+
- name: Set up Micromamba
21+
uses: mamba-org/setup-micromamba@v2
2222
with:
23-
cache-env: true
24-
extra-specs: |
25-
python=3.10
26-
27-
- name: Run unit tests
28-
shell: bash -l {0}
29-
run: |
30-
cd /home/runner/work/xcube-cmems/xcube-cmems
31-
ls
32-
pytest
33-
23+
micromamba-version: '1.4.8-0'
24+
environment-file: environment.yml
25+
init-shell: >-
26+
bash
27+
# Don't cache the environment, since this would prevent us from
28+
# catching test failures caused by updated versions of dependencies.
29+
cache-environment: false
30+
post-cleanup: 'all'
31+
32+
- name: setup-xcube-cmems
33+
shell: bash -l {0}
34+
run: |
35+
conda info
36+
conda list
37+
pip install -e .
38+
39+
- name: unittest-xcube
40+
shell: bash -l {0}
41+
run: |
42+
pytest --cov=xcube_cmems --cov-report=xml
43+
44+
- uses: codecov/codecov-action@v4
45+
with:
46+
verbose: true # optional (default = false)
47+
48+

CHANGES.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,17 @@
44
to `xcube_cmems` and entry point removed, since xcube plugins
55
auto-recognition is updated. (#39 and xcube-dev/xcube#963)
66

7+
- Support boolean-valued include_attrs in get_data_ids in accordance with API update in
8+
xcube 1.8.0.
9+
10+
- Refactored `get_datasets_with_titles()` to align with the updated return type of
11+
`cm.describe()`. (now returning a `CopernicusMarineCatalogue` object). The function
12+
now accesses products and datasets via object attributes instead of dictionary keys.
13+
14+
- Updated dependency versions to ensure compatibility with `copernicusmarine` >= 2.1.1.
15+
16+
- Updated GitHub Actions workflow to use the latest Micromamba setup and use codecov.
17+
718
## Changes in 0.1.5
819

920
- Disabled metadata cache to make it more suitable for cloud based environments. (#36)

environment.yml

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,20 @@ name: xcube-cmems
22
channels:
33
- conda-forge
44
dependencies:
5+
# Python
6+
- python >=3.10
57
# Required
6-
- copernicusmarine >=1.0.10
7-
- numpy
8+
- copernicusmarine >=2.1.1
9+
- xcube >=1.9.1
10+
- numpy <2.0.0 # to avoid inconsistent results with copernicusmarine package
11+
- xarray >=2024.7.0 # to avoid inconsistent results with copernicusmarine package
812
- pandas
9-
- xarray
10-
- xcube>=1.5.1
1113
# for testing
1214
- black
1315
- flake8
1416
- mock
15-
- pathlib
1617
- pytest
17-
18+
- pytest-cov
1819

1920

2021

pyproject.toml

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@ license = {text = "MIT"}
2121
requires-python = ">=3.10"
2222
dependencies = [
2323
# Todo: add xcube-core when issue with matplotlib-base is solved
24-
"copernicusmarine",
25-
"numpy",
24+
"copernicusmarine>=2.1.1",
25+
"numpy<2.0.0",
26+
"xarray>=2024.7.0",
2627
"pandas",
27-
"xarray",
2828
"zarr"
2929
]
3030

@@ -41,22 +41,15 @@ exclude = [
4141

4242
[project.optional-dependencies]
4343
dev = [
44-
"pytest",
45-
"pytest-cov",
46-
"black",
47-
"flake8",
48-
"flake8-bugbear",
49-
"pathlib"
50-
]
51-
doc = [
52-
"mkdocs",
53-
"mkdocs-material",
54-
"mkdocstrings",
55-
"mkdocstrings-python"
44+
"pytest",
45+
"pytest-cov",
46+
"black",
47+
"flake8",
48+
"flake8-bugbear",
5649
]
5750

5851
[project.urls]
5952
Documentation = "https://dcs4cop.github.io/xcube-cmems/"
60-
Issues = "hhttps://github.com/dcs4cop/xcube-cmems/issues"
53+
Issues = "https://github.com/dcs4cop/xcube-cmems/issues"
6154
Changelog = "https://github.com/dcs4cop/xcube-cmems/blob/main/CHANGES.md"
6255
Repository = "https://github.com/dcs4cop/xcube-cmems"

test/test_cmems.py

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,13 @@
1818
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
1919
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2020
# SOFTWARE.
21-
import pathlib
21+
2222
import os
2323
import unittest
24+
from types import SimpleNamespace
25+
from unittest.mock import MagicMock, patch
2426

2527
from xcube_cmems.cmems import Cmems
26-
from unittest.mock import patch, MagicMock
2728

2829

2930
class CmemsTest(unittest.TestCase):
@@ -35,30 +36,28 @@ def setUp(self):
3536

3637
@patch("xcube_cmems.cmems.cm.describe")
3738
def test_get_datasets_with_titles(self, mock_describe):
38-
# Mock the response from cm.describe
39-
mock_describe.return_value = {
40-
"products": [
41-
{
42-
"title": "Product A",
43-
"datasets": [
44-
{"dataset_id": "dataset1"},
45-
{"dataset_id": "dataset2"},
46-
],
47-
},
48-
{"title": "Product B", "datasets": [{"dataset_id": "dataset3"}]},
49-
]
50-
}
39+
# Fake datasets
40+
dataset1 = SimpleNamespace(dataset_id="dataset1", dataset_name="Dataset 1")
41+
dataset2 = SimpleNamespace(dataset_id="dataset2", dataset_name="Dataset 2")
42+
dataset3 = SimpleNamespace(dataset_id="dataset3", dataset_name="Dataset 3")
43+
44+
# Fake products
45+
product_a = SimpleNamespace(title="Product A", datasets=[dataset1, dataset2])
46+
product_b = SimpleNamespace(title="Product B", datasets=[dataset3])
47+
48+
# Fake catalogue
49+
mock_catalogue = SimpleNamespace(products=[product_a, product_b])
50+
mock_describe.return_value = mock_catalogue
51+
5152
cmems = Cmems()
5253
datasets_info = cmems.get_datasets_with_titles()
5354

54-
# Expected result based on the mocked describe response
55-
expected_result = [
56-
{"title": "Product A", "dataset_id": "dataset1"},
57-
{"title": "Product A", "dataset_id": "dataset2"},
58-
{"title": "Product B", "dataset_id": "dataset3"},
55+
expected = [
56+
{"dataset_id": "dataset1", "title": "Product A - Dataset 1"},
57+
{"dataset_id": "dataset2", "title": "Product A - Dataset 2"},
58+
{"dataset_id": "dataset3", "title": "Product B - Dataset 3"},
5959
]
60-
61-
self.assertEqual(datasets_info, expected_result)
60+
self.assertEqual(datasets_info, expected)
6261

6362
@patch("xcube_cmems.cmems.cm.open_dataset")
6463
def test_open_dataset(self, mock_open_dataset):

test/test_store.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,16 @@
1919
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2020
# SOFTWARE.
2121
import unittest
22-
23-
import xarray as xr
2422
from datetime import datetime, timedelta
25-
from unittest.mock import patch, MagicMock
23+
from unittest.mock import MagicMock, patch
2624

25+
import xarray as xr
2726
import xcube.core.store.descriptor as xcube_des
2827
from xcube.util.jsonschema import JsonObjectSchema
28+
2929
from xcube_cmems.constants import DATASET_OPENER_ID
30-
from xcube_cmems.store import CmemsDatasetOpener
31-
from xcube_cmems.store import CmemsDataStore
30+
from xcube_cmems.store import CmemsDatasetOpener, CmemsDataStore
31+
3232
from .sample_data import create_cmems_dataset
3333

3434

xcube_cmems/cmems.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,17 @@ def __init__(
5656

5757
@classmethod
5858
def get_datasets_with_titles(cls) -> List[dict]:
59-
catalogue: dict = cm.describe(include_datasets=True, no_metadata_cache=True)
59+
catalogue: CopernicusMarineCatalogue = cm.describe()
6060
datasets_info: List[dict] = []
61-
for product in catalogue["products"]:
62-
product_title = product["title"]
63-
for dataset in product["datasets"]:
64-
dataset_id: str = dataset["dataset_id"]
65-
datasets_info.append({"title": product_title, "dataset_id": dataset_id})
61+
for product in catalogue.products:
62+
product_title = product.title
63+
for dataset in product.datasets:
64+
datasets_info.append(
65+
{
66+
"dataset_id": dataset.dataset_id,
67+
"title": f"{product_title} - {dataset.dataset_name}",
68+
}
69+
)
6670
return datasets_info
6771

6872
def open_dataset(self, dataset_id, **open_params) -> xr.Dataset:
@@ -72,7 +76,6 @@ def open_dataset(self, dataset_id, **open_params) -> xr.Dataset:
7276
dataset_id=dataset_id,
7377
username=self.cmems_username,
7478
password=self.cmems_password,
75-
no_metadata_cache=True,
7679
**open_params,
7780
)
7881
return ds

xcube_cmems/plugin.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,10 @@
1919
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2020
# SOFTWARE.
2121

22+
from xcube.constants import EXTENSION_POINT_DATA_OPENERS, EXTENSION_POINT_DATA_STORES
2223
from xcube.util import extension
23-
from xcube.constants import EXTENSION_POINT_DATA_OPENERS
24-
from xcube.constants import EXTENSION_POINT_DATA_STORES
25-
from xcube_cmems.constants import DATASET_OPENER_ID
26-
from xcube_cmems.constants import DATA_STORE_ID
24+
25+
from xcube_cmems.constants import DATA_STORE_ID, DATASET_OPENER_ID
2726

2827

2928
def init_plugin(ext_registry: extension.ExtensionRegistry):

xcube_cmems/store.py

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,24 +19,23 @@
1919
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2020
# SOFTWARE.
2121

22-
from typing import Any, List, Tuple, Container, Union, Iterator, Dict
23-
2422
import logging
25-
import xarray as xr
23+
from typing import Any, Container, Dict, Iterator, List, Optional, Tuple
24+
2625
import numpy as np
2726
import pandas as pd
28-
27+
import xarray as xr
2928
from xarray.core.dataset import DataVariables
3029
from xcube.core.gridmapping import GridMapping
3130
from xcube.core.store import (
3231
DATASET_TYPE,
3332
DataDescriptor,
3433
DataOpener,
34+
DatasetDescriptor,
3535
DataStore,
3636
DataStoreError,
3737
DataType,
3838
DataTypeLike,
39-
DatasetDescriptor,
4039
VariableDescriptor,
4140
)
4241
from xcube.util.assertions import assert_not_none
@@ -87,19 +86,18 @@ def _get_var_descriptors(
8786
return var_descriptors
8887

8988
@staticmethod
90-
def _determine_time_period(data: xr.Dataset):
89+
def _determine_time_period(data: xr.Dataset) -> Optional[str]:
9190
if "time" in data and len(data["time"].values) > 1:
92-
time_diff = (
93-
data["time"].diff(dim=data["time"].dims[0]).values.astype(np.float64)
94-
)
91+
time_diff = data["time"].diff(dim=data["time"].dims[0]).values
9592
time_res = time_diff[0]
96-
time_regular = np.allclose(time_res, time_diff, 1e-8)
93+
time_regular = np.allclose(time_diff, time_res, rtol=1e-8, atol=0)
9794
if time_regular:
9895
time_period = pd.to_timedelta(time_res).isoformat()
9996
# remove leading P
10097
time_period = time_period[1:]
10198
# removing sub-day precision
10299
return time_period.split("T")[0]
100+
return None
103101

104102
def describe_data(self, data_id: str) -> DatasetDescriptor:
105103
xr_ds = self.cmems.open_dataset(data_id)
@@ -228,11 +226,21 @@ def _get_opener(
228226
return self._dataset_opener
229227

230228
def get_data_ids(
231-
self, data_type: DataTypeLike = None, include_attrs: Container[str] = None
232-
) -> Union[Iterator[str], Iterator[Tuple[str, Dict[str, Any]]]]:
229+
self,
230+
data_type: DataTypeLike = None,
231+
include_attrs: Container[str] | bool = False,
232+
) -> Iterator[str] | Iterator[Tuple[str, Dict[str, Any]]]:
233+
233234
dataset_ids_with_titles = self._dataset_opener.cmems.get_datasets_with_titles()
234-
return_tuples = include_attrs is not None
235-
include_titles = return_tuples and "title" in include_attrs
235+
236+
if isinstance(include_attrs, bool):
237+
return_tuples = include_attrs
238+
include_titles = include_attrs
239+
elif isinstance(include_attrs, Container):
240+
return_tuples = True
241+
include_titles = "title" in include_attrs
242+
else:
243+
raise ValueError(f"Invalid type {type(include_attrs)} for include_attrs")
236244

237245
for dataset in dataset_ids_with_titles:
238246
data_id = dataset["dataset_id"]

0 commit comments

Comments
 (0)