diff --git a/.coveragerc b/.coveragerc index 3ca2751a9..9dfac6599 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,5 +1,5 @@ [report] -fail_under = 90 +fail_under = 91 [run] source = pystac diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e423a8b7..86bb0ed87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ STAC Items ([#430](https://github.com/stac-utils/pystac/pull/430)) - Support for Python 3.9 ([#420](https://github.com/stac-utils/pystac/pull/420)) - Migration for pre-1.0.0-rc.1 Stats Objects (renamed to Range Objects in 1.0.0-rc.3) ([#447](https://github.com/stac-utils/pystac/pull/447)) +- Attempting to extend a `STACObject` that does not contain the extension's schema URI in + `stac_extensions` raises new `ExtensionNotImplementedError` ([#450](https://github.com/stac-utils/pystac/pull/450)) ### Changed @@ -20,6 +22,9 @@ `StacIO.read_text` ([#433](https://github.com/stac-utils/pystac/pull/433)) - `FileExtension` updated to work with File Info Extension v2.0.0 ([#442](https://github.com/stac-utils/pystac/pull/442)) - `FileExtension` only operates on `pystac.Asset` instances ([#442](https://github.com/stac-utils/pystac/pull/442)) +- `*Extension.ext` methods now have an optional `add_if_missing` argument, which will + add the extension schema URI to the object's `stac_extensions` list if it is not + present ([#450](https://github.com/stac-utils/pystac/pull/450)) ### Fixed @@ -377,4 +382,3 @@ Initial release. [v0.3.2]: [v0.3.1]: [v0.3.0]: - diff --git a/docs/api.rst b/docs/api.rst index 08ef5fe11..91469842b 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -414,6 +414,43 @@ AssetProjectionExtension :members: :show-inheritance: +Raster Extension +---------------- + +DataType +~~~~~~~~ + +.. autoclass:: pystac.extensions.raster.DataType + :members: + :undoc-members: + :show-inheritance: + +Statistics +~~~~~~~~~~ + +.. autoclass:: pystac.extensions.raster.Statistics + :members: + +Histogram +~~~~~~~~~ + +.. autoclass:: pystac.extensions.raster.Histogram + :members: + +RasterBand +~~~~~~~~~~ + +.. autoclass:: pystac.extensions.raster.RasterBand + :members: + +RasterExtension +~~~~~~~~~~~~~~~ + +.. autoclass:: pystac.extensions.raster.RasterExtension + :members: + :show-inheritance: + :inherited-members: + Scientific Extension -------------------- diff --git a/docs/contributing.rst b/docs/contributing.rst index 66335b810..aa1cba281 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -62,7 +62,7 @@ example, to format all the Python code, run ``pre-commit run --all-files black`` You can also install a Git pre-commit hook which will run the relevant linters and formatters on any staged code when committing. This will be much faster than running on -all files, which is usually[#]_ only required when changing the pre-commit version or +all files, which is usually [#]_ only required when changing the pre-commit version or configuration. Once installed you can bypass this check by adding the ``--no-verify`` flag to Git commit commands, as in ``git commit --no-verify``. diff --git a/pystac/__init__.py b/pystac/__init__.py index 19732af9b..749fcfb63 100644 --- a/pystac/__init__.py +++ b/pystac/__init__.py @@ -7,6 +7,7 @@ STACError, STACTypeError, ExtensionAlreadyExistsError, + ExtensionNotImplemented, ExtensionTypeError, RequiredPropertyMissing, STACValidationError, diff --git a/pystac/errors.py b/pystac/errors.py index 488ae781b..a5eae5df4 100644 --- a/pystac/errors.py +++ b/pystac/errors.py @@ -35,6 +35,11 @@ class ExtensionAlreadyExistsError(Exception): pass +class ExtensionNotImplemented(Exception): + """Attempted to extend a STAC object that does not implement the given + extension.""" + + class RequiredPropertyMissing(Exception): """This error is raised when a required value was expected to be there but was missing or None. This will happen, for example, diff --git a/pystac/extensions/base.py b/pystac/extensions/base.py index 8f496dcf0..d33707ffa 100644 --- a/pystac/extensions/base.py +++ b/pystac/extensions/base.py @@ -1,7 +1,7 @@ from abc import ABC, abstractmethod from typing import Generic, Iterable, List, Optional, Dict, Any, Type, TypeVar, Union -from pystac import Collection, RangeSummary, STACObject, Summaries +import pystac class SummariesExtension: @@ -12,16 +12,16 @@ class SummariesExtension: extension-specific class that inherits from this class and instantiate that. See :class:`~pystac.extensions.eo.SummariesEOExtension` for an example.""" - summaries: Summaries + summaries: pystac.Summaries """The summaries for the :class:`~pystac.Collection` being extended.""" - def __init__(self, collection: Collection) -> None: + def __init__(self, collection: pystac.Collection) -> None: self.summaries = collection.summaries def _set_summary( self, prop_key: str, - v: Optional[Union[List[Any], RangeSummary[Any], Dict[str, Any]]], + v: Optional[Union[List[Any], pystac.RangeSummary[Any], Dict[str, Any]]], ) -> None: if v is None: self.summaries.remove(prop_key) @@ -77,7 +77,7 @@ def _set_property( self.properties[prop_name] = v -S = TypeVar("S", bound=STACObject) +S = TypeVar("S", bound=pystac.STACObject) class ExtensionManagementMixin(Generic[S], ABC): @@ -124,3 +124,17 @@ def has_extension(cls, obj: S) -> bool: obj.stac_extensions is not None and cls.get_schema_uri() in obj.stac_extensions ) + + @classmethod + def validate_has_extension(cls, obj: Union[S, pystac.Asset]) -> None: + """Given a :class:`~pystac.STACObject` or :class:`pystac.Asset` instance, checks + if the object (or its owner in the case of an Asset) has this extension's schema + URI in it's :attr:`~pystac.STACObject.stac_extensions` list.""" + extensible = obj.owner if isinstance(obj, pystac.Asset) else obj + if ( + extensible is not None + and cls.get_schema_uri() not in extensible.stac_extensions + ): + raise pystac.ExtensionNotImplemented( + f"Could not find extension schema URI {cls.get_schema_uri()} in object." + ) diff --git a/pystac/extensions/datacube.py b/pystac/extensions/datacube.py index 414400cf9..e43b8de4c 100644 --- a/pystac/extensions/datacube.py +++ b/pystac/extensions/datacube.py @@ -337,13 +337,22 @@ def dimensions(self, v: Dict[str, Dimension]) -> None: def get_schema_uri(cls) -> str: return SCHEMA_URI - @staticmethod - def ext(obj: T) -> "DatacubeExtension[T]": + @classmethod + def ext(cls, obj: T, add_if_missing: bool = False) -> "DatacubeExtension[T]": if isinstance(obj, pystac.Collection): + if add_if_missing: + cls.add_to(obj) + cls.validate_has_extension(obj) return cast(DatacubeExtension[T], CollectionDatacubeExtension(obj)) if isinstance(obj, pystac.Item): + if add_if_missing: + cls.add_to(obj) + cls.validate_has_extension(obj) return cast(DatacubeExtension[T], ItemDatacubeExtension(obj)) elif isinstance(obj, pystac.Asset): + if add_if_missing and obj.owner is not None: + cls.add_to(obj.owner) + cls.validate_has_extension(obj) return cast(DatacubeExtension[T], AssetDatacubeExtension(obj)) else: raise pystac.ExtensionTypeError( diff --git a/pystac/extensions/eo.py b/pystac/extensions/eo.py index d266296b1..576d1c6ba 100644 --- a/pystac/extensions/eo.py +++ b/pystac/extensions/eo.py @@ -346,8 +346,8 @@ def cloud_cover(self, v: Optional[float]) -> None: def get_schema_uri(cls) -> str: return SCHEMA_URI - @staticmethod - def ext(obj: T) -> "EOExtension[T]": + @classmethod + def ext(cls, obj: T, add_if_missing: bool = False) -> "EOExtension[T]": """Extends the given STAC Object with properties from the :stac-ext:`Electro-Optical Extension `. @@ -359,8 +359,14 @@ def ext(obj: T) -> "EOExtension[T]": pystac.ExtensionTypeError : If an invalid object type is passed. """ if isinstance(obj, pystac.Item): + if add_if_missing: + cls.add_to(obj) + cls.validate_has_extension(obj) return cast(EOExtension[T], ItemEOExtension(obj)) elif isinstance(obj, pystac.Asset): + if add_if_missing and isinstance(obj.owner, pystac.Item): + cls.add_to(obj.owner) + cls.validate_has_extension(obj) return cast(EOExtension[T], AssetEOExtension(obj)) else: raise pystac.ExtensionTypeError( diff --git a/pystac/extensions/file.py b/pystac/extensions/file.py index ebee4f475..deb1df4b0 100644 --- a/pystac/extensions/file.py +++ b/pystac/extensions/file.py @@ -193,13 +193,21 @@ def get_schema_uri(cls) -> str: return SCHEMA_URI @classmethod - def ext(cls, obj: pystac.Asset) -> "FileExtension": + def ext(cls, obj: pystac.Asset, add_if_missing: bool = False) -> "FileExtension": """Extends the given STAC Object with properties from the :stac-ext:`File Info Extension `. This extension can be applied to instances of :class:`~pystac.Asset`. """ - return cls(obj) + if isinstance(obj, pystac.Asset): + if add_if_missing and isinstance(obj.owner, pystac.Item): + cls.add_to(obj.owner) + cls.validate_has_extension(obj) + return cls(obj) + else: + raise pystac.ExtensionTypeError( + f"File Info extension does not apply to type {type(obj)}" + ) class FileExtensionHooks(ExtensionHooks): diff --git a/pystac/extensions/item_assets.py b/pystac/extensions/item_assets.py index 5b362017d..f85451c0f 100644 --- a/pystac/extensions/item_assets.py +++ b/pystac/extensions/item_assets.py @@ -117,8 +117,17 @@ def get_schema_uri(cls) -> str: return SCHEMA_URI @classmethod - def ext(cls, collection: pystac.Collection) -> "ItemAssetsExtension": - return cls(collection) + def ext( + cls, obj: pystac.Collection, add_if_missing: bool = False + ) -> "ItemAssetsExtension": + if isinstance(obj, pystac.Collection): + if add_if_missing: + cls.add_to(obj) + return cls(obj) + else: + raise pystac.ExtensionTypeError( + f"Item Assets extension does not apply to type {type(obj)}" + ) class ItemAssetsExtensionHooks(ExtensionHooks): diff --git a/pystac/extensions/label.py b/pystac/extensions/label.py index 9e3f98431..745d991d7 100644 --- a/pystac/extensions/label.py +++ b/pystac/extensions/label.py @@ -639,13 +639,21 @@ def get_schema_uri(cls) -> str: return SCHEMA_URI @classmethod - def ext(cls, obj: pystac.Item) -> "LabelExtension": + def ext(cls, obj: pystac.Item, add_if_missing: bool = False) -> "LabelExtension": """Extends the given STAC Object with properties from the :stac-ext:`Label Extension