From 8bf40b69ac0b80ba75e1cba85e628d7eab6cd8c1 Mon Sep 17 00:00:00 2001 From: Jon Duckworth Date: Thu, 10 Jun 2021 13:10:32 -0400 Subject: [PATCH 01/14] Basic ItemCollection implementation --- pystac/__init__.py | 1 + pystac/item_collection.py | 85 + .../sample-item-collection.json | 1526 +++++++++++++++++ tests/test_item_collection.py | 100 ++ 4 files changed, 1712 insertions(+) create mode 100644 pystac/item_collection.py create mode 100644 tests/data-files/item-collection/sample-item-collection.json create mode 100644 tests/test_item_collection.py diff --git a/pystac/__init__.py b/pystac/__init__.py index 2722d0303..082f98f43 100644 --- a/pystac/__init__.py +++ b/pystac/__init__.py @@ -34,6 +34,7 @@ ) from pystac.summaries import RangeSummary from pystac.item import Item, Asset, CommonMetadata +from pystac.item_collection import ItemCollection import pystac.validation diff --git a/pystac/item_collection.py b/pystac/item_collection.py new file mode 100644 index 000000000..9cad2ca9a --- /dev/null +++ b/pystac/item_collection.py @@ -0,0 +1,85 @@ +from copy import deepcopy +from pystac.errors import STACTypeError +from typing import Any, Dict, Iterator, List, Optional, Sized, Iterable + +import pystac +from pystac.utils import make_absolute_href, is_absolute_href +from pystac.serialization.identify import identify_stac_object_type + + +class ItemCollection(Sized, Iterable[pystac.Item]): + """Implementation of a GeoJSON FeatureCollection whose features are all STAC + Items.""" + + items: List[pystac.Item] + """The list of :class:`pystac.Item` instances contained in this + ``ItemCollection``.""" + + extra_fields: Dict[str, Any] + """Dictionary containing additional top-level fields for the GeoJSON + FeatureCollection.""" + + def __init__( + self, items: List[pystac.Item], extra_fields: Optional[Dict[str, Any]] = None + ): + self.items = [item.clone() for item in items] + for item in self.items: + item.clear_links("root") + self.extra_fields = extra_fields or {} + + def __getitem__(self, idx: int) -> pystac.Item: + return self.items[idx] + + def __iter__(self) -> Iterator[pystac.Item]: + return iter(self.items) + + def __len__(self) -> int: + return len(self.items) + + def to_dict(self) -> Dict[str, Any]: + """Serializes an :class:`ItemCollection` instance to a JSON-like dictionary.""" + return { + "type": "FeatureCollection", + "features": [item.to_dict() for item in self.items], + **self.extra_fields, + } + + def clone(self) -> "ItemCollection": + """Creates a clone of this instance. This clone is a deep copy; all + :class:`~pystac.Item`s are cloned and all additional top-level fields are deep + copied.""" + return self.__class__( + items=[item.clone() for item in self.items], + extra_fields=deepcopy(self.extra_fields), + ) + + @classmethod + def from_dict(cls, d: Dict[str, Any]) -> "ItemCollection": + """Creates a :class:`ItemCollection` instance from a dictionary.""" + if identify_stac_object_type(d) != pystac.STACObjectType.ITEMCOLLECTION: + raise STACTypeError("Dict is not a valid ItemCollection") + + items = [pystac.Item.from_dict(item) for item in d.get("features", [])] + extra_fields = {k: v for k, v in d.items() if k not in ("features", "type")} + + return cls(items=items, extra_fields=extra_fields) + + @classmethod + def from_file( + cls, href: str, stac_io: Optional[pystac.StacIO] = None + ) -> "ItemCollection": + """Reads a :class:`ItemCollection` from a JSON file. + + Arguments: + href : Path to the file. + stac_io : A :class:`~pystac.StacIO` instance to use for file I/O + """ + if stac_io is None: + stac_io = pystac.StacIO.default() + + if not is_absolute_href(href): + href = make_absolute_href(href) + + d = stac_io.read_json(href) + + return cls.from_dict(d) diff --git a/tests/data-files/item-collection/sample-item-collection.json b/tests/data-files/item-collection/sample-item-collection.json new file mode 100644 index 000000000..d0727810f --- /dev/null +++ b/tests/data-files/item-collection/sample-item-collection.json @@ -0,0 +1,1526 @@ +{ + "type": "FeatureCollection", + "stac_version": "1.0.0-rc.4", + "numberMatched": 324132, + "numberReturned": 10, + "features": [ + { + "type": "Feature", + "id": "G1994512890-LPCLOUD", + "stac_version": "1.0.0-rc.4", + "stac_extensions": [ + "https://stac-extensions.github.io/eo/v1.0.0/schema.json" + ], + "collection": "HLSL30.v1.5", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -177.0002548, + -45.2413329 + ], + [ + -176.7533258, + -45.2410666 + ], + [ + -176.4223072, + -44.2514167 + ], + [ + -177.0002505, + -44.2528774 + ], + [ + -177.0002548, + -45.2413329 + ] + ] + ] + }, + "bbox": [ + -177.000255, + -45.241333, + -176.422307, + -44.251417 + ], + "links": [ + { + "rel": "self", + "href": "https://cmr.earthdata.nasa.gov/stac/LPCLOUD/collections/HLSL30.v1.5/items/G1994512890-LPCLOUD" + }, + { + "rel": "parent", + "href": "https://cmr.earthdata.nasa.gov/stac/LPCLOUD/collections/HLSL30.v1.5" + }, + { + "rel": "collection", + "href": "https://cmr.earthdata.nasa.gov/stac/LPCLOUD/collections/HLSL30.v1.5" + }, + { + "rel": "root", + "href": "https://cmr.earthdata.nasa.gov/stac/" + }, + { + "rel": "provider", + "href": "https://cmr.earthdata.nasa.gov/stac/LPCLOUD" + }, + { + "rel": "via", + "href": "https://cmr.earthdata.nasa.gov/search/concepts/G1994512890-LPCLOUD.json" + }, + { + "rel": "via", + "href": "https://cmr.earthdata.nasa.gov/search/concepts/G1994512890-LPCLOUD.umm_json" + } + ], + "properties": { + "datetime": "2021-01-01T21:31:13.552Z", + "start_datetime": "2021-01-01T21:31:13.552Z", + "end_datetime": "2021-01-01T21:31:13.552Z", + "eo:cloud_cover": 17 + }, + "assets": { + "B09": { + "title": "Download HLS.L30.T01GEL.2021001T213113.v1.5.B09.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T01GEL.2021001T213113.v1.5.B09.tif" + }, + "B06": { + "title": "Download HLS.L30.T01GEL.2021001T213113.v1.5.B06.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T01GEL.2021001T213113.v1.5.B06.tif" + }, + "B11": { + "title": "Download HLS.L30.T01GEL.2021001T213113.v1.5.B11.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T01GEL.2021001T213113.v1.5.B11.tif" + }, + "B05": { + "title": "Download HLS.L30.T01GEL.2021001T213113.v1.5.B05.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T01GEL.2021001T213113.v1.5.B05.tif" + }, + "SAA": { + "title": "Download HLS.L30.T01GEL.2021001T213113.v1.5.SAA.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T01GEL.2021001T213113.v1.5.SAA.tif" + }, + "VAA": { + "title": "Download HLS.L30.T01GEL.2021001T213113.v1.5.VAA.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T01GEL.2021001T213113.v1.5.VAA.tif" + }, + "B07": { + "title": "Download HLS.L30.T01GEL.2021001T213113.v1.5.B07.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T01GEL.2021001T213113.v1.5.B07.tif" + }, + "VZA": { + "title": "Download HLS.L30.T01GEL.2021001T213113.v1.5.VZA.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T01GEL.2021001T213113.v1.5.VZA.tif" + }, + "B03": { + "title": "Download HLS.L30.T01GEL.2021001T213113.v1.5.B03.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T01GEL.2021001T213113.v1.5.B03.tif" + }, + "B04": { + "title": "Download HLS.L30.T01GEL.2021001T213113.v1.5.B04.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T01GEL.2021001T213113.v1.5.B04.tif" + }, + "SZA": { + "title": "Download HLS.L30.T01GEL.2021001T213113.v1.5.SZA.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T01GEL.2021001T213113.v1.5.SZA.tif" + }, + "B10": { + "title": "Download HLS.L30.T01GEL.2021001T213113.v1.5.B10.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T01GEL.2021001T213113.v1.5.B10.tif" + }, + "B01": { + "title": "Download HLS.L30.T01GEL.2021001T213113.v1.5.B01.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T01GEL.2021001T213113.v1.5.B01.tif" + }, + "Fmask": { + "title": "Download HLS.L30.T01GEL.2021001T213113.v1.5.Fmask.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T01GEL.2021001T213113.v1.5.Fmask.tif" + }, + "B02": { + "title": "Download HLS.L30.T01GEL.2021001T213113.v1.5.B02.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T01GEL.2021001T213113.v1.5.B02.tif" + }, + "browse": { + "title": "Download HLS.L30.T01GEL.2021001T213113.v1.5.jpg", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-public/HLSL30.015/HLS.L30.T01GEL.2021001T213113.v1.5.jpg", + "type": "image/jpeg" + }, + "metadata": { + "href": "https://cmr.earthdata.nasa.gov/search/concepts/G1994512890-LPCLOUD.xml", + "type": "application/xml" + } + } + }, + { + "type": "Feature", + "id": "G1996013881-LPCLOUD", + "stac_version": "1.0.0-rc.4", + "stac_extensions": [ + "https://stac-extensions.github.io/eo/v1.0.0/schema.json" + ], + "collection": "HLSL30.v1.5", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -167.8646796, + 53.1198806 + ], + [ + -167.8111953, + 53.8385095 + ], + [ + -169.470024, + 54.1186972 + ], + [ + -169.5046204, + 53.1517855 + ], + [ + -167.8646796, + 53.1198806 + ] + ] + ] + }, + "bbox": [ + -169.50462, + 53.119881, + -167.811195, + 54.118697 + ], + "links": [ + { + "rel": "self", + "href": "https://cmr.earthdata.nasa.gov/stac/LPCLOUD/collections/HLSL30.v1.5/items/G1996013881-LPCLOUD" + }, + { + "rel": "parent", + "href": "https://cmr.earthdata.nasa.gov/stac/LPCLOUD/collections/HLSL30.v1.5" + }, + { + "rel": "collection", + "href": "https://cmr.earthdata.nasa.gov/stac/LPCLOUD/collections/HLSL30.v1.5" + }, + { + "rel": "root", + "href": "https://cmr.earthdata.nasa.gov/stac/" + }, + { + "rel": "provider", + "href": "https://cmr.earthdata.nasa.gov/stac/LPCLOUD" + }, + { + "rel": "via", + "href": "https://cmr.earthdata.nasa.gov/search/concepts/G1996013881-LPCLOUD.json" + }, + { + "rel": "via", + "href": "https://cmr.earthdata.nasa.gov/search/concepts/G1996013881-LPCLOUD.umm_json" + } + ], + "properties": { + "datetime": "2021-01-14T22:12:00.265Z", + "start_datetime": "2021-01-14T22:12:00.265Z", + "end_datetime": "2021-01-14T22:12:00.265Z", + "eo:cloud_cover": 69 + }, + "assets": { + "VZA": { + "title": "Download HLS.L30.T02UPE.2021014T221200.v1.5.VZA.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UPE.2021014T221200.v1.5.VZA.tif" + }, + "B02": { + "title": "Download HLS.L30.T02UPE.2021014T221200.v1.5.B02.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UPE.2021014T221200.v1.5.B02.tif" + }, + "B01": { + "title": "Download HLS.L30.T02UPE.2021014T221200.v1.5.B01.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UPE.2021014T221200.v1.5.B01.tif" + }, + "SAA": { + "title": "Download HLS.L30.T02UPE.2021014T221200.v1.5.SAA.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UPE.2021014T221200.v1.5.SAA.tif" + }, + "Fmask": { + "title": "Download HLS.L30.T02UPE.2021014T221200.v1.5.Fmask.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UPE.2021014T221200.v1.5.Fmask.tif" + }, + "B07": { + "title": "Download HLS.L30.T02UPE.2021014T221200.v1.5.B07.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UPE.2021014T221200.v1.5.B07.tif" + }, + "B04": { + "title": "Download HLS.L30.T02UPE.2021014T221200.v1.5.B04.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UPE.2021014T221200.v1.5.B04.tif" + }, + "B10": { + "title": "Download HLS.L30.T02UPE.2021014T221200.v1.5.B10.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UPE.2021014T221200.v1.5.B10.tif" + }, + "B05": { + "title": "Download HLS.L30.T02UPE.2021014T221200.v1.5.B05.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UPE.2021014T221200.v1.5.B05.tif" + }, + "SZA": { + "title": "Download HLS.L30.T02UPE.2021014T221200.v1.5.SZA.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UPE.2021014T221200.v1.5.SZA.tif" + }, + "B03": { + "title": "Download HLS.L30.T02UPE.2021014T221200.v1.5.B03.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UPE.2021014T221200.v1.5.B03.tif" + }, + "B11": { + "title": "Download HLS.L30.T02UPE.2021014T221200.v1.5.B11.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UPE.2021014T221200.v1.5.B11.tif" + }, + "VAA": { + "title": "Download HLS.L30.T02UPE.2021014T221200.v1.5.VAA.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UPE.2021014T221200.v1.5.VAA.tif" + }, + "B06": { + "title": "Download HLS.L30.T02UPE.2021014T221200.v1.5.B06.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UPE.2021014T221200.v1.5.B06.tif" + }, + "B09": { + "title": "Download HLS.L30.T02UPE.2021014T221200.v1.5.B09.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UPE.2021014T221200.v1.5.B09.tif" + }, + "browse": { + "title": "Download HLS.L30.T02UPE.2021014T221200.v1.5.jpg", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-public/HLSL30.015/HLS.L30.T02UPE.2021014T221200.v1.5.jpg", + "type": "image/jpeg" + }, + "metadata": { + "href": "https://cmr.earthdata.nasa.gov/search/concepts/G1996013881-LPCLOUD.xml", + "type": "application/xml" + } + } + }, + { + "type": "Feature", + "id": "G1996014088-LPCLOUD", + "stac_version": "1.0.0-rc.4", + "stac_extensions": [ + "https://stac-extensions.github.io/eo/v1.0.0/schema.json" + ], + "collection": "HLSL30.v1.5", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -169.3906805, + 52.2825976 + ], + [ + -169.355952, + 53.238289 + ], + [ + -170.2256392, + 53.2471116 + ], + [ + -170.5607883, + 52.47777 + ], + [ + -169.3906805, + 52.2825976 + ] + ] + ] + }, + "bbox": [ + -170.560788, + 52.282598, + -169.355952, + 53.247112 + ], + "links": [ + { + "rel": "self", + "href": "https://cmr.earthdata.nasa.gov/stac/LPCLOUD/collections/HLSL30.v1.5/items/G1996014088-LPCLOUD" + }, + { + "rel": "parent", + "href": "https://cmr.earthdata.nasa.gov/stac/LPCLOUD/collections/HLSL30.v1.5" + }, + { + "rel": "collection", + "href": "https://cmr.earthdata.nasa.gov/stac/LPCLOUD/collections/HLSL30.v1.5" + }, + { + "rel": "root", + "href": "https://cmr.earthdata.nasa.gov/stac/" + }, + { + "rel": "provider", + "href": "https://cmr.earthdata.nasa.gov/stac/LPCLOUD" + }, + { + "rel": "via", + "href": "https://cmr.earthdata.nasa.gov/search/concepts/G1996014088-LPCLOUD.json" + }, + { + "rel": "via", + "href": "https://cmr.earthdata.nasa.gov/search/concepts/G1996014088-LPCLOUD.umm_json" + } + ], + "properties": { + "datetime": "2021-01-14T22:12:00.265Z", + "start_datetime": "2021-01-14T22:12:00.265Z", + "end_datetime": "2021-01-14T22:12:00.265Z", + "eo:cloud_cover": 58 + }, + "assets": { + "VAA": { + "title": "Download HLS.L30.T02UND.2021014T221200.v1.5.VAA.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UND.2021014T221200.v1.5.VAA.tif" + }, + "B02": { + "title": "Download HLS.L30.T02UND.2021014T221200.v1.5.B02.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UND.2021014T221200.v1.5.B02.tif" + }, + "SZA": { + "title": "Download HLS.L30.T02UND.2021014T221200.v1.5.SZA.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UND.2021014T221200.v1.5.SZA.tif" + }, + "B10": { + "title": "Download HLS.L30.T02UND.2021014T221200.v1.5.B10.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UND.2021014T221200.v1.5.B10.tif" + }, + "B04": { + "title": "Download HLS.L30.T02UND.2021014T221200.v1.5.B04.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UND.2021014T221200.v1.5.B04.tif" + }, + "B05": { + "title": "Download HLS.L30.T02UND.2021014T221200.v1.5.B05.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UND.2021014T221200.v1.5.B05.tif" + }, + "VZA": { + "title": "Download HLS.L30.T02UND.2021014T221200.v1.5.VZA.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UND.2021014T221200.v1.5.VZA.tif" + }, + "B11": { + "title": "Download HLS.L30.T02UND.2021014T221200.v1.5.B11.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UND.2021014T221200.v1.5.B11.tif" + }, + "B06": { + "title": "Download HLS.L30.T02UND.2021014T221200.v1.5.B06.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UND.2021014T221200.v1.5.B06.tif" + }, + "B01": { + "title": "Download HLS.L30.T02UND.2021014T221200.v1.5.B01.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UND.2021014T221200.v1.5.B01.tif" + }, + "Fmask": { + "title": "Download HLS.L30.T02UND.2021014T221200.v1.5.Fmask.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UND.2021014T221200.v1.5.Fmask.tif" + }, + "SAA": { + "title": "Download HLS.L30.T02UND.2021014T221200.v1.5.SAA.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UND.2021014T221200.v1.5.SAA.tif" + }, + "B03": { + "title": "Download HLS.L30.T02UND.2021014T221200.v1.5.B03.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UND.2021014T221200.v1.5.B03.tif" + }, + "B07": { + "title": "Download HLS.L30.T02UND.2021014T221200.v1.5.B07.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UND.2021014T221200.v1.5.B07.tif" + }, + "B09": { + "title": "Download HLS.L30.T02UND.2021014T221200.v1.5.B09.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UND.2021014T221200.v1.5.B09.tif" + }, + "browse": { + "title": "Download HLS.L30.T02UND.2021014T221200.v1.5.jpg", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-public/HLSL30.015/HLS.L30.T02UND.2021014T221200.v1.5.jpg", + "type": "image/jpeg" + }, + "metadata": { + "href": "https://cmr.earthdata.nasa.gov/search/concepts/G1996014088-LPCLOUD.xml", + "type": "application/xml" + } + } + }, + { + "type": "Feature", + "id": "G1996014249-LPCLOUD", + "stac_version": "1.0.0-rc.4", + "stac_extensions": [ + "https://stac-extensions.github.io/eo/v1.0.0/schema.json" + ], + "collection": "HLSL30.v1.5", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -167.9890892, + 53.123645 + ], + [ + -167.3647298, + 53.1376912 + ], + [ + -167.0709527, + 53.7047108 + ], + [ + -168.0427467, + 53.8789932 + ], + [ + -167.9890892, + 53.123645 + ] + ] + ] + }, + "bbox": [ + -168.042747, + 53.123645, + -167.070953, + 53.878993 + ], + "links": [ + { + "rel": "self", + "href": "https://cmr.earthdata.nasa.gov/stac/LPCLOUD/collections/HLSL30.v1.5/items/G1996014249-LPCLOUD" + }, + { + "rel": "parent", + "href": "https://cmr.earthdata.nasa.gov/stac/LPCLOUD/collections/HLSL30.v1.5" + }, + { + "rel": "collection", + "href": "https://cmr.earthdata.nasa.gov/stac/LPCLOUD/collections/HLSL30.v1.5" + }, + { + "rel": "root", + "href": "https://cmr.earthdata.nasa.gov/stac/" + }, + { + "rel": "provider", + "href": "https://cmr.earthdata.nasa.gov/stac/LPCLOUD" + }, + { + "rel": "via", + "href": "https://cmr.earthdata.nasa.gov/search/concepts/G1996014249-LPCLOUD.json" + }, + { + "rel": "via", + "href": "https://cmr.earthdata.nasa.gov/search/concepts/G1996014249-LPCLOUD.umm_json" + } + ], + "properties": { + "datetime": "2021-01-14T22:12:00.265Z", + "start_datetime": "2021-01-14T22:12:00.265Z", + "end_datetime": "2021-01-14T22:12:00.265Z", + "eo:cloud_cover": 58 + }, + "assets": { + "B07": { + "title": "Download HLS.L30.T03UUV.2021014T221200.v1.5.B07.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T03UUV.2021014T221200.v1.5.B07.tif" + }, + "B09": { + "title": "Download HLS.L30.T03UUV.2021014T221200.v1.5.B09.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T03UUV.2021014T221200.v1.5.B09.tif" + }, + "B06": { + "title": "Download HLS.L30.T03UUV.2021014T221200.v1.5.B06.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T03UUV.2021014T221200.v1.5.B06.tif" + }, + "B01": { + "title": "Download HLS.L30.T03UUV.2021014T221200.v1.5.B01.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T03UUV.2021014T221200.v1.5.B01.tif" + }, + "B05": { + "title": "Download HLS.L30.T03UUV.2021014T221200.v1.5.B05.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T03UUV.2021014T221200.v1.5.B05.tif" + }, + "B02": { + "title": "Download HLS.L30.T03UUV.2021014T221200.v1.5.B02.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T03UUV.2021014T221200.v1.5.B02.tif" + }, + "B03": { + "title": "Download HLS.L30.T03UUV.2021014T221200.v1.5.B03.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T03UUV.2021014T221200.v1.5.B03.tif" + }, + "B10": { + "title": "Download HLS.L30.T03UUV.2021014T221200.v1.5.B10.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T03UUV.2021014T221200.v1.5.B10.tif" + }, + "VAA": { + "title": "Download HLS.L30.T03UUV.2021014T221200.v1.5.VAA.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T03UUV.2021014T221200.v1.5.VAA.tif" + }, + "SAA": { + "title": "Download HLS.L30.T03UUV.2021014T221200.v1.5.SAA.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T03UUV.2021014T221200.v1.5.SAA.tif" + }, + "VZA": { + "title": "Download HLS.L30.T03UUV.2021014T221200.v1.5.VZA.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T03UUV.2021014T221200.v1.5.VZA.tif" + }, + "Fmask": { + "title": "Download HLS.L30.T03UUV.2021014T221200.v1.5.Fmask.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T03UUV.2021014T221200.v1.5.Fmask.tif" + }, + "B04": { + "title": "Download HLS.L30.T03UUV.2021014T221200.v1.5.B04.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T03UUV.2021014T221200.v1.5.B04.tif" + }, + "B11": { + "title": "Download HLS.L30.T03UUV.2021014T221200.v1.5.B11.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T03UUV.2021014T221200.v1.5.B11.tif" + }, + "SZA": { + "title": "Download HLS.L30.T03UUV.2021014T221200.v1.5.SZA.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T03UUV.2021014T221200.v1.5.SZA.tif" + }, + "browse": { + "title": "Download HLS.L30.T03UUV.2021014T221200.v1.5.jpg", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-public/HLSL30.015/HLS.L30.T03UUV.2021014T221200.v1.5.jpg", + "type": "image/jpeg" + }, + "metadata": { + "href": "https://cmr.earthdata.nasa.gov/search/concepts/G1996014249-LPCLOUD.xml", + "type": "application/xml" + } + } + }, + { + "type": "Feature", + "id": "G1996014444-LPCLOUD", + "stac_version": "1.0.0-rc.4", + "stac_extensions": [ + "https://stac-extensions.github.io/eo/v1.0.0/schema.json" + ], + "collection": "HLSL30.v1.5", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -167.3814687, + 53.1061588 + ], + [ + -167.0710293, + 53.7048111 + ], + [ + -167.9589168, + 53.8644684 + ], + [ + -168.0115077, + 53.12366 + ], + [ + -167.3814687, + 53.1061588 + ] + ] + ] + }, + "bbox": [ + -168.011508, + 53.106159, + -167.071029, + 53.864468 + ], + "links": [ + { + "rel": "self", + "href": "https://cmr.earthdata.nasa.gov/stac/LPCLOUD/collections/HLSL30.v1.5/items/G1996014444-LPCLOUD" + }, + { + "rel": "parent", + "href": "https://cmr.earthdata.nasa.gov/stac/LPCLOUD/collections/HLSL30.v1.5" + }, + { + "rel": "collection", + "href": "https://cmr.earthdata.nasa.gov/stac/LPCLOUD/collections/HLSL30.v1.5" + }, + { + "rel": "root", + "href": "https://cmr.earthdata.nasa.gov/stac/" + }, + { + "rel": "provider", + "href": "https://cmr.earthdata.nasa.gov/stac/LPCLOUD" + }, + { + "rel": "via", + "href": "https://cmr.earthdata.nasa.gov/search/concepts/G1996014444-LPCLOUD.json" + }, + { + "rel": "via", + "href": "https://cmr.earthdata.nasa.gov/search/concepts/G1996014444-LPCLOUD.umm_json" + } + ], + "properties": { + "datetime": "2021-01-14T22:12:00.265Z", + "start_datetime": "2021-01-14T22:12:00.265Z", + "end_datetime": "2021-01-14T22:12:00.265Z", + "eo:cloud_cover": 55 + }, + "assets": { + "B07": { + "title": "Download HLS.L30.T02UQE.2021014T221200.v1.5.B07.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UQE.2021014T221200.v1.5.B07.tif" + }, + "B03": { + "title": "Download HLS.L30.T02UQE.2021014T221200.v1.5.B03.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UQE.2021014T221200.v1.5.B03.tif" + }, + "VAA": { + "title": "Download HLS.L30.T02UQE.2021014T221200.v1.5.VAA.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UQE.2021014T221200.v1.5.VAA.tif" + }, + "VZA": { + "title": "Download HLS.L30.T02UQE.2021014T221200.v1.5.VZA.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UQE.2021014T221200.v1.5.VZA.tif" + }, + "B02": { + "title": "Download HLS.L30.T02UQE.2021014T221200.v1.5.B02.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UQE.2021014T221200.v1.5.B02.tif" + }, + "B06": { + "title": "Download HLS.L30.T02UQE.2021014T221200.v1.5.B06.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UQE.2021014T221200.v1.5.B06.tif" + }, + "SZA": { + "title": "Download HLS.L30.T02UQE.2021014T221200.v1.5.SZA.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UQE.2021014T221200.v1.5.SZA.tif" + }, + "B05": { + "title": "Download HLS.L30.T02UQE.2021014T221200.v1.5.B05.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UQE.2021014T221200.v1.5.B05.tif" + }, + "SAA": { + "title": "Download HLS.L30.T02UQE.2021014T221200.v1.5.SAA.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UQE.2021014T221200.v1.5.SAA.tif" + }, + "B11": { + "title": "Download HLS.L30.T02UQE.2021014T221200.v1.5.B11.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UQE.2021014T221200.v1.5.B11.tif" + }, + "B01": { + "title": "Download HLS.L30.T02UQE.2021014T221200.v1.5.B01.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UQE.2021014T221200.v1.5.B01.tif" + }, + "B09": { + "title": "Download HLS.L30.T02UQE.2021014T221200.v1.5.B09.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UQE.2021014T221200.v1.5.B09.tif" + }, + "B04": { + "title": "Download HLS.L30.T02UQE.2021014T221200.v1.5.B04.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UQE.2021014T221200.v1.5.B04.tif" + }, + "Fmask": { + "title": "Download HLS.L30.T02UQE.2021014T221200.v1.5.Fmask.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UQE.2021014T221200.v1.5.Fmask.tif" + }, + "B10": { + "title": "Download HLS.L30.T02UQE.2021014T221200.v1.5.B10.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UQE.2021014T221200.v1.5.B10.tif" + }, + "browse": { + "title": "Download HLS.L30.T02UQE.2021014T221200.v1.5.jpg", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-public/HLSL30.015/HLS.L30.T02UQE.2021014T221200.v1.5.jpg", + "type": "image/jpeg" + }, + "metadata": { + "href": "https://cmr.earthdata.nasa.gov/search/concepts/G1996014444-LPCLOUD.xml", + "type": "application/xml" + } + } + }, + { + "type": "Feature", + "id": "G1996014471-LPCLOUD", + "stac_version": "1.0.0-rc.4", + "stac_extensions": [ + "https://stac-extensions.github.io/eo/v1.0.0/schema.json" + ], + "collection": "HLSL30.v1.5", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -167.9283215, + 52.222566 + ], + [ + -167.8582301, + 53.2082014 + ], + [ + -169.5015396, + 53.2402084 + ], + [ + -169.5332578, + 52.3068431 + ], + [ + -169.1958954, + 52.2487605 + ], + [ + -167.9283215, + 52.222566 + ] + ] + ] + }, + "bbox": [ + -169.533258, + 52.222566, + -167.85823, + 53.240208 + ], + "links": [ + { + "rel": "self", + "href": "https://cmr.earthdata.nasa.gov/stac/LPCLOUD/collections/HLSL30.v1.5/items/G1996014471-LPCLOUD" + }, + { + "rel": "parent", + "href": "https://cmr.earthdata.nasa.gov/stac/LPCLOUD/collections/HLSL30.v1.5" + }, + { + "rel": "collection", + "href": "https://cmr.earthdata.nasa.gov/stac/LPCLOUD/collections/HLSL30.v1.5" + }, + { + "rel": "root", + "href": "https://cmr.earthdata.nasa.gov/stac/" + }, + { + "rel": "provider", + "href": "https://cmr.earthdata.nasa.gov/stac/LPCLOUD" + }, + { + "rel": "via", + "href": "https://cmr.earthdata.nasa.gov/search/concepts/G1996014471-LPCLOUD.json" + }, + { + "rel": "via", + "href": "https://cmr.earthdata.nasa.gov/search/concepts/G1996014471-LPCLOUD.umm_json" + } + ], + "properties": { + "datetime": "2021-01-14T22:12:00.265Z", + "start_datetime": "2021-01-14T22:12:00.265Z", + "end_datetime": "2021-01-14T22:12:00.265Z", + "eo:cloud_cover": 37 + }, + "assets": { + "VAA": { + "title": "Download HLS.L30.T02UPD.2021014T221200.v1.5.VAA.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UPD.2021014T221200.v1.5.VAA.tif" + }, + "B04": { + "title": "Download HLS.L30.T02UPD.2021014T221200.v1.5.B04.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UPD.2021014T221200.v1.5.B04.tif" + }, + "VZA": { + "title": "Download HLS.L30.T02UPD.2021014T221200.v1.5.VZA.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UPD.2021014T221200.v1.5.VZA.tif" + }, + "B09": { + "title": "Download HLS.L30.T02UPD.2021014T221200.v1.5.B09.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UPD.2021014T221200.v1.5.B09.tif" + }, + "SZA": { + "title": "Download HLS.L30.T02UPD.2021014T221200.v1.5.SZA.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UPD.2021014T221200.v1.5.SZA.tif" + }, + "B02": { + "title": "Download HLS.L30.T02UPD.2021014T221200.v1.5.B02.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UPD.2021014T221200.v1.5.B02.tif" + }, + "SAA": { + "title": "Download HLS.L30.T02UPD.2021014T221200.v1.5.SAA.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UPD.2021014T221200.v1.5.SAA.tif" + }, + "B01": { + "title": "Download HLS.L30.T02UPD.2021014T221200.v1.5.B01.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UPD.2021014T221200.v1.5.B01.tif" + }, + "B10": { + "title": "Download HLS.L30.T02UPD.2021014T221200.v1.5.B10.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UPD.2021014T221200.v1.5.B10.tif" + }, + "B03": { + "title": "Download HLS.L30.T02UPD.2021014T221200.v1.5.B03.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UPD.2021014T221200.v1.5.B03.tif" + }, + "B11": { + "title": "Download HLS.L30.T02UPD.2021014T221200.v1.5.B11.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UPD.2021014T221200.v1.5.B11.tif" + }, + "B05": { + "title": "Download HLS.L30.T02UPD.2021014T221200.v1.5.B05.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UPD.2021014T221200.v1.5.B05.tif" + }, + "B06": { + "title": "Download HLS.L30.T02UPD.2021014T221200.v1.5.B06.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UPD.2021014T221200.v1.5.B06.tif" + }, + "B07": { + "title": "Download HLS.L30.T02UPD.2021014T221200.v1.5.B07.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UPD.2021014T221200.v1.5.B07.tif" + }, + "Fmask": { + "title": "Download HLS.L30.T02UPD.2021014T221200.v1.5.Fmask.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T02UPD.2021014T221200.v1.5.Fmask.tif" + }, + "browse": { + "title": "Download HLS.L30.T02UPD.2021014T221200.v1.5.jpg", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-public/HLSL30.015/HLS.L30.T02UPD.2021014T221200.v1.5.jpg", + "type": "image/jpeg" + }, + "metadata": { + "href": "https://cmr.earthdata.nasa.gov/search/concepts/G1996014471-LPCLOUD.xml", + "type": "application/xml" + } + } + }, + { + "type": "Feature", + "id": "G1994877008-LPCLOUD", + "stac_version": "1.0.0-rc.4", + "stac_extensions": [ + "https://stac-extensions.github.io/eo/v1.0.0/schema.json" + ], + "collection": "HLSL30.v1.5", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -178.2128449, + 27.9318071 + ], + [ + -177.9168097, + 27.9340963 + ], + [ + -177.9253908, + 28.9251026 + ], + [ + -177.9690904, + 28.9247951 + ], + [ + -178.2128449, + 27.9318071 + ] + ] + ] + }, + "bbox": [ + -178.212845, + 27.931807, + -177.91681, + 28.925103 + ], + "links": [ + { + "rel": "self", + "href": "https://cmr.earthdata.nasa.gov/stac/LPCLOUD/collections/HLSL30.v1.5/items/G1994877008-LPCLOUD" + }, + { + "rel": "parent", + "href": "https://cmr.earthdata.nasa.gov/stac/LPCLOUD/collections/HLSL30.v1.5" + }, + { + "rel": "collection", + "href": "https://cmr.earthdata.nasa.gov/stac/LPCLOUD/collections/HLSL30.v1.5" + }, + { + "rel": "root", + "href": "https://cmr.earthdata.nasa.gov/stac/" + }, + { + "rel": "provider", + "href": "https://cmr.earthdata.nasa.gov/stac/LPCLOUD" + }, + { + "rel": "via", + "href": "https://cmr.earthdata.nasa.gov/search/concepts/G1994877008-LPCLOUD.json" + }, + { + "rel": "via", + "href": "https://cmr.earthdata.nasa.gov/search/concepts/G1994877008-LPCLOUD.umm_json" + } + ], + "properties": { + "datetime": "2021-01-14T22:18:46.319Z", + "start_datetime": "2021-01-14T22:18:46.319Z", + "end_datetime": "2021-01-14T22:19:10.219Z", + "eo:cloud_cover": 41 + }, + "assets": { + "B07": { + "title": "Download HLS.L30.T01RCM.2021014T221846.v1.5.B07.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T01RCM.2021014T221846.v1.5.B07.tif" + }, + "B04": { + "title": "Download HLS.L30.T01RCM.2021014T221846.v1.5.B04.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T01RCM.2021014T221846.v1.5.B04.tif" + }, + "B02": { + "title": "Download HLS.L30.T01RCM.2021014T221846.v1.5.B02.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T01RCM.2021014T221846.v1.5.B02.tif" + }, + "B10": { + "title": "Download HLS.L30.T01RCM.2021014T221846.v1.5.B10.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T01RCM.2021014T221846.v1.5.B10.tif" + }, + "SZA": { + "title": "Download HLS.L30.T01RCM.2021014T221846.v1.5.SZA.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T01RCM.2021014T221846.v1.5.SZA.tif" + }, + "VZA": { + "title": "Download HLS.L30.T01RCM.2021014T221846.v1.5.VZA.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T01RCM.2021014T221846.v1.5.VZA.tif" + }, + "Fmask": { + "title": "Download HLS.L30.T01RCM.2021014T221846.v1.5.Fmask.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T01RCM.2021014T221846.v1.5.Fmask.tif" + }, + "SAA": { + "title": "Download HLS.L30.T01RCM.2021014T221846.v1.5.SAA.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T01RCM.2021014T221846.v1.5.SAA.tif" + }, + "B05": { + "title": "Download HLS.L30.T01RCM.2021014T221846.v1.5.B05.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T01RCM.2021014T221846.v1.5.B05.tif" + }, + "B01": { + "title": "Download HLS.L30.T01RCM.2021014T221846.v1.5.B01.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T01RCM.2021014T221846.v1.5.B01.tif" + }, + "VAA": { + "title": "Download HLS.L30.T01RCM.2021014T221846.v1.5.VAA.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T01RCM.2021014T221846.v1.5.VAA.tif" + }, + "B09": { + "title": "Download HLS.L30.T01RCM.2021014T221846.v1.5.B09.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T01RCM.2021014T221846.v1.5.B09.tif" + }, + "B11": { + "title": "Download HLS.L30.T01RCM.2021014T221846.v1.5.B11.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T01RCM.2021014T221846.v1.5.B11.tif" + }, + "B03": { + "title": "Download HLS.L30.T01RCM.2021014T221846.v1.5.B03.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T01RCM.2021014T221846.v1.5.B03.tif" + }, + "B06": { + "title": "Download HLS.L30.T01RCM.2021014T221846.v1.5.B06.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T01RCM.2021014T221846.v1.5.B06.tif" + }, + "browse": { + "title": "Download HLS.L30.T01RCM.2021014T221846.v1.5.jpg", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-public/HLSL30.015/HLS.L30.T01RCM.2021014T221846.v1.5.jpg", + "type": "image/jpeg" + }, + "metadata": { + "href": "https://cmr.earthdata.nasa.gov/search/concepts/G1994877008-LPCLOUD.xml", + "type": "application/xml" + } + } + }, + { + "type": "Feature", + "id": "G1994877369-LPCLOUD", + "stac_version": "1.0.0-rc.4", + "stac_extensions": [ + "https://stac-extensions.github.io/eo/v1.0.0/schema.json" + ], + "collection": "HLSL30.v1.5", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -178.0168131, + 27.9333942 + ], + [ + -176.9007924, + 27.9371126 + ], + [ + -176.8998637, + 28.9282455 + ], + [ + -177.9690904, + 28.9247951 + ], + [ + -178.0241477, + 28.7015494 + ], + [ + -178.0168131, + 27.9333942 + ] + ] + ] + }, + "bbox": [ + -178.024148, + 27.933394, + -176.899864, + 28.928281 + ], + "links": [ + { + "rel": "self", + "href": "https://cmr.earthdata.nasa.gov/stac/LPCLOUD/collections/HLSL30.v1.5/items/G1994877369-LPCLOUD" + }, + { + "rel": "parent", + "href": "https://cmr.earthdata.nasa.gov/stac/LPCLOUD/collections/HLSL30.v1.5" + }, + { + "rel": "collection", + "href": "https://cmr.earthdata.nasa.gov/stac/LPCLOUD/collections/HLSL30.v1.5" + }, + { + "rel": "root", + "href": "https://cmr.earthdata.nasa.gov/stac/" + }, + { + "rel": "provider", + "href": "https://cmr.earthdata.nasa.gov/stac/LPCLOUD" + }, + { + "rel": "via", + "href": "https://cmr.earthdata.nasa.gov/search/concepts/G1994877369-LPCLOUD.json" + }, + { + "rel": "via", + "href": "https://cmr.earthdata.nasa.gov/search/concepts/G1994877369-LPCLOUD.umm_json" + } + ], + "properties": { + "datetime": "2021-01-14T22:18:46.319Z", + "start_datetime": "2021-01-14T22:18:46.319Z", + "end_datetime": "2021-01-14T22:19:10.219Z", + "eo:cloud_cover": 43 + }, + "assets": { + "B04": { + "title": "Download HLS.L30.T01RDM.2021014T221846.v1.5.B04.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T01RDM.2021014T221846.v1.5.B04.tif" + }, + "B09": { + "title": "Download HLS.L30.T01RDM.2021014T221846.v1.5.B09.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T01RDM.2021014T221846.v1.5.B09.tif" + }, + "SZA": { + "title": "Download HLS.L30.T01RDM.2021014T221846.v1.5.SZA.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T01RDM.2021014T221846.v1.5.SZA.tif" + }, + "B05": { + "title": "Download HLS.L30.T01RDM.2021014T221846.v1.5.B05.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T01RDM.2021014T221846.v1.5.B05.tif" + }, + "Fmask": { + "title": "Download HLS.L30.T01RDM.2021014T221846.v1.5.Fmask.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T01RDM.2021014T221846.v1.5.Fmask.tif" + }, + "B11": { + "title": "Download HLS.L30.T01RDM.2021014T221846.v1.5.B11.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T01RDM.2021014T221846.v1.5.B11.tif" + }, + "VZA": { + "title": "Download HLS.L30.T01RDM.2021014T221846.v1.5.VZA.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T01RDM.2021014T221846.v1.5.VZA.tif" + }, + "B03": { + "title": "Download HLS.L30.T01RDM.2021014T221846.v1.5.B03.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T01RDM.2021014T221846.v1.5.B03.tif" + }, + "B10": { + "title": "Download HLS.L30.T01RDM.2021014T221846.v1.5.B10.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T01RDM.2021014T221846.v1.5.B10.tif" + }, + "B02": { + "title": "Download HLS.L30.T01RDM.2021014T221846.v1.5.B02.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T01RDM.2021014T221846.v1.5.B02.tif" + }, + "B07": { + "title": "Download HLS.L30.T01RDM.2021014T221846.v1.5.B07.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T01RDM.2021014T221846.v1.5.B07.tif" + }, + "B01": { + "title": "Download HLS.L30.T01RDM.2021014T221846.v1.5.B01.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T01RDM.2021014T221846.v1.5.B01.tif" + }, + "VAA": { + "title": "Download HLS.L30.T01RDM.2021014T221846.v1.5.VAA.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T01RDM.2021014T221846.v1.5.VAA.tif" + }, + "B06": { + "title": "Download HLS.L30.T01RDM.2021014T221846.v1.5.B06.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T01RDM.2021014T221846.v1.5.B06.tif" + }, + "SAA": { + "title": "Download HLS.L30.T01RDM.2021014T221846.v1.5.SAA.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T01RDM.2021014T221846.v1.5.SAA.tif" + }, + "browse": { + "title": "Download HLS.L30.T01RDM.2021014T221846.v1.5.jpg", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-public/HLSL30.015/HLS.L30.T01RDM.2021014T221846.v1.5.jpg", + "type": "image/jpeg" + }, + "metadata": { + "href": "https://cmr.earthdata.nasa.gov/search/concepts/G1994877369-LPCLOUD.xml", + "type": "application/xml" + } + } + }, + { + "type": "Feature", + "id": "G1994873598-LPCLOUD", + "stac_version": "1.0.0-rc.4", + "stac_extensions": [ + "https://stac-extensions.github.io/eo/v1.0.0/schema.json" + ], + "collection": "HLSL30.v1.5", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 175.289865, + -1.8969072 + ], + [ + 175.2901904, + -1.530116 + ], + [ + 175.2899208, + -1.5301158 + ], + [ + 175.2124833, + -1.8968285 + ], + [ + 175.289865, + -1.8969072 + ] + ] + ] + }, + "bbox": [ + 175.212483, + -1.896907, + 175.29019, + -1.530116 + ], + "links": [ + { + "rel": "self", + "href": "https://cmr.earthdata.nasa.gov/stac/LPCLOUD/collections/HLSL30.v1.5/items/G1994873598-LPCLOUD" + }, + { + "rel": "parent", + "href": "https://cmr.earthdata.nasa.gov/stac/LPCLOUD/collections/HLSL30.v1.5" + }, + { + "rel": "collection", + "href": "https://cmr.earthdata.nasa.gov/stac/LPCLOUD/collections/HLSL30.v1.5" + }, + { + "rel": "root", + "href": "https://cmr.earthdata.nasa.gov/stac/" + }, + { + "rel": "provider", + "href": "https://cmr.earthdata.nasa.gov/stac/LPCLOUD" + }, + { + "rel": "via", + "href": "https://cmr.earthdata.nasa.gov/search/concepts/G1994873598-LPCLOUD.json" + }, + { + "rel": "via", + "href": "https://cmr.earthdata.nasa.gov/search/concepts/G1994873598-LPCLOUD.umm_json" + } + ], + "properties": { + "datetime": "2021-01-14T22:27:08.323Z", + "start_datetime": "2021-01-14T22:27:08.323Z", + "end_datetime": "2021-01-14T22:27:08.323Z", + "eo:cloud_cover": 35 + }, + "assets": { + "B07": { + "title": "Download HLS.L30.T60MTD.2021014T222708.v1.5.B07.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T60MTD.2021014T222708.v1.5.B07.tif" + }, + "SZA": { + "title": "Download HLS.L30.T60MTD.2021014T222708.v1.5.SZA.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T60MTD.2021014T222708.v1.5.SZA.tif" + }, + "B10": { + "title": "Download HLS.L30.T60MTD.2021014T222708.v1.5.B10.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T60MTD.2021014T222708.v1.5.B10.tif" + }, + "VZA": { + "title": "Download HLS.L30.T60MTD.2021014T222708.v1.5.VZA.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T60MTD.2021014T222708.v1.5.VZA.tif" + }, + "VAA": { + "title": "Download HLS.L30.T60MTD.2021014T222708.v1.5.VAA.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T60MTD.2021014T222708.v1.5.VAA.tif" + }, + "B09": { + "title": "Download HLS.L30.T60MTD.2021014T222708.v1.5.B09.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T60MTD.2021014T222708.v1.5.B09.tif" + }, + "B01": { + "title": "Download HLS.L30.T60MTD.2021014T222708.v1.5.B01.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T60MTD.2021014T222708.v1.5.B01.tif" + }, + "Fmask": { + "title": "Download HLS.L30.T60MTD.2021014T222708.v1.5.Fmask.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T60MTD.2021014T222708.v1.5.Fmask.tif" + }, + "B03": { + "title": "Download HLS.L30.T60MTD.2021014T222708.v1.5.B03.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T60MTD.2021014T222708.v1.5.B03.tif" + }, + "SAA": { + "title": "Download HLS.L30.T60MTD.2021014T222708.v1.5.SAA.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T60MTD.2021014T222708.v1.5.SAA.tif" + }, + "B02": { + "title": "Download HLS.L30.T60MTD.2021014T222708.v1.5.B02.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T60MTD.2021014T222708.v1.5.B02.tif" + }, + "B11": { + "title": "Download HLS.L30.T60MTD.2021014T222708.v1.5.B11.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T60MTD.2021014T222708.v1.5.B11.tif" + }, + "B04": { + "title": "Download HLS.L30.T60MTD.2021014T222708.v1.5.B04.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T60MTD.2021014T222708.v1.5.B04.tif" + }, + "B05": { + "title": "Download HLS.L30.T60MTD.2021014T222708.v1.5.B05.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T60MTD.2021014T222708.v1.5.B05.tif" + }, + "B06": { + "title": "Download HLS.L30.T60MTD.2021014T222708.v1.5.B06.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T60MTD.2021014T222708.v1.5.B06.tif" + }, + "browse": { + "title": "Download HLS.L30.T60MTD.2021014T222708.v1.5.jpg", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-public/HLSL30.015/HLS.L30.T60MTD.2021014T222708.v1.5.jpg", + "type": "image/jpeg" + }, + "metadata": { + "href": "https://cmr.earthdata.nasa.gov/search/concepts/G1994873598-LPCLOUD.xml", + "type": "application/xml" + } + } + }, + { + "type": "Feature", + "id": "G1994873826-LPCLOUD", + "stac_version": "1.0.0-rc.4", + "stac_extensions": [ + "https://stac-extensions.github.io/eo/v1.0.0/schema.json" + ], + "collection": "HLSL30.v1.5", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 176.1889808, + -1.8975663 + ], + [ + 176.1893223, + -0.9042776 + ], + [ + 175.4231906, + -0.904024 + ], + [ + 175.2124833, + -1.8968285 + ], + [ + 176.1889808, + -1.8975663 + ] + ] + ] + }, + "bbox": [ + 175.212483, + -1.897566, + 176.189322, + -0.904024 + ], + "links": [ + { + "rel": "self", + "href": "https://cmr.earthdata.nasa.gov/stac/LPCLOUD/collections/HLSL30.v1.5/items/G1994873826-LPCLOUD" + }, + { + "rel": "parent", + "href": "https://cmr.earthdata.nasa.gov/stac/LPCLOUD/collections/HLSL30.v1.5" + }, + { + "rel": "collection", + "href": "https://cmr.earthdata.nasa.gov/stac/LPCLOUD/collections/HLSL30.v1.5" + }, + { + "rel": "root", + "href": "https://cmr.earthdata.nasa.gov/stac/" + }, + { + "rel": "provider", + "href": "https://cmr.earthdata.nasa.gov/stac/LPCLOUD" + }, + { + "rel": "via", + "href": "https://cmr.earthdata.nasa.gov/search/concepts/G1994873826-LPCLOUD.json" + }, + { + "rel": "via", + "href": "https://cmr.earthdata.nasa.gov/search/concepts/G1994873826-LPCLOUD.umm_json" + } + ], + "properties": { + "datetime": "2021-01-14T22:27:08.323Z", + "start_datetime": "2021-01-14T22:27:08.323Z", + "end_datetime": "2021-01-14T22:27:08.323Z", + "eo:cloud_cover": 16 + }, + "assets": { + "B03": { + "title": "Download HLS.L30.T60MUD.2021014T222708.v1.5.B03.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T60MUD.2021014T222708.v1.5.B03.tif" + }, + "B07": { + "title": "Download HLS.L30.T60MUD.2021014T222708.v1.5.B07.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T60MUD.2021014T222708.v1.5.B07.tif" + }, + "B09": { + "title": "Download HLS.L30.T60MUD.2021014T222708.v1.5.B09.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T60MUD.2021014T222708.v1.5.B09.tif" + }, + "B06": { + "title": "Download HLS.L30.T60MUD.2021014T222708.v1.5.B06.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T60MUD.2021014T222708.v1.5.B06.tif" + }, + "Fmask": { + "title": "Download HLS.L30.T60MUD.2021014T222708.v1.5.Fmask.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T60MUD.2021014T222708.v1.5.Fmask.tif" + }, + "VZA": { + "title": "Download HLS.L30.T60MUD.2021014T222708.v1.5.VZA.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T60MUD.2021014T222708.v1.5.VZA.tif" + }, + "VAA": { + "title": "Download HLS.L30.T60MUD.2021014T222708.v1.5.VAA.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T60MUD.2021014T222708.v1.5.VAA.tif" + }, + "B10": { + "title": "Download HLS.L30.T60MUD.2021014T222708.v1.5.B10.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T60MUD.2021014T222708.v1.5.B10.tif" + }, + "B02": { + "title": "Download HLS.L30.T60MUD.2021014T222708.v1.5.B02.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T60MUD.2021014T222708.v1.5.B02.tif" + }, + "SZA": { + "title": "Download HLS.L30.T60MUD.2021014T222708.v1.5.SZA.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T60MUD.2021014T222708.v1.5.SZA.tif" + }, + "B04": { + "title": "Download HLS.L30.T60MUD.2021014T222708.v1.5.B04.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T60MUD.2021014T222708.v1.5.B04.tif" + }, + "B01": { + "title": "Download HLS.L30.T60MUD.2021014T222708.v1.5.B01.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T60MUD.2021014T222708.v1.5.B01.tif" + }, + "B05": { + "title": "Download HLS.L30.T60MUD.2021014T222708.v1.5.B05.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T60MUD.2021014T222708.v1.5.B05.tif" + }, + "SAA": { + "title": "Download HLS.L30.T60MUD.2021014T222708.v1.5.SAA.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T60MUD.2021014T222708.v1.5.SAA.tif" + }, + "B11": { + "title": "Download HLS.L30.T60MUD.2021014T222708.v1.5.B11.tif", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-protected/HLSL30.015/HLS.L30.T60MUD.2021014T222708.v1.5.B11.tif" + }, + "browse": { + "title": "Download HLS.L30.T60MUD.2021014T222708.v1.5.jpg", + "href": "https://lpdaac.earthdata.nasa.gov/lp-prod-public/HLSL30.015/HLS.L30.T60MUD.2021014T222708.v1.5.jpg", + "type": "image/jpeg" + }, + "metadata": { + "href": "https://cmr.earthdata.nasa.gov/search/concepts/G1994873826-LPCLOUD.xml", + "type": "application/xml" + } + } + } + ], + "links": [ + { + "rel": "self", + "href": "https://cmr.earthdata.nasa.gov/stac/LPCLOUD/collections/HLSL30.v1.5/items" + }, + { + "rel": "root", + "href": "https://cmr.earthdata.nasa.gov/stac/" + }, + { + "rel": "next", + "method": "GET", + "href": "https://cmr.earthdata.nasa.gov/stac/LPCLOUD/collections/HLSL30.v1.5/items?page=2" + } + ], + "context": { + "returned": 10, + "limit": 1000000, + "matched": 324132 + } +} diff --git a/tests/test_item_collection.py b/tests/test_item_collection.py new file mode 100644 index 000000000..2bb7a1046 --- /dev/null +++ b/tests/test_item_collection.py @@ -0,0 +1,100 @@ +import json + +from pystac.item_collection import ItemCollection + +import unittest +import pystac + +from tests.utils import TestCases + + +class TestItemCollection(unittest.TestCase): + SIMPLE_ITEM = TestCases.get_path("data-files/examples/1.0.0-RC1/simple-item.json") + CORE_ITEM = TestCases.get_path("data-files/examples/1.0.0-RC1/core-item.json") + EXTENDED_ITEM = TestCases.get_path( + "data-files/examples/1.0.0-RC1/extended-item.json" + ) + + ITEM_COLLECTION = TestCases.get_path( + "data-files/item-collection/sample-item-collection.json" + ) + + def setUp(self) -> None: + self.maxDiff = None + with open(self.ITEM_COLLECTION) as src: + self.item_collection_dict = json.load(src) + self.items = [ + pystac.Item.from_dict(f) for f in self.item_collection_dict["features"] + ] + + def test_item_collection_length(self) -> None: + item_collection = pystac.ItemCollection(items=self.items) + + self.assertEqual(len(item_collection), len(self.items)) + + def test_item_collection_iter(self) -> None: + expected_ids = [item.id for item in self.items] + item_collection = pystac.ItemCollection(items=self.items) + + actual_ids = [item.id for item in item_collection] + + self.assertListEqual(expected_ids, actual_ids) + + def test_item_collection_get_item_by_index(self) -> None: + expected_id = self.items[0].id + item_collection = pystac.ItemCollection(items=self.items) + + self.assertEqual(item_collection[0].id, expected_id) + + def test_item_collection_extra_fields(self) -> None: + item_collection = pystac.ItemCollection( + items=self.items, extra_fields={"custom_field": "My value"} + ) + + self.assertEqual(item_collection.extra_fields.get("custom_field"), "My value") + + def test_item_collection_to_dict(self) -> None: + item_collection = pystac.ItemCollection( + items=self.items, extra_fields={"custom_field": "My value"} + ) + + d = item_collection.to_dict() + + self.assertEqual(len(d["features"]), len(self.items)) + self.assertEqual(d.get("custom_field"), "My value") + + def test_item_collection_from_dict(self) -> None: + features = [item.to_dict() for item in self.items] + d = { + "type": "FeatureCollection", + "features": features, + "custom_field": "My value", + } + item_collection = pystac.ItemCollection.from_dict(d) + expected = len(features) + self.assertEqual(expected, len(item_collection.items)) + self.assertEqual(item_collection.extra_fields.get("custom_field"), "My value") + + def test_clone_item_collection(self) -> None: + item_collection_1 = pystac.ItemCollection.from_file(self.ITEM_COLLECTION) + item_collection_2 = item_collection_1.clone() + + item_ids_1 = [item.id for item in item_collection_1] + item_ids_2 = [item.id for item in item_collection_2] + + # All items from the original collection should be in the clone... + self.assertListEqual(item_ids_1, item_ids_2) + # ... but they should not be the same objects + self.assertIsNot(item_collection_1[0], item_collection_2[0]) + + def test_raise_error_for_invalid_object(self) -> None: + with open(self.SIMPLE_ITEM) as src: + item_dict = json.load(src) + + with self.assertRaises(pystac.STACTypeError): + _ = ItemCollection.from_dict(item_dict) + + def test_from_relative_path(self) -> None: + _ = pystac.ItemCollection.from_file( + "./tests/data-files/item-collection/sample-item-collection.json" + ) From 5d58dc13e936e518034da2a9e6038d4e51988eda Mon Sep 17 00:00:00 2001 From: Jon Duckworth Date: Thu, 10 Jun 2021 13:10:32 -0400 Subject: [PATCH 02/14] Add ItemCollection to top-level I/O functions --- pystac/__init__.py | 59 ++++++++++++++++--------- pystac/item_collection.py | 17 +++++++ pystac/serialization/__init__.py | 2 +- tests/data-files/change_stac_version.py | 7 +-- tests/extensions/test_eo.py | 4 +- tests/extensions/test_pointcloud.py | 2 +- tests/extensions/test_raster.py | 5 ++- tests/extensions/test_timestamps.py | 2 +- tests/test_collection.py | 2 +- tests/test_item_collection.py | 19 ++++++-- tests/test_stac_io.py | 43 ++++++++++++++++-- tests/validation/test_validate.py | 5 ++- 12 files changed, 128 insertions(+), 39 deletions(-) diff --git a/pystac/__init__.py b/pystac/__init__.py index 082f98f43..e28a03b87 100644 --- a/pystac/__init__.py +++ b/pystac/__init__.py @@ -12,7 +12,7 @@ STACValidationError, ) -from typing import Any, Dict, Optional +from typing import Any, Dict, Optional, Union from pystac.version import ( __version__, get_stac_version, @@ -72,7 +72,7 @@ ) -def read_file(href: str) -> STACObject: +def read_file(href: str) -> Union[STACObject, ItemCollection]: """Reads a STAC object from a file. This method will return either a Catalog, a Collection, or an Item based on what the @@ -87,11 +87,16 @@ def read_file(href: str) -> STACObject: The specific STACObject implementation class that is represented by the JSON read from the file located at HREF. """ - return STACObject.from_file(href) + try: + return STACObject.from_file(href) + except STACTypeError: + return ItemCollection.from_file(href) def write_file( - obj: STACObject, include_self_link: bool = True, dest_href: Optional[str] = None + obj: Union[STACObject, ItemCollection], + include_self_link: bool = True, + dest_href: Optional[str] = None, ) -> None: """Writes a STACObject to a file. @@ -107,12 +112,20 @@ def write_file( Args: obj : The STACObject to save. - include_self_link : If this is true, include the 'self' link with this object. - Otherwise, leave out the self link. - dest_href : Optional HREF to save the file to. If None, the object will be saved - to the object's self href. + include_self_link : If ``True``, include the ``"self"`` link with this object. + Otherwise, leave out the self link. Ignored for :class:~ItemCollection` + instances. + dest_href : Optional HREF to save the file to. If ``None``, the object will be + saved to the object's ``"self"`` href (for :class:`~STACObject` sub-classes) + or a :exc:`~STACError` will be raised (for :class:`~ItemCollection` + instances). """ - obj.save_object(include_self_link=include_self_link, dest_href=dest_href) + if isinstance(obj, ItemCollection): + if dest_href is None: + raise STACError("Must provide dest_href when saving and ItemCollection.") + obj.save_object(dest_href=dest_href) + else: + obj.save_object(include_self_link=include_self_link, dest_href=dest_href) def read_dict( @@ -120,25 +133,31 @@ def read_dict( href: Optional[str] = None, root: Optional[Catalog] = None, stac_io: Optional[StacIO] = None, -) -> STACObject: - """Reads a STAC object from a dict representing the serialized JSON version of the - STAC object. +) -> Union[STACObject, ItemCollection]: + """Reads a :class:`~STACObject` or :class:`~ItemCollection` from a JSON-like dict + representing a serialized STAC object. - This method will return either a Catalog, a Collection, or an Item based on what the - dict contains. + This method will return either a :class:`~Catalog`, :class:`~Collection`, + :class`~Item`, or :class:`~ItemCollection` based on the contents of the dict. - This is a convenience method for :meth:`pystac.serialization.stac_object_from_dict` + This is a convenience method for either + :meth:`stac_io.stac_object_from_dict ` or + :meth:`ItemCollection.from_dict `. Args: d : The dict to parse. href : Optional href that is the file location of the object being - parsed. + parsed. Ignored if the dict represents an :class:`~ItemCollection`. root : Optional root of the catalog for this object. If provided, the root's resolved object cache can be used to search for - previously resolved instances of the STAC object. - stac_io: Optional StacIO instance to use for reading. If None, the - default instance will be used. + previously resolved instances of the STAC object. Ignored if the dict + represents an :class:`~ItemCollection`. + stac_io: Optional :class:`~StacIO` instance to use for reading. If ``None``, + the default instance will be used. """ if stac_io is None: stac_io = StacIO.default() - return stac_io.stac_object_from_dict(d, href, root) + try: + return stac_io.stac_object_from_dict(d, href, root) + except STACTypeError: + return ItemCollection.from_dict(d) diff --git a/pystac/item_collection.py b/pystac/item_collection.py index 9cad2ca9a..e58312ccb 100644 --- a/pystac/item_collection.py +++ b/pystac/item_collection.py @@ -83,3 +83,20 @@ def from_file( d = stac_io.read_json(href) return cls.from_dict(d) + + def save_object( + self, + dest_href: str, + stac_io: Optional[pystac.StacIO] = None, + ) -> None: + """Saves this instance to the ``dest_href`` location. + + Args: + dest_href : Location to which the file will be saved. + stac_io: Optional :class:`~pystac.StacIO` instance to use. If not provided, + will use the default instance. + """ + if stac_io is None: + stac_io = pystac.StacIO.default() + + stac_io.save_json(dest_href, self.to_dict()) diff --git a/pystac/serialization/__init__.py b/pystac/serialization/__init__.py index 9720c697c..e50412fd3 100644 --- a/pystac/serialization/__init__.py +++ b/pystac/serialization/__init__.py @@ -51,4 +51,4 @@ def stac_object_from_dict( if info.object_type == pystac.STACObjectType.ITEM: return pystac.Item.from_dict(d, href=href, root=root, migrate=False) - raise ValueError(f"Unknown STAC object type {info.object_type}") + raise pystac.STACTypeError(f"Unknown STAC object type {info.object_type}") diff --git a/tests/data-files/change_stac_version.py b/tests/data-files/change_stac_version.py index bc8ab8b1f..2cec60c3d 100644 --- a/tests/data-files/change_stac_version.py +++ b/tests/data-files/change_stac_version.py @@ -29,9 +29,10 @@ def migrate(path: str) -> None: ) ) obj = pystac.read_dict(stac_json, href=path) - migrated = obj.to_dict(include_self_link=False) - with open(path, "w") as f: - json.dump(migrated, f, indent=2) + if not isinstance(obj, pystac.ItemCollection): + migrated = obj.to_dict(include_self_link=False) + with open(path, "w") as f: + json.dump(migrated, f, indent=2) if __name__ == "__main__": diff --git a/tests/extensions/test_eo.py b/tests/extensions/test_eo.py index def803788..467ae6daf 100644 --- a/tests/extensions/test_eo.py +++ b/tests/extensions/test_eo.py @@ -53,8 +53,8 @@ def test_to_from_dict(self) -> None: assert_to_from_dict(self, Item, item_dict) def test_validate_eo(self) -> None: - item = pystac.read_file(self.LANDSAT_EXAMPLE_URI) - item2 = pystac.read_file(self.BANDS_IN_ITEM_URI) + item = pystac.Item.from_file(self.LANDSAT_EXAMPLE_URI) + item2 = pystac.Item.from_file(self.BANDS_IN_ITEM_URI) item.validate() item2.validate() diff --git a/tests/extensions/test_pointcloud.py b/tests/extensions/test_pointcloud.py index 2cf12b777..9bad4a440 100644 --- a/tests/extensions/test_pointcloud.py +++ b/tests/extensions/test_pointcloud.py @@ -44,7 +44,7 @@ def test_apply(self) -> None: self.assertTrue(PointcloudExtension.has_extension(item)) def test_validate_pointcloud(self) -> None: - item = pystac.read_file(self.example_uri) + item = pystac.Item.from_file(self.example_uri) item.validate() def test_count(self) -> None: diff --git a/tests/extensions/test_raster.py b/tests/extensions/test_raster.py index 0737f6585..aeed0d9ba 100644 --- a/tests/extensions/test_raster.py +++ b/tests/extensions/test_raster.py @@ -33,8 +33,9 @@ def test_to_from_dict(self) -> None: assert_to_from_dict(self, Item, item_dict) def test_validate_raster(self) -> None: - item = pystac.read_file(self.PLANET_EXAMPLE_URI) - item2 = pystac.read_file(self.SENTINEL2_EXAMPLE_URI) + item = pystac.Item.from_file(self.PLANET_EXAMPLE_URI) + item2 = pystac.Item.from_file(self.SENTINEL2_EXAMPLE_URI) + item.validate() item2.validate() diff --git a/tests/extensions/test_timestamps.py b/tests/extensions/test_timestamps.py index 4acabc8c5..92e3e2805 100644 --- a/tests/extensions/test_timestamps.py +++ b/tests/extensions/test_timestamps.py @@ -59,7 +59,7 @@ def test_apply(self) -> None: self.assertNotIn(p, item.properties) def test_validate_timestamps(self) -> None: - item = pystac.read_file(self.example_uri) + item = pystac.Item.from_file(self.example_uri) item.validate() def test_expires(self) -> None: diff --git a/tests/test_collection.py b/tests/test_collection.py index a73aa244f..6f87ecd0e 100644 --- a/tests/test_collection.py +++ b/tests/test_collection.py @@ -178,7 +178,7 @@ def test_assets(self) -> None: path = TestCases.get_path("data-files/collections/with-assets.json") with open(path) as f: data = json.load(f) - collection = pystac.read_dict(data) + collection = pystac.Collection.from_dict(data) collection.validate() diff --git a/tests/test_item_collection.py b/tests/test_item_collection.py index 2bb7a1046..433b1a9a2 100644 --- a/tests/test_item_collection.py +++ b/tests/test_item_collection.py @@ -1,7 +1,5 @@ import json - -from pystac.item_collection import ItemCollection - +from typing import cast import unittest import pystac @@ -75,6 +73,19 @@ def test_item_collection_from_dict(self) -> None: self.assertEqual(expected, len(item_collection.items)) self.assertEqual(item_collection.extra_fields.get("custom_field"), "My value") + def test_item_collection_from_dict_top_level(self) -> None: + features = [item.to_dict() for item in self.items] + d = { + "type": "FeatureCollection", + "features": features, + "custom_field": "My value", + } + item_collection = pystac.read_dict(d) + item_collection = cast(pystac.ItemCollection, item_collection) + expected = len(features) + self.assertEqual(expected, len(item_collection.items)) + self.assertEqual(item_collection.extra_fields.get("custom_field"), "My value") + def test_clone_item_collection(self) -> None: item_collection_1 = pystac.ItemCollection.from_file(self.ITEM_COLLECTION) item_collection_2 = item_collection_1.clone() @@ -92,7 +103,7 @@ def test_raise_error_for_invalid_object(self) -> None: item_dict = json.load(src) with self.assertRaises(pystac.STACTypeError): - _ = ItemCollection.from_dict(item_dict) + _ = pystac.ItemCollection.from_dict(item_dict) def test_from_relative_path(self) -> None: _ = pystac.ItemCollection.from_file( diff --git a/tests/test_stac_io.py b/tests/test_stac_io.py index 58ac51e23..96437f959 100644 --- a/tests/test_stac_io.py +++ b/tests/test_stac_io.py @@ -1,9 +1,10 @@ +import os import unittest import warnings import pystac from pystac.stac_io import STAC_IO -from tests.utils import TestCases +from tests.utils import TestCases, get_temp_dir class StacIOTest(unittest.TestCase): @@ -20,7 +21,43 @@ def test_stac_io_issues_warnings(self) -> None: self.assertEqual(len(w), 1) self.assertTrue(issubclass(w[-1].category, DeprecationWarning)) - def test_read_text(self) -> None: - _ = pystac.read_file( + def test_read_write_collection(self) -> None: + collection = pystac.read_file( TestCases.get_path("data-files/collections/multi-extent.json") ) + with get_temp_dir() as tmp_dir: + dest_href = os.path.join(tmp_dir, "collection.json") + pystac.write_file(collection, dest_href=dest_href) + self.assertTrue(os.path.exists(dest_href), msg="File was not written.") + + def test_read_item(self) -> None: + item = pystac.read_file(TestCases.get_path("data-files/item/sample-item.json")) + with get_temp_dir() as tmp_dir: + dest_href = os.path.join(tmp_dir, "item.json") + pystac.write_file(item, dest_href=dest_href) + self.assertTrue(os.path.exists(dest_href), msg="File was not written.") + + def test_read_write_catalog(self) -> None: + catalog = pystac.read_file( + TestCases.get_path("data-files/catalogs/test-case-1/catalog.json") + ) + with get_temp_dir() as tmp_dir: + dest_href = os.path.join(tmp_dir, "catalog.json") + pystac.write_file(catalog, dest_href=dest_href) + self.assertTrue(os.path.exists(dest_href), msg="File was not written.") + + def test_read_write_item_collection(self) -> None: + item_collection = pystac.read_file( + TestCases.get_path("data-files/item-collection/sample-item-collection.json") + ) + with get_temp_dir() as tmp_dir: + dest_href = os.path.join(tmp_dir, "item-collection.json") + pystac.write_file(item_collection, dest_href=dest_href) + self.assertTrue(os.path.exists(dest_href), msg="File was not written.") + + def test_write_item_collection_needs_href(self) -> None: + item_collection = pystac.read_file( + TestCases.get_path("data-files/item-collection/sample-item-collection.json") + ) + with self.assertRaises(pystac.STACError): + pystac.write_file(item_collection) diff --git a/tests/validation/test_validate.py b/tests/validation/test_validate.py index c6bf68444..7e1ea6af1 100644 --- a/tests/validation/test_validate.py +++ b/tests/validation/test_validate.py @@ -1,7 +1,7 @@ from datetime import datetime import json import os -from typing import Any, Dict +from typing import Any, Dict, cast from pystac.utils import get_opt import shutil import unittest @@ -20,6 +20,7 @@ def test_validate_current_version(self) -> None: catalog = pystac.read_file( TestCases.get_path("data-files/catalogs/test-case-1/" "catalog.json") ) + catalog = cast(pystac.STACObject, catalog) catalog.validate() collection = pystac.read_file( @@ -29,9 +30,11 @@ def test_validate_current_version(self) -> None: "collection.json" ) ) + collection = cast(pystac.Collection, collection) collection.validate() item = pystac.read_file(TestCases.get_path("data-files/item/sample-item.json")) + item = cast(pystac.Item, item) item.validate() def test_validate_examples(self) -> None: From 2a8cfa16dd465cef21ac43d35eaea6eab0a98d08 Mon Sep 17 00:00:00 2001 From: Jon Duckworth Date: Thu, 10 Jun 2021 13:10:32 -0400 Subject: [PATCH 03/14] Add ItemCollection documentation --- docs/api.rst | 8 ++++++++ pystac/item_collection.py | 29 ++++++++++++++++++++++++++--- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 742295084..05a973192 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -127,6 +127,14 @@ CommonMetadata :members: :undoc-members: +ItemCollection +-------------- +Represents a GeoJSON FeatureCollection in which all Features are STAC Items + +.. autoclass:: pystac.ItemCollection + :members: + :show-inheritance: + Links ----- diff --git a/pystac/item_collection.py b/pystac/item_collection.py index e58312ccb..28b563bc7 100644 --- a/pystac/item_collection.py +++ b/pystac/item_collection.py @@ -9,7 +9,30 @@ class ItemCollection(Sized, Iterable[pystac.Item]): """Implementation of a GeoJSON FeatureCollection whose features are all STAC - Items.""" + Items. + + All :class:`~pystac.Item` instances passed to the :class:`~ItemCollection` instance + during instantation are cloned and have their ``"root"`` URL cleared. Instances of + this class are iterable and sized (see examples below). + + Any additional top-level fields in the FeatureCollection are retained in + :attr:`~ItemCollection.extra_fields` by the :meth:`~ItemCollection.from_dict` and + :meth:`~ItemCollection.from_file` methods and will be present in the serialized file + from :meth:`~ItemCollection.save_object`. + + Examples: + + Loop over all items in the ItemCollection + + >>> item_collection: ItemCollection = ... + >>> for item in item_collection: + ... ... + + Get the number of Items in the ItemCollection + + >>> length: int = len(item_collection) + + """ items: List[pystac.Item] """The list of :class:`pystac.Item` instances contained in this @@ -46,8 +69,8 @@ def to_dict(self) -> Dict[str, Any]: def clone(self) -> "ItemCollection": """Creates a clone of this instance. This clone is a deep copy; all - :class:`~pystac.Item`s are cloned and all additional top-level fields are deep - copied.""" + :class:`~pystac.Item` instances are cloned and all additional top-level fields + are deep copied.""" return self.__class__( items=[item.clone() for item in self.items], extra_fields=deepcopy(self.extra_fields), From c6b461b1730860456a3a098a3cedde3eafe2aa7a Mon Sep 17 00:00:00 2001 From: Jon Duckworth Date: Thu, 10 Jun 2021 13:25:31 -0400 Subject: [PATCH 04/14] Fix lint issues --- docs/api.rst | 2 +- pystac/item_collection.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 05a973192..0f3624742 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -134,7 +134,7 @@ Represents a GeoJSON FeatureCollection in which all Features are STAC Items .. autoclass:: pystac.ItemCollection :members: :show-inheritance: - + Links ----- diff --git a/pystac/item_collection.py b/pystac/item_collection.py index 28b563bc7..dd49af23a 100644 --- a/pystac/item_collection.py +++ b/pystac/item_collection.py @@ -12,9 +12,9 @@ class ItemCollection(Sized, Iterable[pystac.Item]): Items. All :class:`~pystac.Item` instances passed to the :class:`~ItemCollection` instance - during instantation are cloned and have their ``"root"`` URL cleared. Instances of + during instantiation are cloned and have their ``"root"`` URL cleared. Instances of this class are iterable and sized (see examples below). - + Any additional top-level fields in the FeatureCollection are retained in :attr:`~ItemCollection.extra_fields` by the :meth:`~ItemCollection.from_dict` and :meth:`~ItemCollection.from_file` methods and will be present in the serialized file From 61b12d1b72825c8030cb7cae144a0a1f2e399432 Mon Sep 17 00:00:00 2001 From: Jon Duckworth Date: Thu, 10 Jun 2021 13:28:18 -0400 Subject: [PATCH 05/14] Update CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26468ccda..53d7c96bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ - Links to Issues, Discussions, and documentation sites ([#409](https://github.com/stac-utils/pystac/pull/409)) - Python minimum version set to `>=3.6` ([#409](https://github.com/stac-utils/pystac/pull/409)) - Code of Conduct ([#399](https://github.com/stac-utils/pystac/pull/399)) +- `ItemCollection` class for working with GeoJSON FeatureCollections containing only + STAC Items ([#430](https://github.com/stac-utils/pystac/pull/430)) ### Changed From 9b1fed25c879511e91ead274df5d5244520f34b8 Mon Sep 17 00:00:00 2001 From: Jon Duckworth Date: Thu, 10 Jun 2021 19:47:05 -0400 Subject: [PATCH 06/14] Option to not clone Items when instantiating ItemCollection --- pystac/item_collection.py | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/pystac/item_collection.py b/pystac/item_collection.py index dd49af23a..16f363a41 100644 --- a/pystac/item_collection.py +++ b/pystac/item_collection.py @@ -20,34 +20,47 @@ class ItemCollection(Sized, Iterable[pystac.Item]): :meth:`~ItemCollection.from_file` methods and will be present in the serialized file from :meth:`~ItemCollection.save_object`. + Arguments: + + items : List of :class:`~pystac.Item` instances to include in the + :class:`~ItemCollection`. + extra_fields : Dictionary of additional top-level fields included in the + :class:`~ItemCollection`. + clone_items : Optional flag indicating whether :class:`~pystac.Item` instances + should be cloned before storing in the :class:`~ItemCollection`. Setting to + ``False`` will result in faster instantiation, but any changes made to + :class:`~pystac.Item` instances in the :class:`~ItemCollection` will also + mutate the original :class:`~pystac.Item`. Defaults to ``True``. + Examples: - Loop over all items in the ItemCollection + Loop over all items in the :class`~ItemCollection` >>> item_collection: ItemCollection = ... >>> for item in item_collection: ... ... - Get the number of Items in the ItemCollection + Get the number of :class:`~pytac.Item` instances in the + :class:`~ItemCollection` >>> length: int = len(item_collection) """ items: List[pystac.Item] - """The list of :class:`pystac.Item` instances contained in this - ``ItemCollection``.""" + """List of :class:`pystac.Item` instances contained in this ``ItemCollection``.""" extra_fields: Dict[str, Any] - """Dictionary containing additional top-level fields for the GeoJSON + """Dictionary of additional top-level fields for the GeoJSON FeatureCollection.""" def __init__( - self, items: List[pystac.Item], extra_fields: Optional[Dict[str, Any]] = None + self, + items: List[pystac.Item], + extra_fields: Optional[Dict[str, Any]] = None, + clone_items: bool = True, ): - self.items = [item.clone() for item in items] - for item in self.items: - item.clear_links("root") + self.items = [item.clone() if clone_items else item for item in items] self.extra_fields = extra_fields or {} def __getitem__(self, idx: int) -> pystac.Item: From 542f8c6b4892e6211b7d59201c76e4e35934e835 Mon Sep 17 00:00:00 2001 From: Jon Duckworth Date: Thu, 10 Jun 2021 19:54:01 -0400 Subject: [PATCH 07/14] Make ItemCollection a typing.Collection --- docs/conf.py | 5 +++++ pystac/item_collection.py | 19 ++++++++++++++++--- tests/test_item_collection.py | 6 ++++++ 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 9c4d5abe2..2b860a083 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -52,6 +52,7 @@ extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.viewcode', + 'sphinx.ext.intersphinx', 'sphinx.ext.napoleon', 'sphinx.ext.githubpages', 'sphinx.ext.extlinks', @@ -207,3 +208,7 @@ # -- Extension configuration ------------------------------------------------- + +intersphinx_mapping = { + 'python': ('https://docs.python.org/3', None), +} diff --git a/pystac/item_collection.py b/pystac/item_collection.py index 16f363a41..1ca696f5c 100644 --- a/pystac/item_collection.py +++ b/pystac/item_collection.py @@ -1,19 +1,20 @@ from copy import deepcopy from pystac.errors import STACTypeError -from typing import Any, Dict, Iterator, List, Optional, Sized, Iterable +from typing import Any, Dict, Iterator, List, Optional, Collection import pystac from pystac.utils import make_absolute_href, is_absolute_href from pystac.serialization.identify import identify_stac_object_type -class ItemCollection(Sized, Iterable[pystac.Item]): +class ItemCollection(Collection[pystac.Item]): """Implementation of a GeoJSON FeatureCollection whose features are all STAC Items. All :class:`~pystac.Item` instances passed to the :class:`~ItemCollection` instance during instantiation are cloned and have their ``"root"`` URL cleared. Instances of - this class are iterable and sized (see examples below). + this class implement the abstract methods of :class:`typing.Collection` (see + below for examples using these methods). Any additional top-level fields in the FeatureCollection are retained in :attr:`~ItemCollection.extra_fields` by the :meth:`~ItemCollection.from_dict` and @@ -45,6 +46,15 @@ class ItemCollection(Sized, Iterable[pystac.Item]): >>> length: int = len(item_collection) + Check if an :class:`~pystac.Item` is in the :class:`~ItemCollection`. Note + that you must use `clone_items=False` for this to return ``True``, since + equality of PySTAC objects is currently evaluated using default object + equality (i.e. ``item_1 is item_2``). + + >>> item: Item = ... + >>> item_collection = ItemCollection(items=[item], clone_items=False) + >>> assert item in item_collection + """ items: List[pystac.Item] @@ -72,6 +82,9 @@ def __iter__(self) -> Iterator[pystac.Item]: def __len__(self) -> int: return len(self.items) + def __contains__(self, __x: object) -> bool: + return __x in self.items + def to_dict(self) -> Dict[str, Any]: """Serializes an :class:`ItemCollection` instance to a JSON-like dictionary.""" return { diff --git a/tests/test_item_collection.py b/tests/test_item_collection.py index 433b1a9a2..2bac9627a 100644 --- a/tests/test_item_collection.py +++ b/tests/test_item_collection.py @@ -44,6 +44,12 @@ def test_item_collection_get_item_by_index(self) -> None: self.assertEqual(item_collection[0].id, expected_id) + def test_item_collection_contains(self) -> None: + item = pystac.Item.from_file(self.SIMPLE_ITEM) + item_collection = pystac.ItemCollection(items=[item], clone_items=False) + + self.assertIn(item, item_collection) + def test_item_collection_extra_fields(self) -> None: item_collection = pystac.ItemCollection( items=self.items, extra_fields={"custom_field": "My value"} From 31beb9de97dd7370dab528f7d05b7d53f7003c69 Mon Sep 17 00:00:00 2001 From: Jon Duckworth Date: Thu, 10 Jun 2021 19:55:15 -0400 Subject: [PATCH 08/14] Allow Iterable items argument to ItemCollection --- pystac/item_collection.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pystac/item_collection.py b/pystac/item_collection.py index 1ca696f5c..10111866f 100644 --- a/pystac/item_collection.py +++ b/pystac/item_collection.py @@ -1,6 +1,6 @@ from copy import deepcopy from pystac.errors import STACTypeError -from typing import Any, Dict, Iterator, List, Optional, Collection +from typing import Any, Dict, Iterator, List, Optional, Collection, Iterable import pystac from pystac.utils import make_absolute_href, is_absolute_href @@ -66,7 +66,7 @@ class ItemCollection(Collection[pystac.Item]): def __init__( self, - items: List[pystac.Item], + items: Iterable[pystac.Item], extra_fields: Optional[Dict[str, Any]] = None, clone_items: bool = True, ): From 698c54bf94120f4896da8fb679b53a9f6c5dc76d Mon Sep 17 00:00:00 2001 From: Jon Duckworth Date: Fri, 11 Jun 2021 10:52:17 -0400 Subject: [PATCH 09/14] Do not clone Items in ItemCollection.from_dict --- pystac/item_collection.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pystac/item_collection.py b/pystac/item_collection.py index 10111866f..c4b9d60cb 100644 --- a/pystac/item_collection.py +++ b/pystac/item_collection.py @@ -104,14 +104,20 @@ def clone(self) -> "ItemCollection": @classmethod def from_dict(cls, d: Dict[str, Any]) -> "ItemCollection": - """Creates a :class:`ItemCollection` instance from a dictionary.""" + """Creates a :class:`ItemCollection` instance from a dictionary. + + Arguments: + d : The dictionary from which the :class:`~ItemCollection` will be created + """ if identify_stac_object_type(d) != pystac.STACObjectType.ITEMCOLLECTION: raise STACTypeError("Dict is not a valid ItemCollection") items = [pystac.Item.from_dict(item) for item in d.get("features", [])] extra_fields = {k: v for k, v in d.items() if k not in ("features", "type")} - return cls(items=items, extra_fields=extra_fields) + # Since we are reading these Items from a dict within this method, there will be + # no other references and we do not need to clone them. + return cls(items=items, extra_fields=extra_fields, clone_items=False) @classmethod def from_file( From 702a26f4a9a510d604dd223b490bb27a859ae898 Mon Sep 17 00:00:00 2001 From: Jon Duckworth Date: Fri, 11 Jun 2021 10:52:37 -0400 Subject: [PATCH 10/14] Accept Iterable of dicts or Items in ItemCollection.__init__ --- pystac/item_collection.py | 16 +++++++++++++--- tests/test_item_collection.py | 11 +++++++++-- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/pystac/item_collection.py b/pystac/item_collection.py index c4b9d60cb..78f8ca16c 100644 --- a/pystac/item_collection.py +++ b/pystac/item_collection.py @@ -1,12 +1,15 @@ from copy import deepcopy from pystac.errors import STACTypeError -from typing import Any, Dict, Iterator, List, Optional, Collection, Iterable +from typing import Any, Dict, Iterator, List, Optional, Collection, Iterable, Union import pystac from pystac.utils import make_absolute_href, is_absolute_href from pystac.serialization.identify import identify_stac_object_type +ItemLike = Union[pystac.Item, Dict[str, Any]] + + class ItemCollection(Collection[pystac.Item]): """Implementation of a GeoJSON FeatureCollection whose features are all STAC Items. @@ -66,11 +69,18 @@ class ItemCollection(Collection[pystac.Item]): def __init__( self, - items: Iterable[pystac.Item], + items: Iterable[ItemLike], extra_fields: Optional[Dict[str, Any]] = None, clone_items: bool = True, ): - self.items = [item.clone() if clone_items else item for item in items] + def map_item(item_or_dict: ItemLike) -> pystac.Item: + # Converts dicts to pystac.Items and clones if necessary + if isinstance(item_or_dict, pystac.Item): + return item_or_dict.clone() if clone_items else item_or_dict + else: + return pystac.Item.from_dict(item_or_dict) + + self.items = list(map(map_item, items)) self.extra_fields = extra_fields or {} def __getitem__(self, idx: int) -> pystac.Item: diff --git a/tests/test_item_collection.py b/tests/test_item_collection.py index 2bac9627a..a5b29e2cf 100644 --- a/tests/test_item_collection.py +++ b/tests/test_item_collection.py @@ -1,4 +1,5 @@ import json +from pystac.item_collection import ItemCollection from typing import cast import unittest import pystac @@ -24,6 +25,7 @@ def setUp(self) -> None: self.items = [ pystac.Item.from_dict(f) for f in self.item_collection_dict["features"] ] + self.stac_io = pystac.StacIO.default() def test_item_collection_length(self) -> None: item_collection = pystac.ItemCollection(items=self.items) @@ -105,8 +107,7 @@ def test_clone_item_collection(self) -> None: self.assertIsNot(item_collection_1[0], item_collection_2[0]) def test_raise_error_for_invalid_object(self) -> None: - with open(self.SIMPLE_ITEM) as src: - item_dict = json.load(src) + item_dict = self.stac_io.read_json(self.SIMPLE_ITEM) with self.assertRaises(pystac.STACTypeError): _ = pystac.ItemCollection.from_dict(item_dict) @@ -115,3 +116,9 @@ def test_from_relative_path(self) -> None: _ = pystac.ItemCollection.from_file( "./tests/data-files/item-collection/sample-item-collection.json" ) + + def test_from_list_of_dicts(self) -> None: + item_dict = self.stac_io.read_json(self.SIMPLE_ITEM) + item_collection = ItemCollection(items=[item_dict]) + + self.assertEqual(item_collection[0].id, item_dict.get("id")) From 1ead0d573c6f7356e947583aebec5c9c22a0987d Mon Sep 17 00:00:00 2001 From: Jon Duckworth Date: Fri, 11 Jun 2021 11:16:13 -0400 Subject: [PATCH 11/14] Ability to add ItemCollections --- pystac/item_collection.py | 31 +++++++++++++++++++++++++++++-- tests/test_item_collection.py | 25 +++++++++++++++++++++++-- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/pystac/item_collection.py b/pystac/item_collection.py index 78f8ca16c..037e2af55 100644 --- a/pystac/item_collection.py +++ b/pystac/item_collection.py @@ -16,8 +16,8 @@ class ItemCollection(Collection[pystac.Item]): All :class:`~pystac.Item` instances passed to the :class:`~ItemCollection` instance during instantiation are cloned and have their ``"root"`` URL cleared. Instances of - this class implement the abstract methods of :class:`typing.Collection` (see - below for examples using these methods). + this class implement the abstract methods of :class:`typing.Collection` and can also + be added together (see below for examples using these methods). Any additional top-level fields in the FeatureCollection are retained in :attr:`~ItemCollection.extra_fields` by the :meth:`~ItemCollection.from_dict` and @@ -58,6 +58,22 @@ class ItemCollection(Collection[pystac.Item]): >>> item_collection = ItemCollection(items=[item], clone_items=False) >>> assert item in item_collection + Combine :class:`~ItemCollection` instances + + >>> item_1: Item = ... + >>> item_2: Item = ... + >>> item_3: Item = ... + >>> item_collection_1 = ItemCollection( + ... items=[item_1, item_2], + ... clone_items=False + ... ) + >>> item_collection_2 = ItemCollection( + ... items=[item_2, item_3], + ... clone_items=False + ... ) + >>> combined = item_collection_1 + item_collection_2 + >>> assert len(combined) == 3 + # If an item is present in both ItemCollections it will only be added once """ items: List[pystac.Item] @@ -95,6 +111,17 @@ def __len__(self) -> int: def __contains__(self, __x: object) -> bool: return __x in self.items + def __add__(self, other: object) -> "ItemCollection": + if not isinstance(other, ItemCollection): + return NotImplemented + + combined = [] + for item in self.items + other.items: + if item not in combined: + combined.append(item) + + return ItemCollection(items=combined, clone_items=False) + def to_dict(self) -> Dict[str, Any]: """Serializes an :class:`ItemCollection` instance to a JSON-like dictionary.""" return { diff --git a/tests/test_item_collection.py b/tests/test_item_collection.py index a5b29e2cf..6417c31a7 100644 --- a/tests/test_item_collection.py +++ b/tests/test_item_collection.py @@ -1,5 +1,4 @@ import json -from pystac.item_collection import ItemCollection from typing import cast import unittest import pystac @@ -119,6 +118,28 @@ def test_from_relative_path(self) -> None: def test_from_list_of_dicts(self) -> None: item_dict = self.stac_io.read_json(self.SIMPLE_ITEM) - item_collection = ItemCollection(items=[item_dict]) + item_collection = pystac.ItemCollection(items=[item_dict]) self.assertEqual(item_collection[0].id, item_dict.get("id")) + + def test_add_item_collections(self) -> None: + item_1 = pystac.Item.from_file(self.SIMPLE_ITEM) + item_2 = pystac.Item.from_file(self.EXTENDED_ITEM) + item_3 = pystac.Item.from_file(self.CORE_ITEM) + + item_collection_1 = pystac.ItemCollection( + items=[item_1, item_2], clone_items=False + ) + item_collection_2 = pystac.ItemCollection( + items=[item_2, item_3], clone_items=False + ) + + combined = item_collection_1 + item_collection_2 + + self.assertEqual(len(combined), 3) + + def test_add_other_raises_error(self) -> None: + item_collection = pystac.ItemCollection.from_file(self.ITEM_COLLECTION) + + with self.assertRaises(TypeError): + _ = item_collection + 2 From 97354cdc8aa71e6f41867eb4ae9c4b4b21aa5329 Mon Sep 17 00:00:00 2001 From: Jon Duckworth Date: Fri, 11 Jun 2021 15:25:15 -0400 Subject: [PATCH 12/14] Default to not cloning Items on instantiation --- pystac/item_collection.py | 30 +++++++++++------------------- tests/test_item_collection.py | 10 +++------- 2 files changed, 14 insertions(+), 26 deletions(-) diff --git a/pystac/item_collection.py b/pystac/item_collection.py index 037e2af55..e9fdab107 100644 --- a/pystac/item_collection.py +++ b/pystac/item_collection.py @@ -32,9 +32,9 @@ class ItemCollection(Collection[pystac.Item]): :class:`~ItemCollection`. clone_items : Optional flag indicating whether :class:`~pystac.Item` instances should be cloned before storing in the :class:`~ItemCollection`. Setting to - ``False`` will result in faster instantiation, but any changes made to - :class:`~pystac.Item` instances in the :class:`~ItemCollection` will also - mutate the original :class:`~pystac.Item`. Defaults to ``True``. + ``True`` ensures that changes made to :class:`~pystac.Item` instances in + the :class:`~ItemCollection` will not mutate the original ``Item``, but + will result in slower instantiation. Defaults to ``False``. Examples: @@ -50,12 +50,12 @@ class ItemCollection(Collection[pystac.Item]): >>> length: int = len(item_collection) Check if an :class:`~pystac.Item` is in the :class:`~ItemCollection`. Note - that you must use `clone_items=False` for this to return ``True``, since - equality of PySTAC objects is currently evaluated using default object - equality (i.e. ``item_1 is item_2``). + that the ``clone_items`` argument must be ``False`` for this to return + ``True``, since equality of PySTAC objects is currently evaluated using default + object equality (i.e. ``item_1 is item_2``). >>> item: Item = ... - >>> item_collection = ItemCollection(items=[item], clone_items=False) + >>> item_collection = ItemCollection(items=[item]) >>> assert item in item_collection Combine :class:`~ItemCollection` instances @@ -63,14 +63,8 @@ class ItemCollection(Collection[pystac.Item]): >>> item_1: Item = ... >>> item_2: Item = ... >>> item_3: Item = ... - >>> item_collection_1 = ItemCollection( - ... items=[item_1, item_2], - ... clone_items=False - ... ) - >>> item_collection_2 = ItemCollection( - ... items=[item_2, item_3], - ... clone_items=False - ... ) + >>> item_collection_1 = ItemCollection(items=[item_1, item_2]) + >>> item_collection_2 = ItemCollection(items=[item_2, item_3]) >>> combined = item_collection_1 + item_collection_2 >>> assert len(combined) == 3 # If an item is present in both ItemCollections it will only be added once @@ -87,7 +81,7 @@ def __init__( self, items: Iterable[ItemLike], extra_fields: Optional[Dict[str, Any]] = None, - clone_items: bool = True, + clone_items: bool = False, ): def map_item(item_or_dict: ItemLike) -> pystac.Item: # Converts dicts to pystac.Items and clones if necessary @@ -152,9 +146,7 @@ def from_dict(cls, d: Dict[str, Any]) -> "ItemCollection": items = [pystac.Item.from_dict(item) for item in d.get("features", [])] extra_fields = {k: v for k, v in d.items() if k not in ("features", "type")} - # Since we are reading these Items from a dict within this method, there will be - # no other references and we do not need to clone them. - return cls(items=items, extra_fields=extra_fields, clone_items=False) + return cls(items=items, extra_fields=extra_fields) @classmethod def from_file( diff --git a/tests/test_item_collection.py b/tests/test_item_collection.py index 6417c31a7..f95fbde3e 100644 --- a/tests/test_item_collection.py +++ b/tests/test_item_collection.py @@ -47,7 +47,7 @@ def test_item_collection_get_item_by_index(self) -> None: def test_item_collection_contains(self) -> None: item = pystac.Item.from_file(self.SIMPLE_ITEM) - item_collection = pystac.ItemCollection(items=[item], clone_items=False) + item_collection = pystac.ItemCollection(items=[item]) self.assertIn(item, item_collection) @@ -127,12 +127,8 @@ def test_add_item_collections(self) -> None: item_2 = pystac.Item.from_file(self.EXTENDED_ITEM) item_3 = pystac.Item.from_file(self.CORE_ITEM) - item_collection_1 = pystac.ItemCollection( - items=[item_1, item_2], clone_items=False - ) - item_collection_2 = pystac.ItemCollection( - items=[item_2, item_3], clone_items=False - ) + item_collection_1 = pystac.ItemCollection(items=[item_1, item_2]) + item_collection_2 = pystac.ItemCollection(items=[item_2, item_3]) combined = item_collection_1 + item_collection_2 From 2bb16a13db4b4974c1a52cccaba84e370839e1b4 Mon Sep 17 00:00:00 2001 From: Jon Duckworth Date: Sun, 13 Jun 2021 21:26:25 -0400 Subject: [PATCH 13/14] Remove ItemCollection from top-level functions --- pystac/__init__.py | 54 ++++++++++++------------- tests/data-files/change_stac_version.py | 7 ++-- tests/test_item_collection.py | 14 ------- tests/test_stac_io.py | 22 ++++------ tests/validation/test_validate.py | 5 +-- 5 files changed, 36 insertions(+), 66 deletions(-) diff --git a/pystac/__init__.py b/pystac/__init__.py index e28a03b87..19732af9b 100644 --- a/pystac/__init__.py +++ b/pystac/__init__.py @@ -12,7 +12,7 @@ STACValidationError, ) -from typing import Any, Dict, Optional, Union +from typing import Any, Dict, Optional from pystac.version import ( __version__, get_stac_version, @@ -72,7 +72,7 @@ ) -def read_file(href: str) -> Union[STACObject, ItemCollection]: +def read_file(href: str) -> STACObject: """Reads a STAC object from a file. This method will return either a Catalog, a Collection, or an Item based on what the @@ -86,15 +86,18 @@ def read_file(href: str) -> Union[STACObject, ItemCollection]: Returns: The specific STACObject implementation class that is represented by the JSON read from the file located at HREF. + + Raises: + STACTypeError : If the file at ``href`` does not represent a valid + :class:`~pystac.STACObject`. Note that an :class:`~pystac.ItemCollection` is not + a :class:`~pystac.STACObject` and must be read using + :meth:`ItemCollection.from_file ` """ - try: - return STACObject.from_file(href) - except STACTypeError: - return ItemCollection.from_file(href) + return STACObject.from_file(href) def write_file( - obj: Union[STACObject, ItemCollection], + obj: STACObject, include_self_link: bool = True, dest_href: Optional[str] = None, ) -> None: @@ -113,19 +116,11 @@ def write_file( Args: obj : The STACObject to save. include_self_link : If ``True``, include the ``"self"`` link with this object. - Otherwise, leave out the self link. Ignored for :class:~ItemCollection` - instances. + Otherwise, leave out the self link. dest_href : Optional HREF to save the file to. If ``None``, the object will be - saved to the object's ``"self"`` href (for :class:`~STACObject` sub-classes) - or a :exc:`~STACError` will be raised (for :class:`~ItemCollection` - instances). + saved to the object's ``"self"`` href. """ - if isinstance(obj, ItemCollection): - if dest_href is None: - raise STACError("Must provide dest_href when saving and ItemCollection.") - obj.save_object(dest_href=dest_href) - else: - obj.save_object(include_self_link=include_self_link, dest_href=dest_href) + obj.save_object(include_self_link=include_self_link, dest_href=dest_href) def read_dict( @@ -133,31 +128,32 @@ def read_dict( href: Optional[str] = None, root: Optional[Catalog] = None, stac_io: Optional[StacIO] = None, -) -> Union[STACObject, ItemCollection]: +) -> STACObject: """Reads a :class:`~STACObject` or :class:`~ItemCollection` from a JSON-like dict representing a serialized STAC object. This method will return either a :class:`~Catalog`, :class:`~Collection`, - :class`~Item`, or :class:`~ItemCollection` based on the contents of the dict. + or :class`~Item` based on the contents of the dict. This is a convenience method for either - :meth:`stac_io.stac_object_from_dict ` or - :meth:`ItemCollection.from_dict `. + :meth:`stac_io.stac_object_from_dict `. Args: d : The dict to parse. href : Optional href that is the file location of the object being - parsed. Ignored if the dict represents an :class:`~ItemCollection`. + parsed. root : Optional root of the catalog for this object. If provided, the root's resolved object cache can be used to search for - previously resolved instances of the STAC object. Ignored if the dict - represents an :class:`~ItemCollection`. + previously resolved instances of the STAC object. stac_io: Optional :class:`~StacIO` instance to use for reading. If ``None``, the default instance will be used. + + Raises: + STACTypeError : If the ``d`` dictionary does not represent a valid + :class:`~pystac.STACObject`. Note that an :class:`~pystac.ItemCollection` is not + a :class:`~pystac.STACObject` and must be read using + :meth:`ItemCollection.from_dict ` """ if stac_io is None: stac_io = StacIO.default() - try: - return stac_io.stac_object_from_dict(d, href, root) - except STACTypeError: - return ItemCollection.from_dict(d) + return stac_io.stac_object_from_dict(d, href, root) diff --git a/tests/data-files/change_stac_version.py b/tests/data-files/change_stac_version.py index 2cec60c3d..bc8ab8b1f 100644 --- a/tests/data-files/change_stac_version.py +++ b/tests/data-files/change_stac_version.py @@ -29,10 +29,9 @@ def migrate(path: str) -> None: ) ) obj = pystac.read_dict(stac_json, href=path) - if not isinstance(obj, pystac.ItemCollection): - migrated = obj.to_dict(include_self_link=False) - with open(path, "w") as f: - json.dump(migrated, f, indent=2) + migrated = obj.to_dict(include_self_link=False) + with open(path, "w") as f: + json.dump(migrated, f, indent=2) if __name__ == "__main__": diff --git a/tests/test_item_collection.py b/tests/test_item_collection.py index f95fbde3e..8bdc25213 100644 --- a/tests/test_item_collection.py +++ b/tests/test_item_collection.py @@ -1,5 +1,4 @@ import json -from typing import cast import unittest import pystac @@ -80,19 +79,6 @@ def test_item_collection_from_dict(self) -> None: self.assertEqual(expected, len(item_collection.items)) self.assertEqual(item_collection.extra_fields.get("custom_field"), "My value") - def test_item_collection_from_dict_top_level(self) -> None: - features = [item.to_dict() for item in self.items] - d = { - "type": "FeatureCollection", - "features": features, - "custom_field": "My value", - } - item_collection = pystac.read_dict(d) - item_collection = cast(pystac.ItemCollection, item_collection) - expected = len(features) - self.assertEqual(expected, len(item_collection.items)) - self.assertEqual(item_collection.extra_fields.get("custom_field"), "My value") - def test_clone_item_collection(self) -> None: item_collection_1 = pystac.ItemCollection.from_file(self.ITEM_COLLECTION) item_collection_2 = item_collection_1.clone() diff --git a/tests/test_stac_io.py b/tests/test_stac_io.py index 96437f959..55db6b9b6 100644 --- a/tests/test_stac_io.py +++ b/tests/test_stac_io.py @@ -46,18 +46,10 @@ def test_read_write_catalog(self) -> None: pystac.write_file(catalog, dest_href=dest_href) self.assertTrue(os.path.exists(dest_href), msg="File was not written.") - def test_read_write_item_collection(self) -> None: - item_collection = pystac.read_file( - TestCases.get_path("data-files/item-collection/sample-item-collection.json") - ) - with get_temp_dir() as tmp_dir: - dest_href = os.path.join(tmp_dir, "item-collection.json") - pystac.write_file(item_collection, dest_href=dest_href) - self.assertTrue(os.path.exists(dest_href), msg="File was not written.") - - def test_write_item_collection_needs_href(self) -> None: - item_collection = pystac.read_file( - TestCases.get_path("data-files/item-collection/sample-item-collection.json") - ) - with self.assertRaises(pystac.STACError): - pystac.write_file(item_collection) + def test_read_item_collection_raises_exception(self) -> None: + with self.assertRaises(pystac.STACTypeError): + _ = pystac.read_file( + TestCases.get_path( + "data-files/item-collection/sample-item-collection.json" + ) + ) diff --git a/tests/validation/test_validate.py b/tests/validation/test_validate.py index 7e1ea6af1..c6bf68444 100644 --- a/tests/validation/test_validate.py +++ b/tests/validation/test_validate.py @@ -1,7 +1,7 @@ from datetime import datetime import json import os -from typing import Any, Dict, cast +from typing import Any, Dict from pystac.utils import get_opt import shutil import unittest @@ -20,7 +20,6 @@ def test_validate_current_version(self) -> None: catalog = pystac.read_file( TestCases.get_path("data-files/catalogs/test-case-1/" "catalog.json") ) - catalog = cast(pystac.STACObject, catalog) catalog.validate() collection = pystac.read_file( @@ -30,11 +29,9 @@ def test_validate_current_version(self) -> None: "collection.json" ) ) - collection = cast(pystac.Collection, collection) collection.validate() item = pystac.read_file(TestCases.get_path("data-files/item/sample-item.json")) - item = cast(pystac.Item, item) item.validate() def test_validate_examples(self) -> None: From 47017df9a8a10874e19ee6363dbebb7282c62333 Mon Sep 17 00:00:00 2001 From: Jon Duckworth Date: Mon, 14 Jun 2021 10:56:38 -0400 Subject: [PATCH 14/14] Remove ITEMCOLLECTION from STACObjectTypes --- pystac/item_collection.py | 28 ++++++++++- pystac/serialization/identify.py | 72 ++-------------------------- pystac/serialization/migrate.py | 10 ---- pystac/stac_object.py | 1 - pystac/validation/schema_uri_map.py | 7 --- tests/serialization/test_identify.py | 36 -------------- tests/serialization/test_migrate.py | 6 --- tests/test_item_collection.py | 24 ++++++++++ 8 files changed, 56 insertions(+), 128 deletions(-) diff --git a/pystac/item_collection.py b/pystac/item_collection.py index e9fdab107..33cab9c6a 100644 --- a/pystac/item_collection.py +++ b/pystac/item_collection.py @@ -140,7 +140,7 @@ def from_dict(cls, d: Dict[str, Any]) -> "ItemCollection": Arguments: d : The dictionary from which the :class:`~ItemCollection` will be created """ - if identify_stac_object_type(d) != pystac.STACObjectType.ITEMCOLLECTION: + if not cls.is_item_collection(d): raise STACTypeError("Dict is not a valid ItemCollection") items = [pystac.Item.from_dict(item) for item in d.get("features", [])] @@ -184,3 +184,29 @@ def save_object( stac_io = pystac.StacIO.default() stac_io.save_json(dest_href, self.to_dict()) + + @staticmethod + def is_item_collection(d: Dict[str, Any]) -> bool: + """Checks if the given dictionary represents a valid :class:`ItemCollection`. + + Args: + d : Dictionary to check + """ + typ = d.get("type") + + # All ItemCollections are GeoJSON FeatureCollections + if typ != "FeatureCollection": + return False + + # If it is a FeatureCollection and has a "stac_version" field, then it is an + # ItemCollection. This will cover ItemCollections from STAC 0.9 to + # <1.0.0-beta.1, when ItemCollections were removed from the core STAC Spec + if "stac_version" in d: + return True + + # Prior to STAC 0.9 ItemCollections did not have a stac_version field and could + # only be identified by the fact that all of their 'features' are STAC Items. + return all( + identify_stac_object_type(feature) == pystac.STACObjectType.ITEM + for feature in d.get("features", []) + ) diff --git a/pystac/serialization/identify.py b/pystac/serialization/identify.py index b1001dd01..415b02056 100644 --- a/pystac/serialization/identify.py +++ b/pystac/serialization/identify.py @@ -1,6 +1,6 @@ from enum import Enum from functools import total_ordering -from typing import Any, Dict, List, Optional, Set, TYPE_CHECKING, Union, cast +from typing import Any, Dict, Optional, Set, TYPE_CHECKING, Union import pystac from pystac.version import STACVersion @@ -165,44 +165,6 @@ def __repr__(self) -> str: ) -def _identify_stac_extensions( - object_type: str, d: Dict[str, Any], version_range: STACVersionRange -) -> List[str]: - """Identifies extensions for STAC Objects that don't list their - extensions in a 'stac_extensions' property. - - Returns a list of stac_extensions. May mutate the version_range to update - min or max version. - """ - stac_extensions: Set[str] = set([]) - - # assets (collection assets) - - if object_type == pystac.STACObjectType.ITEMCOLLECTION: - if "assets" in d: - stac_extensions.add("assets") - version_range.set_min(STACVersionID("0.8.0")) - - # checksum - if "links" in d: - for link in d["links"]: - link_props = cast(Dict[str, Any], link).keys() - - if any(prop.startswith("checksum:") for prop in link_props): - stac_extensions.add(OldExtensionShortIDs.CHECKSUM.value) - version_range.set_min(STACVersionID("0.6.2")) - - # Single File STAC - if object_type == pystac.STACObjectType.ITEMCOLLECTION: - if "collections" in d: - stac_extensions.add(OldExtensionShortIDs.SINGLE_FILE_STAC.value) - version_range.set_min(STACVersionID("0.8.0")) - if "stac_extensions" not in d: - version_range.set_max(STACVersionID("0.8.1")) - - return list(stac_extensions) - - def identify_stac_object_type( json_dict: Dict[str, Any] ) -> Optional["STACObjectType_Type"]: @@ -227,15 +189,11 @@ def identify_stac_object_type( obj_type = json_dict.get("type") - # For pre-1.0 objects for version 0.8.* or later 'stac_version' must be present, - # except for in ItemCollections (which are handled in the else clause) + # For pre-1.0 objects for version 0.8.* or later 'stac_version' must be present if "stac_version" in json_dict: # Pre-1.0 STAC objects with 'type' == "Feature" are Items if obj_type == "Feature": return pystac.STACObjectType.ITEM - # Pre-1.0 STAC objects with 'type' == "FeatureCollection" are ItemCollections - if obj_type == "FeatureCollection": - return pystac.STACObjectType.ITEMCOLLECTION # Anything else with a 'type' field is not a STAC object if obj_type is not None: return None @@ -246,16 +204,8 @@ def identify_stac_object_type( # Everything else that has a stac_version is a Catalog else: return pystac.STACObjectType.CATALOG - else: - # Prior to STAC 0.9 ItemCollections did not have a stac_version field and could - # only be identified by the fact that all of their 'features' are STAC Items - if obj_type == "FeatureCollection": - if all( - identify_stac_object_type(feat) == pystac.STACObjectType.ITEM - for feat in json_dict.get("features", []) - ): - return pystac.STACObjectType.ITEMCOLLECTION - return None + + return None def identify_stac_object(json_dict: Dict[str, Any]) -> STACJSONDescription: @@ -287,19 +237,7 @@ def identify_stac_object(json_dict: Dict[str, Any]) -> STACJSONDescription: version_range.set_min(STACVersionID("0.8.0")) if stac_extensions is None: - # If this is post-0.8, we can assume there are no extensions - # if the stac_extensions property doesn't exist for everything - # but ItemCollection (except after 0.9.0, when ItemCollection also got - # the stac_extensions property). - if ( - object_type == pystac.STACObjectType.ITEMCOLLECTION - and not version_range.is_later_than("0.8.1") - ): - stac_extensions = _identify_stac_extensions( - object_type, json_dict, version_range - ) - else: - stac_extensions = [] + stac_extensions = [] # Between 1.0.0-beta.2 and 1.0.0-RC1, STAC extensions changed from # being split between 'common' and custom extensions, with common diff --git a/pystac/serialization/migrate.py b/pystac/serialization/migrate.py index 6b38cabea..86760cd7a 100644 --- a/pystac/serialization/migrate.py +++ b/pystac/serialization/migrate.py @@ -33,16 +33,7 @@ def _migrate_item( pass -def _migrate_itemcollection( - d: Dict[str, Any], version: STACVersionID, info: STACJSONDescription -) -> None: - if version < "0.9.0": - d["stac_extensions"] = list(info.extensions) - - # Extensions - - def _migrate_item_assets( d: Dict[str, Any], version: STACVersionID, info: STACJSONDescription ) -> Optional[Set[str]]: @@ -83,7 +74,6 @@ def _get_object_migrations() -> Dict[ pystac.STACObjectType.CATALOG: _migrate_catalog, pystac.STACObjectType.COLLECTION: _migrate_collection, pystac.STACObjectType.ITEM: _migrate_item, - pystac.STACObjectType.ITEMCOLLECTION: _migrate_itemcollection, } diff --git a/pystac/stac_object.py b/pystac/stac_object.py index ca996cef3..e40c98443 100644 --- a/pystac/stac_object.py +++ b/pystac/stac_object.py @@ -18,7 +18,6 @@ def __str__(self) -> str: CATALOG = "CATALOG" COLLECTION = "COLLECTION" ITEM = "ITEM" - ITEMCOLLECTION = "ITEMCOLLECTION" class STACObject(ABC): diff --git a/pystac/validation/schema_uri_map.py b/pystac/validation/schema_uri_map.py index 1ad488731..2003b89a8 100644 --- a/pystac/validation/schema_uri_map.py +++ b/pystac/validation/schema_uri_map.py @@ -71,13 +71,6 @@ class DefaultSchemaUriMap(SchemaUriMap): None, ), STACObjectType.ITEM: ("item-spec/json-schema/item.json", None), - STACObjectType.ITEMCOLLECTION: ( - None, - [ - STACVersionRange(min_version="v0.8.0-rc1", max_version="0.9.0"), - "item-spec/json-schema/itemcollection.json", - ], - ), } @classmethod diff --git a/tests/serialization/test_identify.py b/tests/serialization/test_identify.py index b48fa5227..8971aaeb9 100644 --- a/tests/serialization/test_identify.py +++ b/tests/serialization/test_identify.py @@ -68,47 +68,11 @@ def test_identify_non_stac_raises_error(self) -> None: self.assertIn("JSON does not represent a STAC object", str(ctx.exception)) - def test_identify_0_8_itemcollection_type(self) -> None: - itemcollection_path = TestCases.get_path( - "data-files/examples/0.8.1/item-spec/" - "examples/itemcollection-sample-full.json" - ) - itemcollection_dict = pystac.StacIO.default().read_json(itemcollection_path) - - self.assertEqual( - identify_stac_object_type(itemcollection_dict), - pystac.STACObjectType.ITEMCOLLECTION, - ) - - def test_identify_0_9_itemcollection(self) -> None: - itemcollection_path = TestCases.get_path( - "data-files/examples/0.9.0/item-spec/" - "examples/itemcollection-sample-full.json" - ) - itemcollection_dict = pystac.StacIO.default().read_json(itemcollection_path) - - self.assertEqual( - identify_stac_object_type(itemcollection_dict), - pystac.STACObjectType.ITEMCOLLECTION, - ) - def test_identify_invalid_with_stac_version(self) -> None: not_stac = {"stac_version": "0.9.0", "type": "Custom"} self.assertIsNone(identify_stac_object_type(not_stac)) - def test_identify_0_8_itemcollection(self) -> None: - itemcollection_path = TestCases.get_path( - "data-files/examples/0.8.1/item-spec/" - "examples/itemcollection-sample-full.json" - ) - itemcollection_dict = pystac.StacIO.default().read_json(itemcollection_path) - - actual = identify_stac_object(itemcollection_dict) - - self.assertEqual(actual.object_type, pystac.STACObjectType.ITEMCOLLECTION) - self.assertTrue(actual.version_range.contains("0.8.1")) - class VersionTest(unittest.TestCase): def test_version_ordering(self) -> None: diff --git a/tests/serialization/test_migrate.py b/tests/serialization/test_migrate.py index 2c78180bc..1fe917cee 100644 --- a/tests/serialization/test_migrate.py +++ b/tests/serialization/test_migrate.py @@ -49,12 +49,6 @@ def test_migrate(self) -> None: e_id.endswith(".json"), f"{e_id} is not a JSON schema URI" ) - # Test that PySTAC can read it without errors. - if info.object_type != pystac.STACObjectType.ITEMCOLLECTION: - self.assertIsInstance( - pystac.read_dict(migrated_d, href=path), pystac.STACObject - ) - def test_migrates_removed_extension(self) -> None: item = pystac.Item.from_file( TestCases.get_path( diff --git a/tests/test_item_collection.py b/tests/test_item_collection.py index 8bdc25213..cec478cd1 100644 --- a/tests/test_item_collection.py +++ b/tests/test_item_collection.py @@ -125,3 +125,27 @@ def test_add_other_raises_error(self) -> None: with self.assertRaises(TypeError): _ = item_collection + 2 + + def test_identify_0_8_itemcollection_type(self) -> None: + itemcollection_path = TestCases.get_path( + "data-files/examples/0.8.1/item-spec/" + "examples/itemcollection-sample-full.json" + ) + itemcollection_dict = pystac.StacIO.default().read_json(itemcollection_path) + + self.assertTrue( + pystac.ItemCollection.is_item_collection(itemcollection_dict), + msg="Did not correctly identify valid STAC 0.8 ItemCollection.", + ) + + def test_identify_0_9_itemcollection(self) -> None: + itemcollection_path = TestCases.get_path( + "data-files/examples/0.9.0/item-spec/" + "examples/itemcollection-sample-full.json" + ) + itemcollection_dict = pystac.StacIO.default().read_json(itemcollection_path) + + self.assertTrue( + pystac.ItemCollection.is_item_collection(itemcollection_dict), + msg="Did not correctly identify valid STAC 0.9 ItemCollection.", + )