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

Fix STACObject inheritance #451

Merged
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
34 changes: 23 additions & 11 deletions pystac/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,34 +73,40 @@
)


def read_file(href: str) -> STACObject:
def read_file(href: str, stac_io: Optional[StacIO] = None) -> STACObject:
"""Reads a STAC object from a file.

This method will return either a Catalog, a Collection, or an Item based on what the
file contains.
This method will return either a Catalog, a Collection, or an Item based on what
the file contains.

This is a convenience method for :meth:`STACObject.from_file <pystac.STACObject.from_file>`
This is a convenience method for :meth:`StacIO.read_stac_object
<pystac.StacIO.read_stac_object>`

Args:
href : The HREF to read the object from.
stac_io: Optional :class:`~StacIO` instance to use for I/O operations. If not
provided, will use :meth:`StacIO.default` to create an instance.

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
:class:`~pystac.STACObject`. Note that an :class:`~pystac.ItemCollection`
is not a :class:`~pystac.STACObject` and must be read using
:meth:`ItemCollection.from_file <pystac.ItemCollection.from_file>`
"""
return STACObject.from_file(href)
if stac_io is None:
stac_io = StacIO.default()
return stac_io.read_stac_object(href)


def write_file(
obj: STACObject,
include_self_link: bool = True,
dest_href: Optional[str] = None,
stac_io: Optional[StacIO] = None,
) -> None:
"""Writes a STACObject to a file.

Expand All @@ -120,8 +126,14 @@ def write_file(
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.
stac_io: Optional :class:`~StacIO` instance to use for I/O operations. If not
provided, will use :meth:`StacIO.default` to create an instance.
"""
obj.save_object(include_self_link=include_self_link, dest_href=dest_href)
if stac_io is None:
stac_io = StacIO.default()
obj.save_object(
include_self_link=include_self_link, dest_href=dest_href, stac_io=stac_io
)


def read_dict(
Expand All @@ -137,7 +149,7 @@ def read_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 <stac_io.stac_object_from_dict>`.
:meth:`StacIO.stac_object_from_dict <pystac.StacIO.stac_object_from_dict>`.

Args:
d : The dict to parse.
Expand All @@ -151,8 +163,8 @@ def read_dict(

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
:class:`~pystac.STACObject`. Note that an :class:`~pystac.ItemCollection`
is not a :class:`~pystac.STACObject` and must be read using
:meth:`ItemCollection.from_dict <pystac.ItemCollection.from_dict>`
"""
if stac_io is None:
Expand Down
28 changes: 22 additions & 6 deletions pystac/catalog.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os
from copy import deepcopy
from enum import Enum
from pystac.errors import STACTypeError
from typing import (
Any,
Callable,
Expand All @@ -15,14 +16,19 @@
)

import pystac
from pystac.stac_object import STACObject
from pystac.stac_object import STACObject, STACObjectType
from pystac.layout import (
BestPracticesLayoutStrategy,
HrefLayoutStrategy,
LayoutTemplate,
)
from pystac.link import Link
from pystac.cache import ResolvedObjectCache
from pystac.serialization import (
identify_stac_object_type,
identify_stac_object,
migrate_to_latest,
)
from pystac.utils import is_absolute_href, make_absolute_href

if TYPE_CHECKING:
Expand Down Expand Up @@ -902,10 +908,11 @@ def from_dict(
migrate: bool = False,
) -> "Catalog":
if migrate:
result = pystac.read_dict(d, href=href, root=root)
if not isinstance(result, Catalog):
raise pystac.STACError(f"{result} is not a Catalog")
return result
info = identify_stac_object(d)
d = migrate_to_latest(d, info)

if not cls.matches_object_type(d):
raise STACTypeError(f"{d} does not represent a {cls.__name__} instance")

catalog_type = CatalogType.determine_type(d)

Expand All @@ -919,7 +926,7 @@ def from_dict(

d.pop("stac_version")

cat = Catalog(
cat = cls(
id=id,
description=description,
title=title,
Expand All @@ -946,7 +953,16 @@ def full_copy(

@classmethod
def from_file(cls, href: str, stac_io: Optional[pystac.StacIO] = None) -> "Catalog":
if stac_io is None:
stac_io = pystac.StacIO.default()

result = super().from_file(href, stac_io)
if not isinstance(result, Catalog):
raise pystac.STACTypeError(f"{result} is not a {Catalog}.")
result._stac_io = stac_io

return result

@classmethod
def matches_object_type(cls, d: Dict[str, Any]) -> bool:
return identify_stac_object_type(d) == STACObjectType.CATALOG
21 changes: 16 additions & 5 deletions pystac/collection.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from copy import copy, deepcopy
from datetime import datetime as Datetime
from pystac.errors import STACTypeError
from typing import (
Any,
Dict,
Expand All @@ -23,6 +24,11 @@
from pystac.layout import HrefLayoutStrategy
from pystac.link import Link
from pystac.utils import datetime_to_str
from pystac.serialization import (
identify_stac_object_type,
identify_stac_object,
migrate_to_latest,
)
from pystac.summaries import Summaries

if TYPE_CHECKING:
Expand Down Expand Up @@ -583,10 +589,11 @@ def from_dict(
migrate: bool = False,
) -> "Collection":
if migrate:
result = pystac.read_dict(d, href=href, root=root)
if not isinstance(result, Collection):
raise pystac.STACError(f"{result} is not a Catalog")
return result
info = identify_stac_object(d)
d = migrate_to_latest(d, info)

if not cls.matches_object_type(d):
raise STACTypeError(f"{d} does not represent a {cls.__name__} instance")

catalog_type = CatalogType.determine_type(d)

Expand All @@ -610,7 +617,7 @@ def from_dict(

d.pop("stac_version")

collection = Collection(
collection = cls(
id=id,
description=description,
extent=extent,
Expand Down Expand Up @@ -676,3 +683,7 @@ def from_file(
if not isinstance(result, Collection):
raise pystac.STACTypeError(f"{result} is not a {Collection}.")
return result

@classmethod
def matches_object_type(cls, d: Dict[str, Any]) -> bool:
return identify_stac_object_type(d) == STACObjectType.COLLECTION
20 changes: 16 additions & 4 deletions pystac/item.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
from pystac import STACError, STACObjectType
from pystac.asset import Asset
from pystac.link import Link
from pystac.serialization import (
identify_stac_object_type,
identify_stac_object,
migrate_to_latest,
)
from pystac.stac_object import STACObject
from pystac.utils import (
is_absolute_href,
Expand Down Expand Up @@ -912,10 +917,13 @@ def from_dict(
migrate: bool = False,
) -> "Item":
if migrate:
result = pystac.read_dict(d, href=href, root=root)
if not isinstance(result, Item):
raise pystac.STACError(f"{result} is not a Catalog")
return result
info = identify_stac_object(d)
d = migrate_to_latest(d, info)

if not cls.matches_object_type(d):
raise pystac.STACTypeError(
f"{d} does not represent a {cls.__name__} instance"
)

d = deepcopy(d)
id = d.pop("id")
Expand Down Expand Up @@ -980,3 +988,7 @@ def from_file(cls, href: str, stac_io: Optional[pystac.StacIO] = None) -> "Item"
if not isinstance(result, Item):
raise pystac.STACTypeError(f"{result} is not a {Item}.")
return result

@classmethod
def matches_object_type(cls, d: Dict[str, Any]) -> bool:
return identify_stac_object_type(d) == STACObjectType.ITEM
46 changes: 0 additions & 46 deletions pystac/serialization/__init__.py
Original file line number Diff line number Diff line change
@@ -1,54 +1,8 @@
# flake8: noqa
from typing import Any, Dict, Optional, TYPE_CHECKING

import pystac
from pystac.serialization.identify import (
STACVersionRange,
identify_stac_object,
identify_stac_object_type,
)
from pystac.serialization.common_properties import merge_common_properties
from pystac.serialization.migrate import migrate_to_latest

if TYPE_CHECKING:
from pystac.stac_object import STACObject
from pystac.catalog import Catalog


def stac_object_from_dict(
d: Dict[str, Any], href: Optional[str] = None, root: Optional["Catalog"] = None
) -> "STACObject":
"""Determines how to deserialize a dictionary into a STAC object.

Args:
d : The dict to parse.
href : Optional href that is the file location of the object being
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.

Note: This is used internally in StacIO instances to deserialize STAC Objects.
"""
if identify_stac_object_type(d) == pystac.STACObjectType.ITEM:
collection_cache = None
if root is not None:
collection_cache = root._resolved_objects.as_collection_cache()

# Merge common properties in case this is an older STAC object.
merge_common_properties(d, json_href=href, collection_cache=collection_cache)

info = identify_stac_object(d)

d = migrate_to_latest(d, info)

if info.object_type == pystac.STACObjectType.CATALOG:
return pystac.Catalog.from_dict(d, href=href, root=root, migrate=False)

if info.object_type == pystac.STACObjectType.COLLECTION:
return pystac.Collection.from_dict(d, href=href, root=root, migrate=False)

if info.object_type == pystac.STACObjectType.ITEM:
return pystac.Item.from_dict(d, href=href, root=root, migrate=False)

raise pystac.STACTypeError(f"Unknown STAC object type {info.object_type}")
61 changes: 54 additions & 7 deletions pystac/stac_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,12 @@

import pystac
from pystac.utils import safe_urlparse
import pystac.serialization
from pystac.serialization import (
merge_common_properties,
identify_stac_object_type,
identify_stac_object,
migrate_to_latest,
)

# Use orjson if available
try:
Expand Down Expand Up @@ -95,12 +100,31 @@ def stac_object_from_dict(
href: Optional[str] = None,
root: Optional["Catalog_Type"] = None,
) -> "STACObject_Type":
result = pystac.serialization.stac_object_from_dict(d, href, root)
if isinstance(result, pystac.Catalog):
# Set the stac_io instance for usage by io operations
# where this catalog is the root.
if identify_stac_object_type(d) == pystac.STACObjectType.ITEM:
collection_cache = None
if root is not None:
collection_cache = root._resolved_objects.as_collection_cache()

# Merge common properties in case this is an older STAC object.
merge_common_properties(
d, json_href=href, collection_cache=collection_cache
)

info = identify_stac_object(d)
d = migrate_to_latest(d, info)

if info.object_type == pystac.STACObjectType.CATALOG:
result = pystac.Catalog.from_dict(d, href=href, root=root, migrate=False)
result._stac_io = self
return result
return result

if info.object_type == pystac.STACObjectType.COLLECTION:
return pystac.Collection.from_dict(d, href=href, root=root, migrate=False)

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}")

def read_json(
self, source: Union[str, "Link_Type"], *args: Any, **kwargs: Any
Expand Down Expand Up @@ -302,7 +326,30 @@ def stac_object_from_dict(
root: Optional["Catalog_Type"] = None,
) -> "STACObject_Type":
STAC_IO.issue_deprecation_warning()
return pystac.serialization.stac_object_from_dict(d, href, root)
if identify_stac_object_type(d) == pystac.STACObjectType.ITEM:
collection_cache = None
if root is not None:
collection_cache = root._resolved_objects.as_collection_cache()

# Merge common properties in case this is an older STAC object.
merge_common_properties(
d, json_href=href, collection_cache=collection_cache
)

info = identify_stac_object(d)

d = migrate_to_latest(d, info)

if info.object_type == pystac.STACObjectType.CATALOG:
return pystac.Catalog.from_dict(d, href=href, root=root, migrate=False)

if info.object_type == pystac.STACObjectType.COLLECTION:
return pystac.Collection.from_dict(d, href=href, root=root, migrate=False)

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}")

# This is set in __init__.py
_STAC_OBJECT_CLASSES = None
Expand Down
Loading