Skip to content

Commit

Permalink
Merge pull request #555 from gadomski/issues/499-resolving-self-href
Browse files Browse the repository at this point in the history
Allow resolved self links
  • Loading branch information
Jon Duckworth authored Jul 16, 2021
2 parents 17c52ed + 21feb88 commit 0ba1a1d
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 22 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,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

0 comments on commit 0ba1a1d

Please sign in to comment.