diff --git a/README.md b/README.md index 767d353..13ea880 100644 --- a/README.md +++ b/README.md @@ -19,10 +19,41 @@ access data from the [STAC - SpatioTemporal Asset Catalogs](https://stacspec.org Installing xcube-cmems directly from the git repository, clone the repository, direct into `xcube-stac`, and follow the steps below: -``` -$ conda env create -f environment.yml -$ conda activate xcube-stac -$ pip install . +```bash +conda env create -f environment.yml +conda activate xcube-stac +pip install . ``` This installs all the dependencies of `xcube-stac` into a fresh conda environment, then installs xcube-stac into this environment from the repository. + +## Testing + +To run the unit test suite: + +```bash +pytest +``` + +To analyze test coverage (after installing pytest as above): + +```bash +pytest --cov=xcube_stac +``` + +To produce an HTML +[coverage report](https://pytest-cov.readthedocs.io/en/latest/reporting.html): + +```bash +pytest --cov-report html --cov=xcube_stac +``` + +### Some notes on the strategy of unittesting: + +The unit test suite uses [pytest-recording](https://pypi.org/project/pytest-recording/) to mock STAC catalogs. During development an actual HTTP request is performed to a STAC catalog and the responses are saved in `cassettes/**.yaml` files. During testing, only the `cassettes/**.yaml` files are used without an actual HTTP request. During development run + +```bash +pytest -v -s --record-mode new_episodes +``` + +which saves the responses to `cassettes/**.yaml`. The testing can be then performed as usual. diff --git a/environment.yml b/environment.yml index 12a58c4..ced038a 100644 --- a/environment.yml +++ b/environment.yml @@ -4,7 +4,7 @@ channels: - defaults dependencies: # Required - - python>=3.10 + - python>=3.11 - pystac - pystac-client - xarray @@ -13,4 +13,5 @@ dependencies: - black - flake8 - pytest + - pytest-cov - pytest-recording diff --git a/pyproject.toml b/pyproject.toml index 9603870..7658dae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,11 +35,11 @@ exclude = [ [project.optional-dependencies] dev = [ + "black", + "flake8", "pytest", "pytest-cov", - "pytest-recording", - "black", - "flake8" + "pytest-recording" ] [project.urls] diff --git a/test/cassettes/test_stac/StacTest.test_do_bboxes_intersect.yaml b/test/cassettes/test_stac/StacTest.test_do_bboxes_intersect.yaml new file mode 100644 index 0000000..50ca058 --- /dev/null +++ b/test/cassettes/test_stac/StacTest.test_do_bboxes_intersect.yaml @@ -0,0 +1,150 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Connection: + - keep-alive + User-Agent: + - python-requests/2.31.0 + method: GET + uri: https://raw.githubusercontent.com/stac-extensions/label/main/examples/multidataset/catalog.json + response: + body: + string: "{\n \"stac_version\": \"1.0.0-rc.1\",\n \"type\": \"Catalog\",\n + \ \"id\": \"label_extension_demo\",\n \"title\": \"label extension demo\",\n + \ \"description\": \"Sample ML training data labels in the STAC format\",\n + \ \"links\": [\n {\n \"rel\": \"root\",\n \"href\": \"./catalog.json\"\n + \ },\n {\n \"rel\": \"child\",\n \"href\": \"zanzibar/collection.json\"\n + \ },\n {\n \"rel\": \"child\",\n \"href\": \"spacenet-buildings/collection.json\"\n + \ }\n ]\n}" + headers: + Accept-Ranges: + - bytes + Access-Control-Allow-Origin: + - '*' + Cache-Control: + - max-age=300 + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Length: + - '236' + Content-Security-Policy: + - default-src 'none'; style-src 'unsafe-inline'; sandbox + Content-Type: + - text/plain; charset=utf-8 + Cross-Origin-Resource-Policy: + - cross-origin + Date: + - Tue, 14 May 2024 09:08:52 GMT + ETag: + - W/"acb7a8d6636e24e32f4018c14f1c4ff418a82567b2746560f9eae6ad97a48a54" + Expires: + - Tue, 14 May 2024 09:13:52 GMT + Source-Age: + - '0' + Strict-Transport-Security: + - max-age=31536000 + Vary: + - Authorization,Accept-Encoding,Origin + Via: + - 1.1 varnish + X-Cache: + - HIT + X-Cache-Hits: + - '0' + X-Content-Type-Options: + - nosniff + X-Fastly-Request-ID: + - 676dd71a8ff122cdec17e94a3ab8a088bd54d819 + X-Frame-Options: + - deny + X-GitHub-Request-Id: + - D0EE:148AA2:18A7844:1A1CD48:66432132 + X-Served-By: + - cache-fra-eddf8230108-FRA + X-Timer: + - S1715677732.487448,VS0,VE143 + X-XSS-Protection: + - 1; mode=block + status: + code: 200 + message: OK +- request: + body: null + headers: + Connection: + - close + Host: + - raw.githubusercontent.com + User-Agent: + - Python-urllib/3.12 + method: GET + uri: https://raw.githubusercontent.com/stac-extensions/label/main/examples/multidataset/catalog.json + response: + body: + string: "{\n \"stac_version\": \"1.0.0-rc.1\",\n \"type\": \"Catalog\",\n + \ \"id\": \"label_extension_demo\",\n \"title\": \"label extension demo\",\n + \ \"description\": \"Sample ML training data labels in the STAC format\",\n + \ \"links\": [\n {\n \"rel\": \"root\",\n \"href\": \"./catalog.json\"\n + \ },\n {\n \"rel\": \"child\",\n \"href\": \"zanzibar/collection.json\"\n + \ },\n {\n \"rel\": \"child\",\n \"href\": \"spacenet-buildings/collection.json\"\n + \ }\n ]\n}" + headers: + Accept-Ranges: + - bytes + Access-Control-Allow-Origin: + - '*' + Cache-Control: + - max-age=300 + Connection: + - close + Content-Length: + - '436' + Content-Security-Policy: + - default-src 'none'; style-src 'unsafe-inline'; sandbox + Content-Type: + - text/plain; charset=utf-8 + Cross-Origin-Resource-Policy: + - cross-origin + Date: + - Tue, 14 May 2024 09:08:52 GMT + ETag: + - '"e74ebcbc46d43c5b693ecb995381fbeba03583627e6d65b21ed7678a10d94729"' + Expires: + - Tue, 14 May 2024 09:13:52 GMT + Source-Age: + - '0' + Strict-Transport-Security: + - max-age=31536000 + Vary: + - Authorization,Accept-Encoding,Origin + Via: + - 1.1 varnish + X-Cache: + - MISS + X-Cache-Hits: + - '0' + X-Content-Type-Options: + - nosniff + X-Fastly-Request-ID: + - 36989abd8874aadf2e50450fc56334d4fe7c358e + X-Frame-Options: + - deny + X-GitHub-Request-Id: + - 6A22:312E01:32FCAB4:35F3692:66432A24 + X-Served-By: + - cache-fra-eddf8230147-FRA + X-Timer: + - S1715677733.682246,VS0,VE177 + X-XSS-Protection: + - 1; mode=block + status: + code: 200 + message: OK +version: 1 diff --git a/test/cassettes/test_stac/StacTest.test_is_datetime_in_range.yaml b/test/cassettes/test_stac/StacTest.test_is_datetime_in_range.yaml new file mode 100644 index 0000000..c97cf6e --- /dev/null +++ b/test/cassettes/test_stac/StacTest.test_is_datetime_in_range.yaml @@ -0,0 +1,150 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Connection: + - keep-alive + User-Agent: + - python-requests/2.31.0 + method: GET + uri: https://raw.githubusercontent.com/stac-extensions/label/main/examples/multidataset/catalog.json + response: + body: + string: "{\n \"stac_version\": \"1.0.0-rc.1\",\n \"type\": \"Catalog\",\n + \ \"id\": \"label_extension_demo\",\n \"title\": \"label extension demo\",\n + \ \"description\": \"Sample ML training data labels in the STAC format\",\n + \ \"links\": [\n {\n \"rel\": \"root\",\n \"href\": \"./catalog.json\"\n + \ },\n {\n \"rel\": \"child\",\n \"href\": \"zanzibar/collection.json\"\n + \ },\n {\n \"rel\": \"child\",\n \"href\": \"spacenet-buildings/collection.json\"\n + \ }\n ]\n}" + headers: + Accept-Ranges: + - bytes + Access-Control-Allow-Origin: + - '*' + Cache-Control: + - max-age=300 + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Length: + - '236' + Content-Security-Policy: + - default-src 'none'; style-src 'unsafe-inline'; sandbox + Content-Type: + - text/plain; charset=utf-8 + Cross-Origin-Resource-Policy: + - cross-origin + Date: + - Tue, 14 May 2024 09:08:53 GMT + ETag: + - W/"acb7a8d6636e24e32f4018c14f1c4ff418a82567b2746560f9eae6ad97a48a54" + Expires: + - Tue, 14 May 2024 09:13:53 GMT + Source-Age: + - '1' + Strict-Transport-Security: + - max-age=31536000 + Vary: + - Authorization,Accept-Encoding,Origin + Via: + - 1.1 varnish + X-Cache: + - HIT + X-Cache-Hits: + - '1' + X-Content-Type-Options: + - nosniff + X-Fastly-Request-ID: + - 2cbca05f9d22f301c772d1262935378fd999b79a + X-Frame-Options: + - deny + X-GitHub-Request-Id: + - D0EE:148AA2:18A7844:1A1CD48:66432132 + X-Served-By: + - cache-fra-eddf8230053-FRA + X-Timer: + - S1715677734.969463,VS0,VE1 + X-XSS-Protection: + - 1; mode=block + status: + code: 200 + message: OK +- request: + body: null + headers: + Connection: + - close + Host: + - raw.githubusercontent.com + User-Agent: + - Python-urllib/3.12 + method: GET + uri: https://raw.githubusercontent.com/stac-extensions/label/main/examples/multidataset/catalog.json + response: + body: + string: "{\n \"stac_version\": \"1.0.0-rc.1\",\n \"type\": \"Catalog\",\n + \ \"id\": \"label_extension_demo\",\n \"title\": \"label extension demo\",\n + \ \"description\": \"Sample ML training data labels in the STAC format\",\n + \ \"links\": [\n {\n \"rel\": \"root\",\n \"href\": \"./catalog.json\"\n + \ },\n {\n \"rel\": \"child\",\n \"href\": \"zanzibar/collection.json\"\n + \ },\n {\n \"rel\": \"child\",\n \"href\": \"spacenet-buildings/collection.json\"\n + \ }\n ]\n}" + headers: + Accept-Ranges: + - bytes + Access-Control-Allow-Origin: + - '*' + Cache-Control: + - max-age=300 + Connection: + - close + Content-Length: + - '436' + Content-Security-Policy: + - default-src 'none'; style-src 'unsafe-inline'; sandbox + Content-Type: + - text/plain; charset=utf-8 + Cross-Origin-Resource-Policy: + - cross-origin + Date: + - Tue, 14 May 2024 09:08:54 GMT + ETag: + - '"e74ebcbc46d43c5b693ecb995381fbeba03583627e6d65b21ed7678a10d94729"' + Expires: + - Tue, 14 May 2024 09:13:54 GMT + Source-Age: + - '1' + Strict-Transport-Security: + - max-age=31536000 + Vary: + - Authorization,Accept-Encoding,Origin + Via: + - 1.1 varnish + X-Cache: + - HIT + X-Cache-Hits: + - '1' + X-Content-Type-Options: + - nosniff + X-Fastly-Request-ID: + - 4d79d1d040b572d5e021147a378a3e58b1c0a14a + X-Frame-Options: + - deny + X-GitHub-Request-Id: + - 6A22:312E01:32FCAB4:35F3692:66432A24 + X-Served-By: + - cache-fra-eddf8230034-FRA + X-Timer: + - S1715677734.018500,VS0,VE2 + X-XSS-Protection: + - 1; mode=block + status: + code: 200 + message: OK +version: 1 diff --git a/test/test_opener.py b/test/test_opener.py index b301ae1..1ce2d9c 100644 --- a/test/test_opener.py +++ b/test/test_opener.py @@ -20,11 +20,12 @@ # SOFTWARE. import unittest -import pytest +import pytest from xcube.util.jsonschema import JsonObjectSchema -from xcube_stac.store import StacDataOpener + from xcube_stac.stac import Stac +from xcube_stac.store import StacDataOpener class StacDataOpenerTest(unittest.TestCase): diff --git a/test/test_plugin.py b/test/test_plugin.py index 796135c..ec3dd68 100644 --- a/test/test_plugin.py +++ b/test/test_plugin.py @@ -22,6 +22,7 @@ import unittest from xcube.util.extension import ExtensionRegistry + from xcube_stac.plugin import init_plugin diff --git a/test/test_stac.py b/test/test_stac.py index 4527046..75519ac 100644 --- a/test/test_stac.py +++ b/test/test_stac.py @@ -20,11 +20,13 @@ # SOFTWARE. import unittest -import pytest + from pystac import ItemCollection -from xcube_stac.stac import Stac +import pytest from xcube.util.jsonschema import JsonObjectSchema +from xcube_stac.stac import Stac + class StacTest(unittest.TestCase): @@ -57,7 +59,7 @@ def test_get_item_collection(self): "spacenet-buildings-collection/AOI_4_Shanghai_img3344" ] self.assertIsInstance(items, ItemCollection) - self.assertListEqual(data_id_items, data_id_items_expected) + self.assertCountEqual(data_id_items, data_id_items_expected) self.assertEqual(len(items), len(data_id_items)) @pytest.mark.vcr() @@ -72,7 +74,7 @@ def test_get_item_collection_open_params(self): "zanzibar-collection/znz001", ] self.assertIsInstance(items, ItemCollection) - self.assertListEqual(data_id_items, data_id_items_expected) + self.assertCountEqual(data_id_items, data_id_items_expected) self.assertEqual(len(items), len(data_id_items)) items, data_id_items = stac_instance.get_item_collection( @@ -108,11 +110,11 @@ def test_get_item_collection_searchable_catalog(self): "sentinel-2-l2a/S2A_32UNU_20200302_0_L2A" ] self.assertIsInstance(items, ItemCollection) - self.assertListEqual(data_id_items, data_id_items_expected) + self.assertCountEqual(data_id_items, data_id_items_expected) self.assertEqual(len(items), len(data_id_items)) @pytest.mark.vcr() - def test_assert_datetime(self): + def test_is_datetime_in_range(self): class Item1(): def __init__(self) -> None: @@ -129,74 +131,63 @@ def __init__(self) -> None: end_datetime="2024-05-02T09:19:38.543000Z" ) - item1_open_paramss = [ - dict(time_range=["2024-04-30", "2024-05-03"]), - dict(time_range=["2024-04-26", "2024-05-02"]), - dict(time_range=["2024-04-26", "2024-05-01"]), - ] - item1_funs = [ - self.assertTrue, - self.assertFalse, - self.assertFalse + item1_test_paramss = [ + ("2024-04-30", "2024-05-03", self.assertTrue), + ("2024-04-26", "2024-05-02", self.assertFalse), + ("2024-04-26", "2024-05-01", self.assertFalse) ] - item2_open_paramss = [ - dict(time_range=["2024-05-05", "2024-05-08"]), - dict(time_range=["2024-04-30", "2024-05-03"]), - dict(time_range=["2024-04-26", "2024-04-29"]), - dict(time_range=["2023-11-26", "2023-12-31"]), - dict(time_range=["2023-11-26", "2023-11-30"]), - dict(time_range=["2023-11-26", "2024-05-08"]) - ] - item2_funs = [ - self.assertFalse, - self.assertTrue, - self.assertTrue, - self.assertTrue, - self.assertFalse, - self.assertTrue + item2_test_paramss = [ + ("2024-05-05", "2024-05-08", self.assertFalse), + ("2024-04-30", "2024-05-03", self.assertTrue), + ("2024-04-26", "2024-04-29", self.assertTrue), + ("2023-11-26", "2023-12-31", self.assertTrue), + ("2023-11-26", "2023-11-30", self.assertFalse), + ("2023-11-26", "2024-05-08", self.assertTrue), ] stac_instance = Stac(self.url_nonsearchable) item1 = Item1() - for (open_params, fun) in zip(item1_open_paramss, item1_funs): + for (time_start, time_end, fun) in item1_test_paramss: fun( - stac_instance._assert_datetime(item1, **open_params) + stac_instance._is_datetime_in_range( + item1, + time_range=[time_start, time_end] + ) ) item1 = Item2() - for (open_params, fun) in zip(item2_open_paramss, item2_funs): + for (time_start, time_end, fun) in item2_test_paramss: fun( - stac_instance._assert_datetime(item1, **open_params) + stac_instance._is_datetime_in_range( + item1, + time_range=[time_start, time_end] + ) ) @pytest.mark.vcr() - def test_assert_bbox_intersect(self): + def test_do_bboxes_intersect(self): class Item(): def __init__(self) -> None: self.bbox = [0, 0, 1, 1] - item_open_paramss = [ - dict(bbox=[0, 0, 1, 1]), - dict(bbox=[0.5, 0.5, 1.5, 1.5]), - dict(bbox=[-0.5, -0.5, 0.5, 0.5]), - dict(bbox=[1, 1, 2, 2]), - dict(bbox=[2, 2, 3, 3]) - ] - item_funs = [ - self.assertTrue, - self.assertTrue, - self.assertTrue, - self.assertTrue, - self.assertFalse + item_test_paramss = [ + (0, 0, 1, 1, self.assertTrue), + (0.5, 0.5, 1.5, 1.5, self.assertTrue), + (-0.5, -0.5, 0.5, 0.5, self.assertTrue), + (1, 1, 2, 2, self.assertTrue), + (2, 2, 3, 3, self.assertFalse) ] stac_instance = Stac(self.url_nonsearchable) item = Item() - for (open_params, fun) in zip(item_open_paramss, item_funs): + for (west, south, east, north, fun) in item_test_paramss: fun( - stac_instance._assert_bbox_intersect(item, **open_params) + stac_instance._do_bboxes_intersect( + item, + bbox=[west, south, east, north] + ) ) diff --git a/test/test_store.py b/test/test_store.py index d1ae2d0..d1b94c6 100644 --- a/test/test_store.py +++ b/test/test_store.py @@ -20,12 +20,13 @@ # SOFTWARE. import unittest -import pytest from pystac import ItemCollection -from xcube.util.jsonschema import JsonObjectSchema +import pytest from xcube.core.store import DataStoreError from xcube.core.store.store import new_data_store +from xcube.util.jsonschema import JsonObjectSchema + from xcube_stac.constants import DATA_STORE_ID diff --git a/xcube_stac/opener.py b/xcube_stac/opener.py index 1c9503f..9a7889f 100644 --- a/xcube_stac/opener.py +++ b/xcube_stac/opener.py @@ -19,17 +19,14 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -import logging import xarray as xr - -from xcube.util.jsonschema import JsonObjectSchema from xcube.core.store import ( DataOpener, DatasetDescriptor ) -from .stac import Stac +from xcube.util.jsonschema import JsonObjectSchema -_LOG = logging.getLogger("xcube") +from .stac import Stac class StacDataOpener(DataOpener): @@ -40,7 +37,11 @@ class StacDataOpener(DataOpener): """ def __init__(self, stac: Stac): - self.stac = stac + self._stac = stac + + @property + def stac(self) -> Stac: + return self._stac def get_open_data_params_schema(self, data_id: str = None) -> JsonObjectSchema: return self.stac.get_open_data_params_schema(data_id) diff --git a/xcube_stac/plugin.py b/xcube_stac/plugin.py index 5ef9c69..a03bd2f 100644 --- a/xcube_stac/plugin.py +++ b/xcube_stac/plugin.py @@ -19,11 +19,16 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +from xcube.constants import ( + EXTENSION_POINT_DATA_OPENERS, + EXTENSION_POINT_DATA_STORES +) from xcube.util import extension -from xcube.constants import EXTENSION_POINT_DATA_OPENERS -from xcube.constants import EXTENSION_POINT_DATA_STORES -from xcube_stac.constants import DATASET_OPENER_ID -from xcube_stac.constants import DATA_STORE_ID + +from .constants import ( + DATASET_OPENER_ID, + DATA_STORE_ID +) def init_plugin(ext_registry: extension.ExtensionRegistry): diff --git a/xcube_stac/stac.py b/xcube_stac/stac.py index d12d98b..d45ad0e 100644 --- a/xcube_stac/stac.py +++ b/xcube_stac/stac.py @@ -19,21 +19,18 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from typing import Tuple, Iterable, Iterator, Union, List - from datetime import datetime, timezone -from shapely.geometry import box -import xarray as xr from itertools import chain +from typing import Tuple, Iterable, Iterator, Union, List import pystac - -import pystac_client from pystac import Catalog, Collection, ItemCollection, Item - +import pystac_client +from shapely.geometry import box +import xarray as xr from xcube.util.jsonschema import JsonObjectSchema -from xcube_stac.constants import STAC_SEARCH_ITEM_PARAMETERS +from .constants import STAC_SEARCH_ITEM_PARAMETERS class Stac: @@ -63,23 +60,26 @@ def __init__( if not catalog.conforms_to("ITEM_SEARCH"): catalog = pystac.Catalog.from_file(url) self._searchable = False - self.catalog = catalog + self._catalog = catalog # TODO: Add a data store "file" here when implementing # open_data(), which will be used to open the hrefs + @property + def catalog(self) -> Catalog: + return self._catalog + def get_open_data_params_schema(self, data_id: str = None) -> JsonObjectSchema: """Get the JSON schema for instantiating a new data store. Returns: The JSON schema for the data store's parameters. """ - stac_schema = JsonObjectSchema( + return JsonObjectSchema( properties=dict(**STAC_SEARCH_ITEM_PARAMETERS), required=[], additional_properties=False ) - return stac_schema def get_item_collection( self, **open_params @@ -92,13 +92,13 @@ def get_item_collection( item_data_ids: data IDs corresponding to items """ if self._searchable: - open_params_mod = open_params.copy() - if "variable_names" in open_params_mod: - del open_params_mod["variable_names"] - if "time_range" in open_params_mod: - open_params_mod["datetime"] = "/".join(open_params_mod["time_range"]) - del open_params_mod["time_range"] - items = self.catalog.search(**open_params_mod).item_collection() + # not used + open_params.pop("variable_names", None) + # rewrite to "datetime" + time_range = open_params.pop("time_range", None) + if time_range: + open_params["datetime"] = "/".join(time_range) + items = self.catalog.search(**open_params).item_collection() else: items = self._get_items_nonsearchable_catalog( self.catalog, @@ -120,15 +120,15 @@ def get_item_data_id(self, item: Item) -> str: data ID of an item """ id_parts = [] - pystac_object = item - while pystac_object.STAC_OBJECT_TYPE != "Catalog": - id_parts.append(pystac_object.id) - pystac_object = pystac_object.get_parent() + parent_item = item + while parent_item.STAC_OBJECT_TYPE != "Catalog": + id_parts.append(parent_item.id) + parent_item = parent_item.get_parent() id_parts.reverse() return self._data_id_delimiter.join(id_parts) def get_item_data_ids(self, items: Iterable[Item]) -> Iterator[str]: - """Generates the data ID of an item collection, + """Generates the data IDs of an item collection, which follows the structure: `collection_id_0/../collection_id_n/item_id` @@ -180,27 +180,24 @@ def _get_items_nonsearchable_catalog( recursive: bool = True, **open_params ) -> Iterator[Tuple[Item, str]]: - """Get the items for a catalog of the catalog, which is not - conform with the 'ITEM_SEARCH' specifications. + """Get the items of a catalog which does not implement the + "STAC API - Item Search" conformance class. Args: pystac_object: either a `pystac.catalog:Catalog` or a `pystac.collection:Collection` object - recursive: If True, data IDs of a multiple collection - and/or nested collection STAC catalog can be build. If False, - flat STAC catalog hierarchy is assumed consisting only of items. - Defaults to True. + recursive: If True, the data IDs of a multiple-collection + and/or nested-collection STAC catalog can be collected. If False, + a flat STAC catalog hierarchy is assumed, consisting only of items. Yields: An iterator over the items matching the **open_params. """ if ( - pystac_object.extra_fields["type"] == "Collection" and - pystac_object.id not in open_params.get("collections", [pystac_object.id]) + pystac_object.extra_fields["type"] != "Collection" or + pystac_object.id in open_params.get("collections", [pystac_object.id]) ): - pass - else: if recursive: if any(True for _ in pystac_object.get_children()): iterators = (self._get_items_nonsearchable_catalog( @@ -220,26 +217,29 @@ def _get_items_nonsearchable_catalog( for item in pystac_object.get_items(): # test if item's bbox intersects with the desired bbox if "bbox" in open_params: - if not self._assert_bbox_intersect(item, **open_params): + if not self._do_bboxes_intersect(item, **open_params): continue # test if item fit to desired time range if "time_range" in open_params: - if not self._assert_datetime(item, **open_params): + if not self._is_datetime_in_range(item, **open_params): continue # iterate through assets of item yield item - def _assert_datetime(self, item: Item, **open_params) -> bool: - """Assert if the datetime or datetime range of an item fits to the - 'time_range' given by *open_params*. + def _is_datetime_in_range(self, item: Item, **open_params) -> bool: + """Determine whether the datetime or datetime range of an item + intersects to the 'time_range' given by *open_params*. Args: item: item/feature + open_params: Optional opening parameters which need + to include 'time_range' + Returns: True, if the datetime of an item is within the 'time_range', - otherwise False. True, if there is any overlap between the - 'time_range' and the datetime range of an item. + or if there is any overlap between the 'time_range' and + the datetime range of an item; otherwise False. """ dt_start = datetime.fromisoformat( @@ -255,22 +255,18 @@ def _assert_datetime(self, item: Item, **open_params) -> bool: dt_end_data = datetime.fromisoformat( item.properties["end_datetime"] ) - if dt_end < dt_start_data or dt_start > dt_end_data: - return False - else: - return True + return dt_end >= dt_start_data and dt_start <= dt_end_data else: dt_data = datetime.fromisoformat(item.properties["datetime"]) - if dt_end < dt_data or dt_start > dt_data: - return False - else: - return True + return dt_start <= dt_data <= dt_end - def _assert_bbox_intersect(self, item: Item, **open_params) -> bool: - """Checks if two bounding boxes intersect. + def _do_bboxes_intersect(self, item: Item, **open_params) -> bool: + """Determine whether two bounding boxes intersect. Args: item: item/feature + open_params: Optional opening parameters which need + to include 'bbox' Returns: True if the bounding box given by the item intersects with diff --git a/xcube_stac/store.py b/xcube_stac/store.py index fdf5ee2..459e0f2 100644 --- a/xcube_stac/store.py +++ b/xcube_stac/store.py @@ -19,16 +19,10 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +from pystac import ItemCollection from typing import Any, Tuple, Iterator, Dict, Container, Union, List -import logging import xarray as xr - -from pystac import ItemCollection -from xcube.util.jsonschema import ( - JsonObjectSchema, - JsonStringSchema -) from xcube.core.store import ( DATASET_TYPE, DataDescriptor, @@ -36,12 +30,15 @@ DataStoreError, DataTypeLike ) +from xcube.util.jsonschema import ( + JsonObjectSchema, + JsonStringSchema +) + from .constants import DATASET_OPENER_ID from .opener import StacDataOpener from .stac import Stac -_LOG = logging.getLogger("xcube") - class StacDataStore(StacDataOpener, DataStore): """STAC implementation of the data store. @@ -249,7 +246,7 @@ def _is_valid_data_type(cls, data_type: DataTypeLike) -> bool: by the store. Args: - data_type (DataTypeLike): Data type that is to be checked. + data_type: Data type that is to be checked. Returns: bool: True if *data_type* is supported by the store, otherwise False @@ -262,7 +259,7 @@ def _assert_valid_data_type(cls, data_type: DataTypeLike): by the store. Args: - data_type (DataTypeLike): Data type that is to be checked. + data_type: Data type that is to be checked. Raises: DataStoreError: Error, if *data_type* is not