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

Allow resolved self links #555

Merged
merged 4 commits into from
Jul 16, 2021
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 @@ -17,6 +17,7 @@
### Fixed

- Added `Collections` as a type that can be extended for extensions whose fields can appear in collection summaries ([#547](https://github.com/stac-utils/pystac/pull/547))
- Allow resolved self links when getting an object's self href ([#555](https://github.com/stac-utils/pystac/pull/555))

### Deprecated

Expand Down
81 changes: 61 additions & 20 deletions pystac/link.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from copy import copy
from typing import Any, Dict, Optional, TYPE_CHECKING, Union, cast
from typing import Any, Dict, Optional, TYPE_CHECKING, Union

import pystac
from pystac.utils import make_absolute_href, make_relative_href, is_absolute_href
Expand Down Expand Up @@ -52,11 +52,6 @@ class Link:
"""The relation of the link (e.g. 'child', 'item'). Registered rel Types are
preferred. See :class:`~pystac.RelType` for common media types."""

target: Union[str, "STACObject_Type"]
"""The target of the link. If the link is unresolved, or the link is to something
that is not a STACObject, the target is an HREF. If resolved, the target is a
STACObject."""

media_type: Optional[str]
"""Optional description of the media type. Registered Media Types are preferred.
See :class:`~pystac.MediaType` for common media types."""
Expand All @@ -82,7 +77,12 @@ def __init__(
extra_fields: Optional[Dict[str, Any]] = None,
) -> None:
self.rel = rel
self.target = target
if isinstance(target, str):
self._target_href: Optional[str] = target
self._target_object = None
else:
self._target_href = None
self._target_object = target
self.media_type = media_type
self.title = title
self.extra_fields = extra_fields or {}
Expand Down Expand Up @@ -119,10 +119,10 @@ def get_href(self) -> Optional[str]:
In all other cases, this method will return an absolute HREF.
"""
# get the self href
if self.is_resolved():
href = cast(pystac.STACObject, self.target).get_self_href()
if self._target_object:
href = self._target_object.get_self_href()
else:
href = cast(Optional[str], self.target)
href = self._target_href

if href and is_absolute_href(href) and self.owner and self.owner.get_root():
root = self.owner.get_root()
Expand Down Expand Up @@ -158,16 +158,55 @@ def get_absolute_href(self) -> Optional[str]:
from this link; however, if the link is relative, has no owner,
and has an unresolved target, this will return a relative HREF.
"""
if self.is_resolved():
href = cast(pystac.STACObject, self.target).get_self_href()
if self._target_object:
href = self._target_object.get_self_href()
else:
href = cast(Optional[str], self.target)
href = self._target_href

if href is not None and self.owner is not None:
href = make_absolute_href(href, self.owner.get_self_href())

return href

@property
def target(self) -> Union[str, "STACObject_Type"]:
"""The target of the link. If the link is unresolved, or the link is to something
that is not a STACObject, the target is an HREF. If resolved, the target is a
STACObject."""
if self._target_object:
return self._target_object
elif self._target_href:
return self._target_href
else:
raise ValueError("No target defined for link.")

@target.setter
def target(self, target: Union[str, "STACObject_Type"]) -> None:
"""Sets this link's target to a string or a STAC object."""
if isinstance(target, str):
self._target_href = target
self._target_object = None
else:
self._target_href = None
self._target_object = target

def get_target_str(self) -> Optional[str]:
"""Returns this link's target as a string.

If a string href was provided, returns that. If not, tries to resolve
the self link of the target object.
"""
if self._target_href:
return self._target_href
elif self._target_object:
return self._target_object.get_self_href()
else:
return None

def has_target_href(self) -> bool:
"""Returns true if this link has a string href in its target information."""
return self._target_href is not None

def __repr__(self) -> str:
return "<Link rel={} target={}>".format(self.rel, self.target)

Expand All @@ -180,8 +219,10 @@ def resolve_stac_object(self, root: Optional["Catalog_Type"] = None) -> "Link":
If provided, the root's resolved object cache is used to search for
previously resolved instances of the STAC object.
"""
if isinstance(self.target, str):
target_href = self.target
if self._target_object:
pass
elif self._target_href:
target_href = self._target_href

# If it's a relative link, base it off the parent.
if not is_absolute_href(target_href):
Expand Down Expand Up @@ -221,17 +262,17 @@ def resolve_stac_object(self, root: Optional["Catalog_Type"] = None) -> "Link":
if root is not None:
obj = root._resolved_objects.get_or_cache(obj)
obj.set_root(root)
self._target_object = obj
else:
obj = self.target

self.target = obj
raise ValueError("Cannot resolve STAC object without a target")

if (
self.owner
and self.rel in [pystac.RelType.CHILD, pystac.RelType.ITEM]
and isinstance(self.owner, pystac.Catalog)
):
self.target.set_parent(self.owner)
assert self._target_object
self._target_object.set_parent(self.owner)

return self

Expand All @@ -241,7 +282,7 @@ def is_resolved(self) -> bool:
Returns:
bool: True if this link is resolved.
"""
return not isinstance(self.target, str)
return self._target_object is not None

def to_dict(self) -> Dict[str, Any]:
"""Generate a dictionary representing the JSON of this serialized Link.
Expand Down
4 changes: 2 additions & 2 deletions pystac/stac_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,8 @@ def get_self_href(self) -> Optional[str]:
links have absolute (as opposed to relative) HREFs.
"""
self_link = self.get_single_link(pystac.RelType.SELF)
if self_link:
return cast(str, self_link.target)
if self_link and self_link.has_target_href():
return self_link.get_target_str()
else:
return None

Expand Down
31 changes: 31 additions & 0 deletions tests/test_link.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import datetime
import os.path
import unittest
from tempfile import TemporaryDirectory
from typing import Any, Dict, List

import pystac
Expand Down Expand Up @@ -82,6 +84,35 @@ def test_resolve_stac_object_no_root_and_target_is_item(self) -> None:
link = pystac.Link("my rel", target=self.item)
link.resolve_stac_object()

def test_resolved_self_href(self) -> None:
catalog = pystac.Catalog(id="test", description="test desc")
with TemporaryDirectory() as temporary_directory:
catalog.normalize_and_save(temporary_directory)
path = os.path.join(temporary_directory, "catalog.json")
catalog = pystac.Catalog.from_file(path)
link = catalog.get_single_link(pystac.RelType.SELF)
assert link
link.resolve_stac_object()
self.assertEqual(link.get_absolute_href(), path)

def test_target_getter_setter(self) -> None:
link = pystac.Link("my rel", target="./foo/bar.json")
self.assertEqual(link.target, "./foo/bar.json")
self.assertEqual(link.get_target_str(), "./foo/bar.json")

link.target = self.item
self.assertEqual(link.target, self.item)
self.assertEqual(link.get_target_str(), self.item.get_self_href())

link.target = "./bar/foo.json"
self.assertEqual(link.target, "./bar/foo.json")

def test_get_target_str_no_href(self) -> None:
self.item.remove_links("self")
link = pystac.Link("self", target=self.item)
self.item.add_link(link)
self.assertIsNone(link.get_target_str())


class StaticLinkTest(unittest.TestCase):
def setUp(self) -> None:
Expand Down