Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove structural links #999

Merged
merged 4 commits into from
Feb 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
- Optional `strategy` parameter to `catalog.add_items()` ([#967](https://github.com/stac-utils/pystac/pull/967))
- `start_datetime` and `end_datetime` arguments to the `Item` constructor ([#918](https://github.com/stac-utils/pystac/pull/918))
- `RetryStacIO` ([#986](https://github.com/stac-utils/pystac/pull/986))
- `STACObject.remove_hierarchical_links` and `Link.is_hierarchical` ([#999](https://github.com/stac-utils/pystac/pull/999))

### Removed

Expand Down
13 changes: 13 additions & 0 deletions pystac/link.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

HREF = Union[str, os.PathLike]

#: Hierarchical links provide structure to STAC catalogs.
HIERARCHICAL_LINKS = [
pystac.RelType.ROOT,
pystac.RelType.CHILD,
Expand Down Expand Up @@ -345,6 +346,18 @@ def is_resolved(self) -> bool:
"""
return self._target_object is not None

def is_hierarchical(self) -> bool:
"""Returns true if this link's rel type is hierarchical.

Hierarchical links are used to build relationships in STAC, e.g.
"parent", "child", "item", etc. For a complete list of hierarchical
relation types, see :py:const:`HIERARCHICAL_LINKS`.

Returns:
bool: True if the link's rel type is hierarchical.
"""
return self.rel in HIERARCHICAL_LINKS

def to_dict(self, transform_href: bool = True) -> Dict[str, Any]:
"""Generate a dictionary representing the JSON of this serialized Link.

Expand Down
29 changes: 29 additions & 0 deletions pystac/stac_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,35 @@ def remove_links(self, rel: Union[str, pystac.RelType]) -> None:

self.links = [link for link in self.links if link.rel != rel]

def remove_hierarchical_links(self, add_canonical: bool = False) -> List[Link]:
"""Removes all hierarchical links from this object.

See :py:const:`pystac.link.HIERARCHICAL_LINKS` for a list of all
hierarchical links. If the object has a ``self`` href and
``add_canonical`` is True, a link with ``rel="canonical"`` is added.

Args:
add_canonical : If true, and this item has a ``self`` href, that
href is used to build a ``canonical`` link.

Returns:
List[Link]: All removed links
"""
keep = list()
self_href = self.get_self_href()
if add_canonical and self_href is not None:
keep.append(
Link("canonical", self_href, media_type=pystac.MediaType.GEOJSON)
)
remove = list()
for link in self.links:
if link.is_hierarchical():
remove.append(link)
else:
keep.append(link)
self.links = keep
return remove

def get_single_link(
self,
rel: Optional[Union[str, pystac.RelType]] = None,
Expand Down
15 changes: 11 additions & 4 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
# TODO move all test case code to this file

from datetime import datetime

import pytest

from pystac import Catalog, Collection, Item

from .utils import ARBITRARY_BBOX, ARBITRARY_EXTENT, ARBITRARY_GEOM
from .utils import ARBITRARY_BBOX, ARBITRARY_EXTENT, ARBITRARY_GEOM, TestCases


@pytest.fixture
def test_catalog() -> Catalog:
def catalog() -> Catalog:
return Catalog("test-catalog", "A test catalog")


@pytest.fixture
def test_collection() -> Catalog:
def collection() -> Catalog:
return Collection("test-collection", "A test collection", ARBITRARY_EXTENT)


@pytest.fixture
def test_item() -> Item:
def item() -> Item:
return Item("test-item", ARBITRARY_GEOM, ARBITRARY_BBOX, datetime.now(), {})


@pytest.fixture
def label_catalog() -> Catalog:
return TestCases.case_1()
12 changes: 10 additions & 2 deletions tests/test_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -1444,7 +1444,7 @@ def test_clone(self) -> None:
self.assertIsInstance(cloned_catalog, self.BasicCustomCatalog)


def test_custom_catalog_from_dict(test_catalog: Catalog) -> None:
def test_custom_catalog_from_dict(catalog: Catalog) -> None:
# https://github.com/stac-utils/pystac/issues/862
class CustomCatalog(Catalog):
@classmethod
Expand All @@ -1458,4 +1458,12 @@ def from_dict(
) -> CustomCatalog:
return super().from_dict(d)

_ = CustomCatalog.from_dict(test_catalog.to_dict())
_ = CustomCatalog.from_dict(catalog.to_dict())


@pytest.mark.parametrize("add_canonical", (True, False))
def test_remove_hierarchical_links(label_catalog: Catalog, add_canonical: bool) -> None:
label_catalog.remove_hierarchical_links(add_canonical=add_canonical)
for link in label_catalog.links:
assert not link.is_hierarchical()
assert bool(label_catalog.get_single_link("canonical")) == add_canonical
14 changes: 12 additions & 2 deletions tests/test_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from datetime import datetime
from typing import Any, Dict, Optional

import pytest
from dateutil import tz

import pystac
Expand Down Expand Up @@ -514,7 +515,7 @@ def test_clone(self) -> None:
self.assertIsInstance(cloned_collection, self.BasicCustomCollection)


def test_custom_collection_from_dict(test_collection: Collection) -> None:
def test_custom_collection_from_dict(collection: Collection) -> None:
# https://github.com/stac-utils/pystac/issues/862
class CustomCollection(Collection):
@classmethod
Expand All @@ -528,4 +529,13 @@ def from_dict(
) -> CustomCollection:
return super().from_dict(d)

_ = CustomCollection.from_dict(test_collection.to_dict())
_ = CustomCollection.from_dict(collection.to_dict())


@pytest.mark.parametrize("add_canonical", (True, False))
def test_remove_hierarchical_links(label_catalog: Catalog, add_canonical: bool) -> None:
collection = list(label_catalog.get_all_collections())[0]
collection.remove_hierarchical_links(add_canonical=add_canonical)
for link in collection.links:
assert not link.is_hierarchical()
assert bool(collection.get_single_link("canonical")) == add_canonical
13 changes: 11 additions & 2 deletions tests/test_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,7 @@ def test_clone(self) -> None:
self.assertIsInstance(cloned_asset, self.CustomAsset)


def test_custom_item_from_dict(test_item: Item) -> None:
def test_custom_item_from_dict(item: Item) -> None:
# https://github.com/stac-utils/pystac/issues/862
class CustomItem(Item):
@classmethod
Expand All @@ -436,7 +436,7 @@ def from_dict(
) -> CustomItem:
return super().from_dict(d)

_ = CustomItem.from_dict(test_item.to_dict())
_ = CustomItem.from_dict(item.to_dict())


def test_item_from_dict_raises_useful_error() -> None:
Expand All @@ -455,3 +455,12 @@ def test_item_from_dict_with_missing_type_raises_useful_error() -> None:
item_dict = {"stac_version": "0.8.0", "id": "lalalalala"}
with pytest.raises(pystac.STACTypeError, match="'type' is missing"):
Item.from_dict(item_dict)


@pytest.mark.parametrize("add_canonical", (True, False))
def test_remove_hierarchical_links(label_catalog: Catalog, add_canonical: bool) -> None:
item = list(label_catalog.get_all_items())[0]
item.remove_hierarchical_links(add_canonical=add_canonical)
for link in item.links:
assert not link.is_hierarchical()
assert bool(item.get_single_link("canonical")) == add_canonical
15 changes: 15 additions & 0 deletions tests/test_link.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@
from tempfile import TemporaryDirectory
from typing import Any, Dict, List

import pytest

import pystac
from pystac import Collection, Item, Link
from pystac.link import HIERARCHICAL_LINKS
from tests.utils.test_cases import ARBITRARY_EXTENT

TEST_DATETIME: datetime = datetime(2020, 3, 14, 16, 32)
Expand Down Expand Up @@ -330,3 +333,15 @@ def test_relative_self_link(tmp_path: Path) -> None:
asset_href = read_item.assets["data"].get_absolute_href()
assert asset_href
assert Path(asset_href).exists()


@pytest.mark.parametrize("rel", HIERARCHICAL_LINKS)
def test_is_hierarchical(rel: str) -> None:
assert Link(rel, "a-target").is_hierarchical()


@pytest.mark.parametrize(
"rel", ["canonical", "derived_from", "alternate", "via", "prev", "next", "preview"]
)
def test_is_not_hierarchical(rel: str) -> None:
assert not Link(rel, "a-target").is_hierarchical()