diff --git a/docs/api.rst b/docs/api.rst index b434b9190..8f4d75bef 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -294,6 +294,106 @@ ExtensionManagementMixin :members: :show-inheritance: +Datacube Extension +------------------ + +These classes are representations of the :stac-ext:`EO Extension Spec `. + +DimensionType +~~~~~~~~~~~~~ + +.. autoclass:: pystac.extensions.datacube.DimensionType + :members: + :undoc-members: + :show-inheritance: + +HorizontalSpatialDimensionAxis +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. autoclass:: pystac.extensions.datacube.HorizontalSpatialDimensionAxis + :members: + :undoc-members: + :show-inheritance: + +VerticalSpatialDimensionAxis +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. autoclass:: pystac.extensions.datacube.VerticalSpatialDimensionAxis + :members: + :undoc-members: + :show-inheritance: + +Dimension +~~~~~~~~~ + +.. autoclass:: pystac.extensions.datacube.Dimension + :members: + :show-inheritance: + +HorizontalSpatialDimension +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. autoclass:: pystac.extensions.datacube.HorizontalSpatialDimension + :members: + :show-inheritance: + :inherited-members: + +VerticalSpatialDimension +~~~~~~~~~~~~~~~~~~~~~~~~ + +.. autoclass:: pystac.extensions.datacube.VerticalSpatialDimension + :members: + :show-inheritance: + :inherited-members: + +TemporalSpatialDimension +~~~~~~~~~~~~~~~~~~~~~~~~ + +.. autoclass:: pystac.extensions.datacube.TemporalSpatialDimension + :members: + :show-inheritance: + :inherited-members: + +AdditionalDimension +~~~~~~~~~~~~~~~~~~~ + +.. autoclass:: pystac.extensions.datacube.AdditionalDimension + :members: + :show-inheritance: + :inherited-members: + +DatacubeExtension +~~~~~~~~~~~~~~~~~ + +.. autoclass:: pystac.extensions.datacube.DatacubeExtension + :members: + :show-inheritance: + :inherited-members: + +CollectionDatacubeExtension +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. autoclass:: pystac.extensions.datacube.CollectionDatacubeExtension + :members: + :show-inheritance: + :inherited-members: + +ItemDatacubeExtension +~~~~~~~~~~~~~~~~~~~~~ + +.. autoclass:: pystac.extensions.datacube.ItemDatacubeExtension + :members: + :show-inheritance: + :inherited-members: + +AssetDatacubeExtension +~~~~~~~~~~~~~~~~~~~~~~ + +.. autoclass:: pystac.extensions.datacube.AssetDatacubeExtension + :members: + :show-inheritance: + :inherited-members: + Electro-Optical Extension ------------------------- diff --git a/pystac/extensions/datacube.py b/pystac/extensions/datacube.py index 064d152e1..fa51f61b1 100644 --- a/pystac/extensions/datacube.py +++ b/pystac/extensions/datacube.py @@ -4,6 +4,7 @@ """ from abc import ABC +from enum import Enum from typing import Any, Dict, Generic, List, Optional, TypeVar, Union, cast import pystac @@ -18,7 +19,8 @@ SCHEMA_URI = "https://stac-extensions.github.io/datacube/v1.0.0/schema.json" -DIMENSIONS_PROP = "cube:dimensions" +PREFIX: str = "cube:" +DIMENSIONS_PROP = PREFIX + "dimensions" # Dimension properties DIM_TYPE_PROP = "type" @@ -31,24 +33,60 @@ DIM_UNIT_PROP = "unit" +class DimensionType(str, Enum): + """Dimension object types for spatial and temporal Dimension Objects.""" + + SPATIAL = "spatial" + TEMPORAL = "temporal" + + +class HorizontalSpatialDimensionAxis(str, Enum): + """Allowed values for ``axis`` field of :class:`HorizontalSpatialDimension` + object.""" + + X = "x" + Y = "y" + + +class VerticalSpatialDimensionAxis(str, Enum): + """Allowed values for ``axis`` field of :class:`VerticalSpatialDimension` + object.""" + + Z = "z" + + class Dimension(ABC): + """Object representing a dimension of the datacube. The fields contained in + Dimension Object vary by ``type``. See the :stac-ext:`Datacube Dimension Object + ` docs for details. + """ + properties: Dict[str, Any] def __init__(self, properties: Dict[str, Any]) -> None: self.properties = properties @property - def dim_type(self) -> str: + def dim_type(self) -> Union[DimensionType, str]: + """The type of the dimension. Must be ``"spatial"`` for :stac-ext:`Horizontal + Spatial Dimension Objects ` or + :stac-ext:`Vertical Spatial Dimension Objects + `, and ``"temporal"`` for + :stac-ext:`Temporal Dimension Objects `. May + be an arbitrary string for :stac-ext:`Additional Dimension Objects + `.""" return get_required( self.properties.get(DIM_TYPE_PROP), "cube:dimension", DIM_TYPE_PROP ) @dim_type.setter - def dim_type(self, v: str) -> None: + def dim_type(self, v: Union[DimensionType, str]) -> None: self.properties[DIM_TYPE_PROP] = v @property def description(self) -> Optional[str]: + """Detailed multi-line description to explain the dimension. `CommonMark 0.29 + `__ syntax MAY be used for rich text representation.""" return self.properties.get(DIM_DESC_PROP) @description.setter @@ -66,7 +104,7 @@ def from_dict(d: Dict[str, Any]) -> "Dimension": dim_type: str = get_required( d.get(DIM_TYPE_PROP), "cube_dimension", DIM_TYPE_PROP ) - if dim_type == "spatial": + if dim_type == DimensionType.SPATIAL: axis: str = get_required( d.get(DIM_AXIS_PROP), "cube_dimension", DIM_AXIS_PROP ) @@ -74,7 +112,7 @@ def from_dict(d: Dict[str, Any]) -> "Dimension": return VerticalSpatialDimension(d) else: return HorizontalSpatialDimension(d) - elif dim_type == "temporal": + elif dim_type == DimensionType.TEMPORAL: # The v1.0.0 spec says that AdditionalDimensions can have # type 'temporal', but it is unclear how to differentiate that # from a temporal dimension. Just key off of type for now. @@ -86,17 +124,20 @@ def from_dict(d: Dict[str, Any]) -> "Dimension": class HorizontalSpatialDimension(Dimension): @property - def axis(self) -> str: + def axis(self) -> HorizontalSpatialDimensionAxis: + """Axis of the spatial dimension. Must be one of ``"x"`` or ``"y"``.""" return get_required( self.properties.get(DIM_AXIS_PROP), "cube:dimension", DIM_AXIS_PROP ) @axis.setter - def axis(self, v: str) -> None: + def axis(self, v: HorizontalSpatialDimensionAxis) -> None: self.properties[DIM_TYPE_PROP] = v @property def extent(self) -> List[float]: + """Extent (lower and upper bounds) of the dimension as two-dimensional array. + Open intervals with ``None`` are not allowed.""" return get_required( self.properties.get(DIM_EXTENT_PROP), "cube:dimension", DIM_EXTENT_PROP ) @@ -107,6 +148,7 @@ def extent(self, v: List[float]) -> None: @property def values(self) -> Optional[List[float]]: + """Optional set of all potential values.""" return self.properties.get(DIM_VALUES_PROP) @values.setter @@ -118,6 +160,7 @@ def values(self, v: Optional[List[float]]) -> None: @property def step(self) -> Optional[float]: + """The space between the values. Use ``None`` for irregularly spaced steps.""" return self.properties.get(DIM_STEP_PROP) @step.setter @@ -132,6 +175,11 @@ def clear_step(self) -> None: @property def reference_system(self) -> Optional[Union[str, float, Dict[str, Any]]]: + """The spatial reference system for the data, specified as `numerical EPSG code + `__, `WKT2 (ISO 19162) string + `__ or `PROJJSON + object `__. + Defaults to EPSG code 4326.""" return self.properties.get(DIM_REF_SYS_PROP) @reference_system.setter @@ -144,17 +192,22 @@ def reference_system(self, v: Optional[Union[str, float, Dict[str, Any]]]) -> No class VerticalSpatialDimension(Dimension): @property - def axis(self) -> str: + def axis(self) -> VerticalSpatialDimensionAxis: + """Axis of the spatial dimension. Always ``"z"``.""" return get_required( self.properties.get(DIM_AXIS_PROP), "cube:dimension", DIM_AXIS_PROP ) @axis.setter - def axis(self, v: str) -> None: + def axis(self, v: VerticalSpatialDimensionAxis) -> None: self.properties[DIM_TYPE_PROP] = v @property def extent(self) -> Optional[List[Optional[float]]]: + """If the dimension consists of `ordinal + `__ values, + the extent (lower and upper bounds) of the values as two-dimensional array. Use + null for open intervals.""" return self.properties.get(DIM_EXTENT_PROP) @extent.setter @@ -166,6 +219,9 @@ def extent(self, v: Optional[List[Optional[float]]]) -> None: @property def values(self) -> Optional[Union[List[float], List[str]]]: + """A set of all potential values, especially useful for `nominal + `__ values.""" + return self.properties.get(DIM_VALUES_PROP) @values.setter @@ -177,6 +233,9 @@ def values(self, v: Optional[Union[List[float], List[str]]]) -> None: @property def step(self) -> Optional[float]: + """If the dimension consists of `interval + `__ values, + the space between the values. Use null for irregularly spaced steps.""" return self.properties.get(DIM_STEP_PROP) @step.setter @@ -191,6 +250,8 @@ def clear_step(self) -> None: @property def unit(self) -> Optional[str]: + """The unit of measurement for the data, preferably compliant to `UDUNITS-2 + `__ units (singular).""" return self.properties.get(DIM_UNIT_PROP) @unit.setter @@ -202,6 +263,11 @@ def unit(self, v: Optional[str]) -> None: @property def reference_system(self) -> Optional[Union[str, float, Dict[str, Any]]]: + """The spatial reference system for the data, specified as `numerical EPSG code + `__, `WKT2 (ISO 19162) string + `__ or `PROJJSON + object `__. + Defaults to EPSG code 4326.""" return self.properties.get(DIM_REF_SYS_PROP) @reference_system.setter @@ -215,6 +281,10 @@ def reference_system(self, v: Optional[Union[str, float, Dict[str, Any]]]) -> No class TemporalDimension(Dimension): @property def extent(self) -> Optional[List[Optional[str]]]: + """Extent (lower and upper bounds) of the dimension as two-dimensional array. + The dates and/or times must be strings compliant to `ISO 8601 + `__. ``None`` is allowed for open date + ranges.""" return self.properties.get(DIM_EXTENT_PROP) @extent.setter @@ -226,6 +296,9 @@ def extent(self, v: Optional[List[Optional[str]]]) -> None: @property def values(self) -> Optional[List[str]]: + """If the dimension consists of set of specific values they can be listed here. + The dates and/or times must be strings compliant to `ISO 8601 + `__.""" return self.properties.get(DIM_VALUES_PROP) @values.setter @@ -237,6 +310,9 @@ def values(self, v: Optional[List[str]]) -> None: @property def step(self) -> Optional[str]: + """The space between the temporal instances as `ISO 8601 duration + `__, e.g. P1D. Use null for + irregularly spaced steps.""" return self.properties.get(DIM_STEP_PROP) @step.setter @@ -253,6 +329,10 @@ def clear_step(self) -> None: class AdditionalDimension(Dimension): @property def extent(self) -> Optional[List[Optional[float]]]: + """If the dimension consists of `ordinal + `__ values, + the extent (lower and upper bounds) of the values as two-dimensional array. Use + null for open intervals.""" return self.properties.get(DIM_EXTENT_PROP) @extent.setter @@ -264,6 +344,8 @@ def extent(self, v: Optional[List[Optional[float]]]) -> None: @property def values(self) -> Optional[Union[List[str], List[float]]]: + """A set of all potential values, especially useful for `nominal + `__ values.""" return self.properties.get(DIM_VALUES_PROP) @values.setter @@ -275,6 +357,9 @@ def values(self, v: Optional[Union[List[str], List[float]]]) -> None: @property def step(self) -> Optional[float]: + """If the dimension consists of `interval + `__ values, + the space between the values. Use null for irregularly spaced steps.""" return self.properties.get(DIM_STEP_PROP) @step.setter @@ -289,6 +374,8 @@ def clear_step(self) -> None: @property def unit(self) -> Optional[str]: + """The unit of measurement for the data, preferably compliant to `UDUNITS-2 + units `__ (singular).""" return self.properties.get(DIM_UNIT_PROP) @unit.setter @@ -300,6 +387,7 @@ def unit(self, v: Optional[str]) -> None: @property def reference_system(self) -> Optional[Union[str, float, Dict[str, Any]]]: + """The reference system for the data.""" return self.properties.get(DIM_REF_SYS_PROP) @reference_system.setter @@ -315,11 +403,36 @@ class DatacubeExtension( PropertiesExtension, ExtensionManagementMixin[Union[pystac.Collection, pystac.Item]], ): + """An abstract class that can be used to extend the properties of a + :class:`~pystac.Collection`, :class:`~pystac.Item`, or :class:`~pystac.Asset` with + properties from the :stac-ext:`Datacube Extension `. This class is + generic over the type of STAC Object to be extended (e.g. :class:`~pystac.Item`, + :class:`~pystac.Asset`). + + To create a concrete instance of :class:`DatacubeExtension`, use the + :meth:`DatacubeExtension.ext` method. For example: + + .. code-block:: python + + >>> item: pystac.Item = ... + >>> dc_ext = DatacubeExtension.ext(item) + """ + def apply(self, dimensions: Dict[str, Dimension]) -> None: + """Applies label extension properties to the extended + :class:`~pystac.Collection`, :class:`~pystac.Item` or :class:`~pystac.Asset`. + + Args: + bands : A list of available bands where each item is a :class:`~Band` + object. If given, requires at least one band. + dimensions : Dictionary mapping dimension name to a :class:`Dimension` + object. + """ self.dimensions = dimensions @property def dimensions(self) -> Dict[str, Dimension]: + """Dictionary mapping dimension name to a :class:`Dimension` object.""" result = get_required( self._get_property(DIMENSIONS_PROP, Dict[str, Any]), self, DIMENSIONS_PROP ) @@ -335,6 +448,16 @@ def get_schema_uri(cls) -> str: @classmethod def ext(cls, obj: T, add_if_missing: bool = False) -> "DatacubeExtension[T]": + """Extends the given STAC Object with properties from the :stac-ext:`Datacube + Extension `. + + This extension can be applied to instances of :class:`~pystac.Collection`, + :class:`~pystac.Item` or :class:`~pystac.Asset`. + + Raises: + + pystac.ExtensionTypeError : If an invalid object type is passed. + """ if isinstance(obj, pystac.Collection): cls.validate_has_extension(obj, add_if_missing) return cast(DatacubeExtension[T], CollectionDatacubeExtension(obj)) @@ -351,6 +474,14 @@ def ext(cls, obj: T, add_if_missing: bool = False) -> "DatacubeExtension[T]": class CollectionDatacubeExtension(DatacubeExtension[pystac.Collection]): + """A concrete implementation of :class:`DatacubeExtension` on an + :class:`~pystac.Collection` that extends the properties of the Item to include + properties defined in the :stac-ext:`Datacube Extension `. + + This class should generally not be instantiated directly. Instead, call + :meth:`DatacubeExtension.ext` on an :class:`~pystac.Collection` to extend it. + """ + collection: pystac.Collection properties: Dict[str, Any] @@ -363,6 +494,14 @@ def __repr__(self) -> str: class ItemDatacubeExtension(DatacubeExtension[pystac.Item]): + """A concrete implementation of :class:`DatacubeExtension` on an + :class:`~pystac.Item` that extends the properties of the Item to include properties + defined in the :stac-ext:`Datacube Extension `. + + This class should generally not be instantiated directly. Instead, call + :meth:`DatacubeExtension.ext` on an :class:`~pystac.Item` to extend it. + """ + item: pystac.Item properties: Dict[str, Any] @@ -375,6 +514,14 @@ def __repr__(self) -> str: class AssetDatacubeExtension(DatacubeExtension[pystac.Asset]): + """A concrete implementation of :class:`DatacubeExtension` on an + :class:`~pystac.Asset` that extends the Asset fields to include properties defined + in the :stac-ext:`Datacube Extension `. + + This class should generally not be instantiated directly. Instead, call + :meth:`EOExtension.ext` on an :class:`~pystac.Asset` to extend it. + """ + asset_href: str properties: Dict[str, Any] additional_read_properties: Optional[List[Dict[str, Any]]]