From 43cfdecef1519d930848c60a072d7ccdbf549526 Mon Sep 17 00:00:00 2001 From: Julia Signell Date: Mon, 6 Jan 2025 16:03:43 +0100 Subject: [PATCH] Item assets (#1476) * Move item_assets out of extensions * Move item_assets to dummy file * Move item_assets out of dummy file * Add new `item_assets` access pattern * Export AssetDefinition * Add deprecated warning and update tests * Remove schema uri from stac_extensions on migrate * Update docs * Add some more tests * Remove TYPE_CHECKING * Update changelog --- CHANGELOG.md | 6 + docs/api/item_assets.rst | 7 + docs/api/pystac.rst | 9 + pystac/__init__.py | 2 + pystac/collection.py | 58 ++++++ pystac/extensions/base.py | 8 +- pystac/extensions/classification.py | 19 +- pystac/extensions/datacube.py | 11 +- pystac/extensions/eo.py | 12 +- pystac/extensions/ext.py | 22 ++- pystac/extensions/item_assets.py | 241 ++++------------------- pystac/extensions/pointcloud.py | 11 +- pystac/extensions/projection.py | 11 +- pystac/extensions/raster.py | 17 +- pystac/extensions/sar.py | 11 +- pystac/extensions/sat.py | 11 +- pystac/extensions/storage.py | 11 +- pystac/extensions/table.py | 11 +- pystac/extensions/version.py | 8 +- pystac/extensions/view.py | 11 +- pystac/item_assets.py | 246 ++++++++++++++++++++++++ tests/extensions/test_classification.py | 30 +-- tests/extensions/test_item_assets.py | 118 ------------ tests/extensions/test_raster.py | 11 +- tests/serialization/test_migrate.py | 5 +- tests/test_item_assets.py | 234 ++++++++++++++++++++++ 26 files changed, 712 insertions(+), 429 deletions(-) create mode 100644 docs/api/item_assets.rst create mode 100644 pystac/item_assets.py delete mode 100644 tests/extensions/test_item_assets.py create mode 100644 tests/test_item_assets.py diff --git a/CHANGELOG.md b/CHANGELOG.md index da0df99ef..e1656cea0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,17 @@ ## [Unreleased] + +### Added + +- Top-level `item_assets` dict on `Collection`s ([#1476](https://github.com/stac-utils/pystac/pull/1476)) + ### Changed - Write STAC v1.1.0 ([#1427](https://github.com/stac-utils/pystac/pull/1427)) - Use [uv](https://github.com/astral-sh/uv) for development dependencies and docs ([#1439](https://github.com/stac-utils/pystac/pull/1439)) - Correctly detect absolute file path ref on windows, reflecting change in python 3.13 ([#1475](https://github.com/stac-utils/pystac/pull/14750)) (only effects python 3.13) +- Deprecated `ItemAssetExtension` ([#1476](https://github.com/stac-utils/pystac/pull/1476)) ## [v1.11.0] - 2024-09-26 diff --git a/docs/api/item_assets.rst b/docs/api/item_assets.rst new file mode 100644 index 000000000..501c56420 --- /dev/null +++ b/docs/api/item_assets.rst @@ -0,0 +1,7 @@ +pystac.item_assets +================== + +.. automodule:: pystac.item_assets + :members: + :undoc-members: + :noindex: diff --git a/docs/api/pystac.rst b/docs/api/pystac.rst index 8c6da7abd..d1c676955 100644 --- a/docs/api/pystac.rst +++ b/docs/api/pystac.rst @@ -15,6 +15,7 @@ pystac Summaries Item Asset + ItemAssetDefinition CommonMetadata ItemCollection Link @@ -116,6 +117,14 @@ Asset :members: :undoc-members: +ItemAssetDefinition +------------------- + +.. autoclass:: pystac.ItemAssetDefinition + :members: + :undoc-members: + + CommonMetadata -------------- diff --git a/pystac/__init__.py b/pystac/__init__.py index c029ac283..c2a5b53b1 100644 --- a/pystac/__init__.py +++ b/pystac/__init__.py @@ -33,6 +33,7 @@ "RangeSummary", "Item", "Asset", + "ItemAssetDefinition", "ItemCollection", "Provider", "ProviderRole", @@ -81,6 +82,7 @@ from pystac.summaries import RangeSummary, Summaries from pystac.asset import Asset from pystac.item import Item +from pystac.item_assets import ItemAssetDefinition from pystac.item_collection import ItemCollection from pystac.provider import ProviderRole, Provider from pystac.utils import HREF diff --git a/pystac/collection.py b/pystac/collection.py index 9c0c24b7f..63fd63619 100644 --- a/pystac/collection.py +++ b/pystac/collection.py @@ -20,6 +20,7 @@ from pystac.asset import Asset, Assets from pystac.catalog import Catalog from pystac.errors import DeprecatedWarning, ExtensionNotImplemented, STACTypeError +from pystac.item_assets import ItemAssetDefinition, _ItemAssets from pystac.layout import HrefLayoutStrategy from pystac.link import Link from pystac.provider import Provider @@ -553,6 +554,7 @@ def __init__( self.keywords = keywords self.providers = providers self.summaries = summaries or Summaries.empty() + self._item_assets: _ItemAssets | None = None self.assets = {} if assets is not None: @@ -731,6 +733,62 @@ def get_item(self, id: str, recursive: bool = False) -> Item | None: return super().get_item(id, recursive=recursive) raise e + @property + def item_assets(self) -> dict[str, ItemAssetDefinition]: + """Accessor for `item_assets + `__ + on this collection. + + Example:: + + .. code-block:: python + + >>> print(collection.item_assets) + {'thumbnail': , + 'metadata': , + 'B5': , + 'B6': , + 'B7': , + 'B8': } + >>> collection.item_assets["thumbnail"].title + 'Thumbnail' + + Set attributes on :class:`~pystac.ItemAssetDefinition` objects + + .. code-block:: python + + >>> collection.item_assets["thumbnail"].title = "New Title" + + Add to the ``item_assets`` dict: + + .. code-block:: python + + >>> collection.item_assets["B4"] = { + 'type': 'image/tiff; application=geotiff; profile=cloud-optimized', + 'eo:bands': [{'name': 'B4', 'common_name': 'red'}] + } + >>> collection.item_assets["B4"].owner == collection + True + """ + if self._item_assets is None: + self._item_assets = _ItemAssets(self) + return self._item_assets + + @item_assets.setter + def item_assets( + self, item_assets: dict[str, ItemAssetDefinition | dict[str, Any]] | None + ) -> None: + # clear out the cached value + self._item_assets = None + + if item_assets is None: + self.extra_fields.pop("item_assets") + else: + self.extra_fields["item_assets"] = { + k: v if isinstance(v, dict) else v.to_dict() + for k, v in item_assets.items() + } + def update_extent_from_items(self) -> None: """ Update datetime and bbox based on all items to a single bbox and time window. diff --git a/pystac/extensions/base.py b/pystac/extensions/base.py index 1b3016fe3..241c8381e 100644 --- a/pystac/extensions/base.py +++ b/pystac/extensions/base.py @@ -5,7 +5,6 @@ from abc import ABC, abstractmethod from collections.abc import Iterable from typing import ( - TYPE_CHECKING, Any, Generic, TypeVar, @@ -14,9 +13,6 @@ import pystac -if TYPE_CHECKING: - from pystac.extensions.item_assets import AssetDefinition - VERSION_REGEX = re.compile("/v[0-9].[0-9].*/") @@ -158,7 +154,7 @@ def has_extension(cls, obj: S) -> bool: @classmethod def validate_owner_has_extension( cls, - asset: pystac.Asset | AssetDefinition, + asset: pystac.Asset | pystac.ItemAssetDefinition, add_if_missing: bool = False, ) -> None: """ @@ -190,7 +186,7 @@ def validate_owner_has_extension( @classmethod def ensure_owner_has_extension( cls, - asset_or_link: pystac.Asset | AssetDefinition | pystac.Link, + asset_or_link: pystac.Asset | pystac.ItemAssetDefinition | pystac.Link, add_if_missing: bool = False, ) -> None: """Given an :class:`~pystac.Asset`, checks if the asset's owner has this diff --git a/pystac/extensions/classification.py b/pystac/extensions/classification.py index 5842ab418..1e4d02ed7 100644 --- a/pystac/extensions/classification.py +++ b/pystac/extensions/classification.py @@ -16,7 +16,6 @@ ) import pystac -from pystac.extensions import item_assets from pystac.extensions.base import ( ExtensionManagementMixin, PropertiesExtension, @@ -27,7 +26,7 @@ from pystac.serialization.identify import STACJSONDescription, STACVersionID from pystac.utils import get_required, map_opt -T = TypeVar("T", pystac.Item, pystac.Asset, item_assets.AssetDefinition, RasterBand) +T = TypeVar("T", pystac.Item, pystac.Asset, pystac.ItemAssetDefinition, RasterBand) SCHEMA_URI_PATTERN: str = ( "https://stac-extensions.github.io/classification/v{version}/schema.json" @@ -492,7 +491,7 @@ class ClassificationExtension( """An abstract class that can be used to extend the properties of :class:`~pystac.Item`, :class:`~pystac.Asset`, :class:`~pystac.extension.raster.RasterBand`, or - :class:`~pystac.extension.item_assets.AssetDefinition` with properties from the + :class:`~pystac.ItemAssetDefinition` with properties from the :stac-ext:`Classification Extension `. This class is generic over the type of STAC object being extended. @@ -602,7 +601,7 @@ def ext(cls, obj: T, add_if_missing: bool = False) -> ClassificationExtension[T] This extension can be applied to instances of :class:`~pystac.Item`, :class:`~pystac.Asset`, - :class:`~pystac.extensions.item_assets.AssetDefinition`, or + :class:`~pystac.ItemAssetDefinition`, or :class:`~pystac.extension.raster.RasterBand`. Raises: @@ -614,7 +613,7 @@ def ext(cls, obj: T, add_if_missing: bool = False) -> ClassificationExtension[T] elif isinstance(obj, pystac.Asset): cls.ensure_owner_has_extension(obj, add_if_missing) return cast(ClassificationExtension[T], AssetClassificationExtension(obj)) - elif isinstance(obj, item_assets.AssetDefinition): + elif isinstance(obj, pystac.ItemAssetDefinition): cls.ensure_owner_has_extension(obj, add_if_missing) return cast( ClassificationExtension[T], ItemAssetsClassificationExtension(obj) @@ -665,17 +664,19 @@ def __repr__(self) -> str: class ItemAssetsClassificationExtension( - ClassificationExtension[item_assets.AssetDefinition] + ClassificationExtension[pystac.ItemAssetDefinition] ): properties: dict[str, Any] - asset_defn: item_assets.AssetDefinition + asset_defn: pystac.ItemAssetDefinition - def __init__(self, item_asset: item_assets.AssetDefinition): + def __init__(self, item_asset: pystac.ItemAssetDefinition): self.asset_defn = item_asset self.properties = item_asset.properties def __repr__(self) -> str: - return f" DatacubeExtension[T]: elif isinstance(obj, pystac.Asset): cls.ensure_owner_has_extension(obj, add_if_missing) return cast(DatacubeExtension[T], AssetDatacubeExtension(obj)) - elif isinstance(obj, item_assets.AssetDefinition): + elif isinstance(obj, pystac.ItemAssetDefinition): cls.ensure_owner_has_extension(obj, add_if_missing) return cast(DatacubeExtension[T], ItemAssetsDatacubeExtension(obj)) else: @@ -691,11 +690,11 @@ def __repr__(self) -> str: return f"" -class ItemAssetsDatacubeExtension(DatacubeExtension[item_assets.AssetDefinition]): +class ItemAssetsDatacubeExtension(DatacubeExtension[pystac.ItemAssetDefinition]): properties: dict[str, Any] - asset_defn: item_assets.AssetDefinition + asset_defn: pystac.ItemAssetDefinition - def __init__(self, item_asset: item_assets.AssetDefinition): + def __init__(self, item_asset: pystac.ItemAssetDefinition): self.asset_defn = item_asset self.properties = item_asset.properties diff --git a/pystac/extensions/eo.py b/pystac/extensions/eo.py index 54b6cbcbf..c9628a3c7 100644 --- a/pystac/extensions/eo.py +++ b/pystac/extensions/eo.py @@ -14,7 +14,7 @@ ) import pystac -from pystac.extensions import item_assets, projection, view +from pystac.extensions import projection, view from pystac.extensions.base import ( ExtensionManagementMixin, PropertiesExtension, @@ -25,7 +25,7 @@ from pystac.summaries import RangeSummary from pystac.utils import get_required, map_opt -T = TypeVar("T", pystac.Item, pystac.Asset, item_assets.AssetDefinition) +T = TypeVar("T", pystac.Item, pystac.Asset, pystac.ItemAssetDefinition) SCHEMA_URI: str = "https://stac-extensions.github.io/eo/v1.1.0/schema.json" SCHEMA_URIS: list[str] = [ @@ -409,7 +409,7 @@ def ext(cls, obj: T, add_if_missing: bool = False) -> EOExtension[T]: elif isinstance(obj, pystac.Asset): cls.ensure_owner_has_extension(obj, add_if_missing) return cast(EOExtension[T], AssetEOExtension(obj)) - elif isinstance(obj, item_assets.AssetDefinition): + elif isinstance(obj, pystac.ItemAssetDefinition): cls.ensure_owner_has_extension(obj, add_if_missing) return cast(EOExtension[T], ItemAssetsEOExtension(obj)) else: @@ -536,9 +536,9 @@ def __repr__(self) -> str: return f"" -class ItemAssetsEOExtension(EOExtension[item_assets.AssetDefinition]): +class ItemAssetsEOExtension(EOExtension[pystac.ItemAssetDefinition]): properties: dict[str, Any] - asset_defn: item_assets.AssetDefinition + asset_defn: pystac.ItemAssetDefinition def _get_bands(self) -> list[Band] | None: if BANDS_PROP not in self.properties: @@ -550,7 +550,7 @@ def _get_bands(self) -> list[Band] | None: ) ) - def __init__(self, item_asset: item_assets.AssetDefinition): + def __init__(self, item_asset: pystac.ItemAssetDefinition): self.asset_defn = item_asset self.properties = item_asset.properties diff --git a/pystac/extensions/ext.py b/pystac/extensions/ext.py index 2488b1c42..581886c10 100644 --- a/pystac/extensions/ext.py +++ b/pystac/extensions/ext.py @@ -1,13 +1,21 @@ from dataclasses import dataclass from typing import Any, Generic, Literal, TypeVar, cast -from pystac import Asset, Catalog, Collection, Item, Link, STACError +from pystac import ( + Asset, + Catalog, + Collection, + Item, + ItemAssetDefinition, + Link, + STACError, +) from pystac.extensions.classification import ClassificationExtension from pystac.extensions.datacube import DatacubeExtension from pystac.extensions.eo import EOExtension from pystac.extensions.file import FileExtension from pystac.extensions.grid import GridExtension -from pystac.extensions.item_assets import AssetDefinition, ItemAssetsExtension +from pystac.extensions.item_assets import ItemAssetsExtension from pystac.extensions.mgrs import MgrsExtension from pystac.extensions.pointcloud import PointcloudExtension from pystac.extensions.projection import ProjectionExtension @@ -22,8 +30,8 @@ from pystac.extensions.view import ViewExtension from pystac.extensions.xarray_assets import XarrayAssetsExtension -T = TypeVar("T", Asset, AssetDefinition, Link) -U = TypeVar("U", Asset, AssetDefinition) +T = TypeVar("T", Asset, ItemAssetDefinition, Link) +U = TypeVar("U", Asset, ItemAssetDefinition) EXTENSION_NAMES = Literal[ "classification", @@ -107,7 +115,7 @@ def cube(self) -> DatacubeExtension[Collection]: return DatacubeExtension.ext(self.stac_object) @property - def item_assets(self) -> dict[str, AssetDefinition]: + def item_assets(self) -> dict[str, ItemAssetDefinition]: return ItemAssetsExtension.ext(self.stac_object).item_assets @property @@ -300,8 +308,8 @@ def xarray(self) -> XarrayAssetsExtension[Asset]: @dataclass -class ItemAssetExt(_AssetExt[AssetDefinition]): - stac_object: AssetDefinition +class ItemAssetExt(_AssetExt[ItemAssetDefinition]): + stac_object: ItemAssetDefinition @dataclass diff --git a/pystac/extensions/item_assets.py b/pystac/extensions/item_assets.py index 37c3b72d1..140207275 100644 --- a/pystac/extensions/item_assets.py +++ b/pystac/extensions/item_assets.py @@ -2,242 +2,73 @@ from __future__ import annotations -from copy import deepcopy -from typing import TYPE_CHECKING, Any, Literal +import warnings +from typing import Any, Literal import pystac +from pystac.errors import DeprecatedWarning from pystac.extensions.base import ExtensionManagementMixin from pystac.extensions.hooks import ExtensionHooks +from pystac.item_assets import ItemAssetDefinition from pystac.serialization.identify import STACJSONDescription, STACVersionID from pystac.utils import get_required -if TYPE_CHECKING: - from pystac.extensions.ext import ItemAssetExt - SCHEMA_URI = "https://stac-extensions.github.io/item-assets/v1.0.0/schema.json" ITEM_ASSETS_PROP = "item_assets" -ASSET_TITLE_PROP = "title" -ASSET_DESC_PROP = "description" -ASSET_TYPE_PROP = "type" -ASSET_ROLES_PROP = "roles" - -class AssetDefinition: - """Object that contains details about the datafiles that will be included in member - Items for this Collection. - - See the :stac-ext:`Asset Object ` for details. +class AssetDefinition(ItemAssetDefinition): """ + DEPRECATED - properties: dict[str, Any] - - owner: pystac.Collection | None - - def __init__( - self, properties: dict[str, Any], owner: pystac.Collection | None = None - ) -> None: - self.properties = properties - self.owner = owner - - def __eq__(self, o: object) -> bool: - if not isinstance(o, AssetDefinition): - return NotImplemented - return self.to_dict() == o.to_dict() - - @classmethod - def create( - cls, - title: str | None, - description: str | None, - media_type: str | None, - roles: list[str] | None, - extra_fields: dict[str, Any] | None = None, - ) -> AssetDefinition: - """ - Creates a new asset definition. - - Args: - title : Displayed title for clients and users. - description : Description of the Asset providing additional details, - such as how it was processed or created. - `CommonMark 0.29 `__ syntax MAY be used - for rich text representation. - media_type : `media type\ - `__ - of the asset. - roles : `semantic roles - `__ - of the asset, similar to the use of rel in links. - extra_fields : Additional fields on the asset definition, e.g. from - extensions. - """ - asset_defn = cls({}) - asset_defn.apply( - title=title, - description=description, - media_type=media_type, - roles=roles, - extra_fields=extra_fields, - ) - return asset_defn - - def apply( - self, - title: str | None, - description: str | None, - media_type: str | None, - roles: list[str] | None, - extra_fields: dict[str, Any] | None = None, - ) -> None: - """ - Sets the properties for this asset definition. - - Args: - title : Displayed title for clients and users. - description : Description of the Asset providing additional details, - such as how it was processed or created. - `CommonMark 0.29 `__ syntax MAY be used - for rich text representation. - media_type : `media type\ - `__ - of the asset. - roles : `semantic roles - `__ - of the asset, similar to the use of rel in links. - extra_fields : Additional fields on the asset definition, e.g. from - extensions. - """ - if extra_fields: - self.properties.update(extra_fields) - self.title = title - self.description = description - self.media_type = media_type - self.roles = roles - self.owner = None - - def set_owner(self, obj: pystac.Collection) -> None: - """Sets the owning item of this AssetDefinition. - - The owning item will be used to resolve relative HREFs of this asset. - - Args: - obj: The Collection that owns this asset. - """ - self.owner = obj - - @property - def title(self) -> str | None: - """Gets or sets the displayed title for clients and users.""" - return self.properties.get(ASSET_TITLE_PROP) - - @title.setter - def title(self, v: str | None) -> None: - if v is None: - self.properties.pop(ASSET_TITLE_PROP, None) - else: - self.properties[ASSET_TITLE_PROP] = v - - @property - def description(self) -> str | None: - """Gets or sets a description of the Asset providing additional details, such as - how it was processed or created. `CommonMark 0.29 `__ - syntax MAY be used for rich text representation.""" - return self.properties.get(ASSET_DESC_PROP) - - @description.setter - def description(self, v: str | None) -> None: - if v is None: - self.properties.pop(ASSET_DESC_PROP, None) - else: - self.properties[ASSET_DESC_PROP] = v - - @property - def media_type(self) -> str | None: - """Gets or sets the `media type - `__ - of the asset.""" - return self.properties.get(ASSET_TYPE_PROP) - - @media_type.setter - def media_type(self, v: str | None) -> None: - if v is None: - self.properties.pop(ASSET_TYPE_PROP, None) - else: - self.properties[ASSET_TYPE_PROP] = v - - @property - def roles(self) -> list[str] | None: - """Gets or sets the `semantic roles - `__ - of the asset, similar to the use of rel in links.""" - return self.properties.get(ASSET_ROLES_PROP) - - @roles.setter - def roles(self, v: list[str] | None) -> None: - if v is None: - self.properties.pop(ASSET_ROLES_PROP, None) - else: - self.properties[ASSET_ROLES_PROP] = v - - def to_dict(self) -> dict[str, Any]: - """Returns a dictionary representing this ``AssetDefinition``.""" - return deepcopy(self.properties) + .. deprecated:: 1.12.0 + Use :class:`~pystac.ItemAssetDefinition` instead. + """ - def create_asset(self, href: str) -> pystac.Asset: - """Creates a new :class:`~pystac.Asset` instance using the fields from this - ``AssetDefinition`` and the given ``href``.""" - return pystac.Asset( - href=href, - title=self.title, - description=self.description, - media_type=self.media_type, - roles=self.roles, - extra_fields={ - k: v - for k, v in self.properties.items() - if k - not in { - ASSET_TITLE_PROP, - ASSET_DESC_PROP, - ASSET_TYPE_PROP, - ASSET_ROLES_PROP, - } - }, + def __init__(cls, *args: Any, **kwargs: Any) -> None: + warnings.warn( + ( + "``AssetDefinition`` is deprecated. " + "Please use ``pystac.ItemAssetDefinition`` instead." + ), + DeprecationWarning, ) + super().__init__(*args, **kwargs) - @property - def ext(self) -> ItemAssetExt: - """Accessor for extension classes on this item_asset - - Example:: - - collection.ext.item_assets["data"].ext.proj.epsg = 4326 - """ - from pystac.extensions.ext import ItemAssetExt - return ItemAssetExt(stac_object=self) +class ItemAssetsExtension(ExtensionManagementMixin[pystac.Collection]): + """ + DEPRECATED + .. deprecated:: 1.12.0 + Use :attr:`~pystac.Collection.item_assets` instead. + """ -class ItemAssetsExtension(ExtensionManagementMixin[pystac.Collection]): name: Literal["item_assets"] = "item_assets" collection: pystac.Collection def __init__(self, collection: pystac.Collection) -> None: + warnings.warn( + ( + "The ``item_assets`` extension is deprecated. " + "``item_assets`` is now a top-level property of ``Collection``." + ), + DeprecatedWarning, + ) self.collection = collection @property - def item_assets(self) -> dict[str, AssetDefinition]: + def item_assets(self) -> dict[str, ItemAssetDefinition]: """Gets or sets a dictionary of assets that can be found in member Items. Maps the asset key to an :class:`AssetDefinition` instance.""" result: dict[str, Any] = get_required( self.collection.extra_fields.get(ITEM_ASSETS_PROP), self, ITEM_ASSETS_PROP ) - return {k: AssetDefinition(v, self.collection) for k, v in result.items()} + return {k: ItemAssetDefinition(v, self.collection) for k, v in result.items()} @item_assets.setter - def item_assets(self, v: dict[str, AssetDefinition]) -> None: + def item_assets(self, v: dict[str, ItemAssetDefinition]) -> None: self.collection.extra_fields[ITEM_ASSETS_PROP] = { k: asset_def.properties for k, asset_def in v.items() } @@ -284,5 +115,11 @@ def migrate( super().migrate(obj, version, info) + # As of STAC spec version 1.1.0 item-assets are part of core + if obj["stac_version"] >= "1.1.0" and self.schema_uri in obj.get( + "stac_extensions", [] + ): + obj["stac_extensions"].remove(self.schema_uri) + ITEM_ASSETS_EXTENSION_HOOKS: ExtensionHooks = ItemAssetsExtensionHooks() diff --git a/pystac/extensions/pointcloud.py b/pystac/extensions/pointcloud.py index b82566e74..804c55372 100644 --- a/pystac/extensions/pointcloud.py +++ b/pystac/extensions/pointcloud.py @@ -13,7 +13,6 @@ ) import pystac -from pystac.extensions import item_assets from pystac.extensions.base import ( ExtensionManagementMixin, PropertiesExtension, @@ -23,7 +22,7 @@ from pystac.summaries import RangeSummary from pystac.utils import StringEnum, get_required, map_opt -T = TypeVar("T", pystac.Item, pystac.Asset, item_assets.AssetDefinition) +T = TypeVar("T", pystac.Item, pystac.Asset, pystac.ItemAssetDefinition) SCHEMA_URI: str = "https://stac-extensions.github.io/pointcloud/v1.0.0/schema.json" PREFIX: str = "pc:" @@ -468,7 +467,7 @@ def ext(cls, obj: T, add_if_missing: bool = False) -> PointcloudExtension[T]: ) cls.ensure_owner_has_extension(obj, add_if_missing) return cast(PointcloudExtension[T], AssetPointcloudExtension(obj)) - elif isinstance(obj, item_assets.AssetDefinition): + elif isinstance(obj, pystac.ItemAssetDefinition): cls.ensure_owner_has_extension(obj, add_if_missing) return cast(PointcloudExtension[T], ItemAssetsPointcloudExtension(obj)) else: @@ -534,11 +533,11 @@ def __repr__(self) -> str: return f"" -class ItemAssetsPointcloudExtension(PointcloudExtension[item_assets.AssetDefinition]): +class ItemAssetsPointcloudExtension(PointcloudExtension[pystac.ItemAssetDefinition]): properties: dict[str, Any] - asset_defn: item_assets.AssetDefinition + asset_defn: pystac.ItemAssetDefinition - def __init__(self, item_asset: item_assets.AssetDefinition): + def __init__(self, item_asset: pystac.ItemAssetDefinition): self.asset_defn = item_asset self.properties = item_asset.properties diff --git a/pystac/extensions/projection.py b/pystac/extensions/projection.py index 1982e8b55..8d3cb1523 100644 --- a/pystac/extensions/projection.py +++ b/pystac/extensions/projection.py @@ -15,7 +15,6 @@ ) import pystac -from pystac.extensions import item_assets from pystac.extensions.base import ( ExtensionManagementMixin, PropertiesExtension, @@ -23,7 +22,7 @@ ) from pystac.extensions.hooks import ExtensionHooks -T = TypeVar("T", pystac.Item, pystac.Asset, item_assets.AssetDefinition) +T = TypeVar("T", pystac.Item, pystac.Asset, pystac.ItemAssetDefinition) SCHEMA_URI: str = "https://stac-extensions.github.io/projection/v1.1.0/schema.json" SCHEMA_URIS: list[str] = [ @@ -301,7 +300,7 @@ def ext(cls, obj: T, add_if_missing: bool = False) -> ProjectionExtension[T]: elif isinstance(obj, pystac.Asset): cls.ensure_owner_has_extension(obj, add_if_missing) return cast(ProjectionExtension[T], AssetProjectionExtension(obj)) - elif isinstance(obj, item_assets.AssetDefinition): + elif isinstance(obj, pystac.ItemAssetDefinition): cls.ensure_owner_has_extension(obj, add_if_missing) return cast(ProjectionExtension[T], ItemAssetsProjectionExtension(obj)) else: @@ -368,11 +367,11 @@ def __repr__(self) -> str: return f"" -class ItemAssetsProjectionExtension(ProjectionExtension[item_assets.AssetDefinition]): +class ItemAssetsProjectionExtension(ProjectionExtension[pystac.ItemAssetDefinition]): properties: dict[str, Any] - asset_defn: item_assets.AssetDefinition + asset_defn: pystac.ItemAssetDefinition - def __init__(self, item_asset: item_assets.AssetDefinition): + def __init__(self, item_asset: pystac.ItemAssetDefinition): self.asset_defn = item_asset self.properties = item_asset.properties diff --git a/pystac/extensions/raster.py b/pystac/extensions/raster.py index ab57d7c3e..eb01ee6bd 100644 --- a/pystac/extensions/raster.py +++ b/pystac/extensions/raster.py @@ -14,7 +14,6 @@ ) import pystac -from pystac.extensions import item_assets from pystac.extensions.base import ( ExtensionManagementMixin, PropertiesExtension, @@ -23,7 +22,7 @@ from pystac.extensions.hooks import ExtensionHooks from pystac.utils import StringEnum, get_opt, get_required, map_opt -T = TypeVar("T", pystac.Asset, item_assets.AssetDefinition) +T = TypeVar("T", pystac.Asset, pystac.ItemAssetDefinition) SCHEMA_URI = "https://stac-extensions.github.io/raster/v1.1.0/schema.json" SCHEMA_URIS = [ @@ -663,7 +662,7 @@ class RasterExtension( ): """An abstract class that can be used to extend the properties of an :class:`~pystac.Item`, :class:`~pystac.Asset`, or - :class:`~pystac.extension.item_assets.AssetDefinition` with properties from + :class:`~pystac.extension.pystac.ItemAssetDefinition` with properties from the :stac-ext:`Raster Extension `. This class is generic over the type of STAC Object to be extended (e.g. :class:`~pystac.Item`, :class:`~pystac.Asset`). @@ -736,7 +735,7 @@ def ext(cls, obj: T, add_if_missing: bool = False) -> RasterExtension[T]: if isinstance(obj, pystac.Asset): cls.ensure_owner_has_extension(obj, add_if_missing) return cast(RasterExtension[T], AssetRasterExtension(obj)) - elif isinstance(obj, item_assets.AssetDefinition): + elif isinstance(obj, pystac.ItemAssetDefinition): cls.ensure_owner_has_extension(obj, add_if_missing) return cast(RasterExtension[T], ItemAssetsRasterExtension(obj)) else: @@ -771,16 +770,16 @@ def __repr__(self) -> str: return f"" -class ItemAssetsRasterExtension(RasterExtension[item_assets.AssetDefinition]): - asset_definition: item_assets.AssetDefinition - """A reference to the :class:`~pystac.extensions.item_assets.AssetDefinition` +class ItemAssetsRasterExtension(RasterExtension[pystac.ItemAssetDefinition]): + asset_definition: pystac.ItemAssetDefinition + """A reference to the :class:`~pystac.extensions.pystac.ItemAssetDefinition` being extended.""" properties: dict[str, Any] - """The :class:`~pystac.extensions.item_assets.AssetDefinition` fields, including + """The :class:`~pystac.extensions.pystac.ItemAssetDefinition` fields, including extension properties.""" - def __init__(self, item_asset: item_assets.AssetDefinition): + def __init__(self, item_asset: pystac.ItemAssetDefinition): self.properties = item_asset.properties self.asset_definition = item_asset diff --git a/pystac/extensions/sar.py b/pystac/extensions/sar.py index 112c63a9a..161272213 100644 --- a/pystac/extensions/sar.py +++ b/pystac/extensions/sar.py @@ -13,7 +13,6 @@ ) import pystac -from pystac.extensions import item_assets from pystac.extensions.base import ( ExtensionManagementMixin, PropertiesExtension, @@ -24,7 +23,7 @@ from pystac.summaries import RangeSummary from pystac.utils import StringEnum, get_required, map_opt -T = TypeVar("T", pystac.Item, pystac.Asset, item_assets.AssetDefinition) +T = TypeVar("T", pystac.Item, pystac.Asset, pystac.ItemAssetDefinition) SCHEMA_URI: str = "https://stac-extensions.github.io/sar/v1.0.0/schema.json" PREFIX: str = "sar:" @@ -332,7 +331,7 @@ def ext(cls, obj: T, add_if_missing: bool = False) -> SarExtension[T]: ) cls.ensure_owner_has_extension(obj, add_if_missing) return cast(SarExtension[T], AssetSarExtension(obj)) - elif isinstance(obj, item_assets.AssetDefinition): + elif isinstance(obj, pystac.ItemAssetDefinition): cls.ensure_owner_has_extension(obj, add_if_missing) return cast(SarExtension[T], ItemAssetsSarExtension(obj)) else: @@ -399,11 +398,11 @@ def __repr__(self) -> str: return f"" -class ItemAssetsSarExtension(SarExtension[item_assets.AssetDefinition]): +class ItemAssetsSarExtension(SarExtension[pystac.ItemAssetDefinition]): properties: dict[str, Any] - asset_defn: item_assets.AssetDefinition + asset_defn: pystac.ItemAssetDefinition - def __init__(self, item_asset: item_assets.AssetDefinition): + def __init__(self, item_asset: pystac.ItemAssetDefinition): self.asset_defn = item_asset self.properties = item_asset.properties diff --git a/pystac/extensions/sat.py b/pystac/extensions/sat.py index ab5b638fe..ed900b46e 100644 --- a/pystac/extensions/sat.py +++ b/pystac/extensions/sat.py @@ -14,7 +14,6 @@ ) import pystac -from pystac.extensions import item_assets from pystac.extensions.base import ( ExtensionManagementMixin, PropertiesExtension, @@ -24,7 +23,7 @@ from pystac.summaries import RangeSummary from pystac.utils import StringEnum, datetime_to_str, map_opt, str_to_datetime -T = TypeVar("T", pystac.Item, pystac.Asset, item_assets.AssetDefinition) +T = TypeVar("T", pystac.Item, pystac.Asset, pystac.ItemAssetDefinition) SCHEMA_URI = "https://stac-extensions.github.io/sat/v1.0.0/schema.json" @@ -163,7 +162,7 @@ def ext(cls, obj: T, add_if_missing: bool = False) -> SatExtension[T]: elif isinstance(obj, pystac.Asset): cls.ensure_owner_has_extension(obj, add_if_missing) return cast(SatExtension[T], AssetSatExtension(obj)) - elif isinstance(obj, item_assets.AssetDefinition): + elif isinstance(obj, pystac.ItemAssetDefinition): cls.ensure_owner_has_extension(obj, add_if_missing) return cast(SatExtension[T], ItemAssetsSatExtension(obj)) else: @@ -232,11 +231,11 @@ def __repr__(self) -> str: return f"" -class ItemAssetsSatExtension(SatExtension[item_assets.AssetDefinition]): +class ItemAssetsSatExtension(SatExtension[pystac.ItemAssetDefinition]): properties: dict[str, Any] - asset_defn: item_assets.AssetDefinition + asset_defn: pystac.ItemAssetDefinition - def __init__(self, item_asset: item_assets.AssetDefinition): + def __init__(self, item_asset: pystac.ItemAssetDefinition): self.asset_defn = item_asset self.properties = item_asset.properties diff --git a/pystac/extensions/storage.py b/pystac/extensions/storage.py index 0b0aa0a1a..e7155887f 100644 --- a/pystac/extensions/storage.py +++ b/pystac/extensions/storage.py @@ -16,7 +16,6 @@ ) import pystac -from pystac.extensions import item_assets from pystac.extensions.base import ( ExtensionManagementMixin, PropertiesExtension, @@ -25,7 +24,7 @@ from pystac.extensions.hooks import ExtensionHooks from pystac.utils import StringEnum -T = TypeVar("T", pystac.Item, pystac.Asset, item_assets.AssetDefinition) +T = TypeVar("T", pystac.Item, pystac.Asset, pystac.ItemAssetDefinition) SCHEMA_URI: str = "https://stac-extensions.github.io/storage/v1.0.0/schema.json" PREFIX: str = "storage:" @@ -154,7 +153,7 @@ def ext(cls, obj: T, add_if_missing: bool = False) -> StorageExtension[T]: elif isinstance(obj, pystac.Asset): cls.ensure_owner_has_extension(obj, add_if_missing) return cast(StorageExtension[T], AssetStorageExtension(obj)) - elif isinstance(obj, item_assets.AssetDefinition): + elif isinstance(obj, pystac.ItemAssetDefinition): cls.ensure_owner_has_extension(obj, add_if_missing) return cast(StorageExtension[T], ItemAssetsStorageExtension(obj)) else: @@ -221,11 +220,11 @@ def __repr__(self) -> str: return f"" -class ItemAssetsStorageExtension(StorageExtension[item_assets.AssetDefinition]): +class ItemAssetsStorageExtension(StorageExtension[pystac.ItemAssetDefinition]): properties: dict[str, Any] - asset_defn: item_assets.AssetDefinition + asset_defn: pystac.ItemAssetDefinition - def __init__(self, item_asset: item_assets.AssetDefinition): + def __init__(self, item_asset: pystac.ItemAssetDefinition): self.asset_defn = item_asset self.properties = item_asset.properties diff --git a/pystac/extensions/table.py b/pystac/extensions/table.py index 782d741a2..55feda51a 100644 --- a/pystac/extensions/table.py +++ b/pystac/extensions/table.py @@ -5,13 +5,12 @@ from typing import Any, Generic, Literal, TypeVar, Union, cast import pystac -from pystac.extensions import item_assets from pystac.extensions.base import ExtensionManagementMixin, PropertiesExtension from pystac.extensions.hooks import ExtensionHooks from pystac.utils import get_required T = TypeVar( - "T", pystac.Collection, pystac.Item, pystac.Asset, item_assets.AssetDefinition + "T", pystac.Collection, pystac.Item, pystac.Asset, pystac.ItemAssetDefinition ) SCHEMA_URI = "https://stac-extensions.github.io/table/v1.2.0/schema.json" @@ -165,7 +164,7 @@ def ext(cls, obj: T, add_if_missing: bool = False) -> TableExtension[T]: if isinstance(obj, pystac.Asset): cls.ensure_owner_has_extension(obj, add_if_missing) return cast(TableExtension[T], AssetTableExtension(obj)) - elif isinstance(obj, item_assets.AssetDefinition): + elif isinstance(obj, pystac.ItemAssetDefinition): cls.ensure_owner_has_extension(obj, add_if_missing) return cast(TableExtension[T], ItemAssetsTableExtension(obj)) else: @@ -294,11 +293,11 @@ def __repr__(self) -> str: return f"" -class ItemAssetsTableExtension(TableExtension[item_assets.AssetDefinition]): +class ItemAssetsTableExtension(TableExtension[pystac.ItemAssetDefinition]): properties: dict[str, Any] - asset_defn: item_assets.AssetDefinition + asset_defn: pystac.ItemAssetDefinition - def __init__(self, item_asset: item_assets.AssetDefinition): + def __init__(self, item_asset: pystac.ItemAssetDefinition): self.asset_defn = item_asset self.properties = item_asset.properties diff --git a/pystac/extensions/version.py b/pystac/extensions/version.py index ee394f483..951a7c95d 100644 --- a/pystac/extensions/version.py +++ b/pystac/extensions/version.py @@ -20,6 +20,7 @@ Collection, ExtensionTypeError, Item, + ItemAssetDefinition, Link, MediaType, STACObject, @@ -28,10 +29,9 @@ from pystac.errors import DeprecatedWarning from pystac.extensions.base import ExtensionManagementMixin, PropertiesExtension from pystac.extensions.hooks import ExtensionHooks -from pystac.extensions.item_assets import AssetDefinition from pystac.utils import StringEnum, map_opt -T = TypeVar("T", Collection, Item, Catalog, Asset, AssetDefinition) +T = TypeVar("T", Collection, Item, Catalog, Asset, ItemAssetDefinition) U = TypeVar("U", Collection, Item, Catalog) SCHEMA_URI = "https://stac-extensions.github.io/version/v1.2.0/schema.json" @@ -395,10 +395,10 @@ def __repr__(self) -> str: return f"" -class ItemAssetsViewExtension(BaseVersionExtension[AssetDefinition]): +class ItemAssetsViewExtension(BaseVersionExtension[ItemAssetDefinition]): properties: dict[str, Any] - def __init__(self, item_asset: AssetDefinition): + def __init__(self, item_asset: ItemAssetDefinition): self.properties = item_asset.properties diff --git a/pystac/extensions/view.py b/pystac/extensions/view.py index 3a91249eb..29470d874 100644 --- a/pystac/extensions/view.py +++ b/pystac/extensions/view.py @@ -6,7 +6,6 @@ from typing import Any, Generic, Literal, TypeVar, Union, cast import pystac -from pystac.extensions import item_assets from pystac.extensions.base import ( ExtensionManagementMixin, PropertiesExtension, @@ -15,7 +14,7 @@ from pystac.extensions.hooks import ExtensionHooks from pystac.summaries import RangeSummary -T = TypeVar("T", pystac.Item, pystac.Asset, item_assets.AssetDefinition) +T = TypeVar("T", pystac.Item, pystac.Asset, pystac.ItemAssetDefinition) SCHEMA_URI: str = "https://stac-extensions.github.io/view/v1.0.0/schema.json" PREFIX: str = "view:" @@ -166,7 +165,7 @@ def ext(cls, obj: T, add_if_missing: bool = False) -> ViewExtension[T]: elif isinstance(obj, pystac.Asset): cls.ensure_owner_has_extension(obj, add_if_missing) return cast(ViewExtension[T], AssetViewExtension(obj)) - elif isinstance(obj, item_assets.AssetDefinition): + elif isinstance(obj, pystac.ItemAssetDefinition): cls.ensure_owner_has_extension(obj, add_if_missing) return cast(ViewExtension[T], ItemAssetsViewExtension(obj)) else: @@ -233,11 +232,11 @@ def __repr__(self) -> str: return f"" -class ItemAssetsViewExtension(ViewExtension[item_assets.AssetDefinition]): +class ItemAssetsViewExtension(ViewExtension[pystac.ItemAssetDefinition]): properties: dict[str, Any] - asset_defn: item_assets.AssetDefinition + asset_defn: pystac.ItemAssetDefinition - def __init__(self, item_asset: item_assets.AssetDefinition): + def __init__(self, item_asset: pystac.ItemAssetDefinition): self.asset_defn = item_asset self.properties = item_asset.properties diff --git a/pystac/item_assets.py b/pystac/item_assets.py new file mode 100644 index 000000000..2260cdf0b --- /dev/null +++ b/pystac/item_assets.py @@ -0,0 +1,246 @@ +""" +Implements the `Item Asset Definition Object +`__ +for use as values in the :attr:`~pystac.Collection.item_assets` dict. +""" + +from __future__ import annotations + +from copy import deepcopy +from typing import TYPE_CHECKING, Any + +import pystac + +if TYPE_CHECKING: + from pystac.extensions.ext import ItemAssetExt + + +ASSET_TITLE_PROP = "title" +ASSET_DESC_PROP = "description" +ASSET_TYPE_PROP = "type" +ASSET_ROLES_PROP = "roles" + + +class ItemAssetDefinition: + """Implementation of the `Item Asset Definition Object + `__ + for use as values in the :attr:`~pystac.Collection.item_assets` dict. + """ + + properties: dict[str, Any] + + owner: pystac.Collection | None + + def __init__( + self, properties: dict[str, Any], owner: pystac.Collection | None = None + ) -> None: + self.properties = properties + self.owner = owner + + def __eq__(self, o: object) -> bool: + if not isinstance(o, ItemAssetDefinition): + return NotImplemented + return self.to_dict() == o.to_dict() + + @classmethod + def create( + cls, + title: str | None, + description: str | None, + media_type: str | None, + roles: list[str] | None, + extra_fields: dict[str, Any] | None = None, + ) -> ItemAssetDefinition: + """ + Creates a new asset definition. + + Args: + title : Displayed title for clients and users. + description : Description of the Asset providing additional details, + such as how it was processed or created. + `CommonMark 0.29 `__ syntax MAY be used + for rich text representation. + media_type : `media type\ + `__ + of the asset. + roles : `semantic roles + `__ + of the asset, similar to the use of rel in links. + extra_fields : Additional fields on the asset definition, e.g. from + extensions. + """ + asset_defn = cls({}) + asset_defn.apply( + title=title, + description=description, + media_type=media_type, + roles=roles, + extra_fields=extra_fields, + ) + return asset_defn + + def apply( + self, + title: str | None, + description: str | None, + media_type: str | None, + roles: list[str] | None, + extra_fields: dict[str, Any] | None = None, + ) -> None: + """ + Sets the properties for this asset definition. + + Args: + title : Displayed title for clients and users. + description : Description of the Asset providing additional details, + such as how it was processed or created. + `CommonMark 0.29 `__ syntax MAY be used + for rich text representation. + media_type : `media type\ + `__ + of the asset. + roles : `semantic roles + `__ + of the asset, similar to the use of rel in links. + extra_fields : Additional fields on the asset definition, e.g. from + extensions. + """ + if extra_fields: + self.properties.update(extra_fields) + self.title = title + self.description = description + self.media_type = media_type + self.roles = roles + self.owner = None + + def set_owner(self, obj: pystac.Collection) -> None: + """Sets the owning item of this ItemAssetDefinition. + + The owning item will be used to resolve relative HREFs of this asset. + + Args: + obj: The Collection that owns this asset. + """ + self.owner = obj + + @property + def title(self) -> str | None: + """Gets or sets the displayed title for clients and users.""" + return self.properties.get(ASSET_TITLE_PROP) + + @title.setter + def title(self, v: str | None) -> None: + if v is None: + self.properties.pop(ASSET_TITLE_PROP, None) + else: + self.properties[ASSET_TITLE_PROP] = v + + @property + def description(self) -> str | None: + """Gets or sets a description of the Asset providing additional details, such as + how it was processed or created. `CommonMark 0.29 `__ + syntax MAY be used for rich text representation.""" + return self.properties.get(ASSET_DESC_PROP) + + @description.setter + def description(self, v: str | None) -> None: + if v is None: + self.properties.pop(ASSET_DESC_PROP, None) + else: + self.properties[ASSET_DESC_PROP] = v + + @property + def media_type(self) -> str | None: + """Gets or sets the `media type + `__ + of the asset.""" + return self.properties.get(ASSET_TYPE_PROP) + + @media_type.setter + def media_type(self, v: str | None) -> None: + if v is None: + self.properties.pop(ASSET_TYPE_PROP, None) + else: + self.properties[ASSET_TYPE_PROP] = v + + @property + def roles(self) -> list[str] | None: + """Gets or sets the `semantic roles + `__ + of the asset, similar to the use of rel in links.""" + return self.properties.get(ASSET_ROLES_PROP) + + @roles.setter + def roles(self, v: list[str] | None) -> None: + if v is None: + self.properties.pop(ASSET_ROLES_PROP, None) + else: + self.properties[ASSET_ROLES_PROP] = v + + def to_dict(self) -> dict[str, Any]: + """Returns a dictionary representing this ``ItemAssetDefinition``.""" + return deepcopy(self.properties) + + def create_asset(self, href: str) -> pystac.Asset: + """Creates a new :class:`~pystac.Asset` instance using the fields from this + ``ItemAssetDefinition`` and the given ``href``.""" + return pystac.Asset( + href=href, + title=self.title, + description=self.description, + media_type=self.media_type, + roles=self.roles, + extra_fields={ + k: v + for k, v in self.properties.items() + if k + not in { + ASSET_TITLE_PROP, + ASSET_DESC_PROP, + ASSET_TYPE_PROP, + ASSET_ROLES_PROP, + } + }, + ) + + @property + def ext(self) -> ItemAssetExt: + """Accessor for extension classes on this item_asset + + Example:: + + collection.item_assets["data"].ext.proj.epsg = 4326 + """ + from pystac.extensions.ext import ItemAssetExt + + return ItemAssetExt(stac_object=self) + + +class _ItemAssets(dict): # type:ignore + """Private class for exposing item_assets as a dict + + This class coerces values to ``ItemAssetDefinition``s and + sets that owner on all ``ItemAssetDefinition``s to the collection + that it is owned by. + """ + + collection: pystac.Collection + + def __init__(self, collection: pystac.Collection) -> None: + self.collection = collection + if not collection.extra_fields.get("item_assets"): + collection.extra_fields["item_assets"] = {} + self.update(collection.extra_fields["item_assets"]) + + def __setitem__(self, key: str, value: Any) -> None: + if isinstance(value, ItemAssetDefinition): + asset_definition = value + asset_definition.set_owner(self.collection) + else: + asset_definition = ItemAssetDefinition(value, self.collection) + self.collection.extra_fields["item_assets"][key] = asset_definition.properties + super().__setitem__(key, asset_definition) + + def update(self, *args: Any, **kwargs: Any) -> None: + for k, v in dict(*args, **kwargs).items(): + self[k] = v diff --git a/tests/extensions/test_classification.py b/tests/extensions/test_classification.py index 8ff526b94..ea5c16416 100644 --- a/tests/extensions/test_classification.py +++ b/tests/extensions/test_classification.py @@ -18,7 +18,6 @@ Classification, ClassificationExtension, ) -from pystac.extensions.item_assets import ItemAssetsExtension from pystac.extensions.raster import RasterBand, RasterExtension from tests.utils import TestCases @@ -51,7 +50,7 @@ def plain_item() -> Item: @pytest.fixture -def collection() -> Collection: +def classification_collection() -> Collection: return Collection.from_file(CLASSIFICATION_COLLECTION_RASTER_URI) @@ -108,12 +107,12 @@ def test_ext_raises_if_item_does_not_conform(plain_item: Item) -> None: ClassificationExtension.ext(plain_item) -def test_ext_raises_on_collection(collection: pystac.Collection) -> None: +def test_ext_raises_on_collection(classification_collection: Collection) -> None: with pytest.raises( pystac.errors.ExtensionTypeError, match="ClassificationExtension does not apply to type 'Collection'", ) as e: - ClassificationExtension.ext(collection) # type:ignore + ClassificationExtension.ext(classification_collection) # type:ignore assert "Hint" in str(e.value) @@ -311,24 +310,27 @@ def test_add_asset_classes(plain_item: Item) -> None: assert asset.extra_fields[CLASSES_PROP] == [{"value": 0, "name": "dummy"}] -def test_item_asset_raster_classes(collection: Collection) -> None: - item_asset = ItemAssetsExtension.ext(collection, add_if_missing=True).item_assets[ - "cloud-mask-raster" - ] +def test_item_asset_raster_classes(classification_collection: Collection) -> None: + assert classification_collection.item_assets + item_asset = classification_collection.item_assets["cloud-mask-raster"] raster_bands = cast(list[RasterBand], RasterExtension.ext(item_asset).bands) raster_bands_ext = ClassificationExtension.ext(raster_bands[0]) raster_bands_ext.__repr__() assert raster_bands_ext.classes is not None -def test_item_assets_extension(collection: Collection) -> None: - item_asset = ItemAssetsExtension.ext(collection, add_if_missing=True).item_assets[ - "cloud-mask-raster" - ] +def test_item_assets_extension(classification_collection: Collection) -> None: + assert classification_collection.item_assets + item_asset = classification_collection.item_assets["cloud-mask-raster"] ext = ClassificationExtension.ext(item_asset) ext.__repr__() - assert ClassificationExtension.get_schema_uri() in collection.stac_extensions - assert collection.ext.item_assets["cloud-mask-raster"].ext.has("classification") + assert ( + ClassificationExtension.get_schema_uri() + in classification_collection.stac_extensions + ) + assert classification_collection.item_assets["cloud-mask-raster"].ext.has( + "classification" + ) def test_older_extension_version(landsat_item: Item) -> None: diff --git a/tests/extensions/test_item_assets.py b/tests/extensions/test_item_assets.py deleted file mode 100644 index a847975b0..000000000 --- a/tests/extensions/test_item_assets.py +++ /dev/null @@ -1,118 +0,0 @@ -import unittest - -from pystac import Collection -from pystac.extensions.item_assets import AssetDefinition, ItemAssetsExtension -from tests.utils import TestCases - - -class TestItemAssetsExtension(unittest.TestCase): - def setUp(self) -> None: - self.maxDiff = None - self.collection = Collection.from_file( - TestCases.get_path("data-files/item-assets/example-landsat8.json") - ) - - def test_example(self) -> None: - collection = self.collection.clone() - item_ext = ItemAssetsExtension.ext(collection) - - self.assertEqual(len(item_ext.item_assets), 13) - - self.assertEqual( - item_ext.item_assets["B1"], - AssetDefinition( - { - "type": "image/tiff; application=geotiff", - "eo:bands": [ - { - "name": "B1", - "common_name": "coastal", - "center_wavelength": 0.44, - "full_width_half_max": 0.02, - } - ], - "title": "Coastal Band (B1)", - "description": "Coastal Band Top Of the Atmosphere", - } - ), - ) - - -class TestAssetDefinition(unittest.TestCase): - def setUp(self) -> None: - self.maxDiff = None - self.collection = Collection.from_file( - TestCases.get_path("data-files/item-assets/example-landsat8.json") - ) - - def test_create(self) -> None: - title = "Coastal Band (B1)" - description = "Coastal Band Top Of the Atmosphere" - media_type = "image/tiff; application=geotiff" - roles = ["data"] - asset_defn = AssetDefinition.create( - title=title, description=description, media_type=media_type, roles=roles - ) - self.assertEqual(asset_defn.title, title) - self.assertEqual(asset_defn.description, description) - self.assertEqual(asset_defn.media_type, media_type) - self.assertEqual(asset_defn.roles, roles) - - def test_title(self) -> None: - asset_defn = AssetDefinition({}) - title = "Very Important Asset" - - asset_defn.title = title - - self.assertEqual(asset_defn.title, title) - self.assertEqual(asset_defn.to_dict()["title"], title) - - def test_description(self) -> None: - asset_defn = AssetDefinition({}) - description = "What an incredibly important asset this is!" - - asset_defn.description = description - - self.assertEqual(asset_defn.description, description) - self.assertEqual(asset_defn.to_dict()["description"], description) - - def test_media_type(self) -> None: - asset_defn = AssetDefinition({}) - media_type = "application/json" - - asset_defn.media_type = media_type - - self.assertEqual(asset_defn.media_type, media_type) - self.assertEqual(asset_defn.to_dict()["type"], media_type) - - def test_roles(self) -> None: - asset_defn = AssetDefinition({}) - roles = ["thumbnail"] - - asset_defn.roles = roles - - self.assertEqual(asset_defn.roles, roles) - self.assertEqual(asset_defn.to_dict()["roles"], roles) - - -def test_extra_fields(collection: Collection) -> None: - asset_definition = AssetDefinition.create( - title=None, - description=None, - media_type=None, - roles=None, - extra_fields={"raster:bands": [{"nodata": 42}]}, - ) - item_assets = ItemAssetsExtension.ext(collection, add_if_missing=True) - item_assets.item_assets = {"data": asset_definition} - collection_as_dict = collection.to_dict() - assert collection_as_dict["item_assets"]["data"]["raster:bands"] == [{"nodata": 42}] - asset = asset_definition.create_asset("asset.tif") - assert asset.extra_fields["raster:bands"] == [{"nodata": 42}] - - collection.ext.item_assets["data"].ext.add("raster") - assert (bands := collection.ext.item_assets["data"].ext.raster.bands) - assert bands[0].nodata == 42 - - assert collection.ext.item_assets["data"].ext.has("raster") - assert collection.ext.has("raster") diff --git a/tests/extensions/test_raster.py b/tests/extensions/test_raster.py index 2ffff58cf..925113dc9 100644 --- a/tests/extensions/test_raster.py +++ b/tests/extensions/test_raster.py @@ -5,7 +5,6 @@ import pystac from pystac import ExtensionTypeError, Item -from pystac.extensions.item_assets import ItemAssetsExtension from pystac.extensions.raster import ( DataType, Histogram, @@ -282,11 +281,13 @@ def test_summaries_adds_uri(self) -> None: def test_collection_item_asset(self) -> None: coll = pystac.Collection.from_file(self.LANDSAT_COLLECTION_EXAMPLE_URI) - qa = ItemAssetsExtension.ext(coll).item_assets["qa"] - ang = ItemAssetsExtension.ext(coll).item_assets["ang"] + assert coll.item_assets - assert RasterExtension.ext(qa).bands is not None - assert RasterExtension.ext(ang).bands is None + qa = coll.item_assets["qa"] + ang = coll.item_assets["ang"] + + assert qa.ext.raster.bands is not None + assert ang.ext.raster.bands is None @pytest.fixture diff --git a/tests/serialization/test_migrate.py b/tests/serialization/test_migrate.py index 0d41a781c..1bc5c4acb 100644 --- a/tests/serialization/test_migrate.py +++ b/tests/serialization/test_migrate.py @@ -75,8 +75,11 @@ def test_migrates_renamed_extension(self) -> None: ) ) - assert ItemAssetsExtension.has_extension(collection) + assert ItemAssetsExtension.get_schema_uri() not in collection.stac_extensions + assert not ItemAssetsExtension.has_extension(collection) assert "item_assets" in collection.extra_fields + assert collection.item_assets + assert collection.item_assets["thumbnail"].title == "Thumbnail" def test_migrates_pre_1_0_0_rc1_stats_summary(self) -> None: collection = pystac.Collection.from_file( diff --git a/tests/test_item_assets.py b/tests/test_item_assets.py new file mode 100644 index 000000000..0e5ac895f --- /dev/null +++ b/tests/test_item_assets.py @@ -0,0 +1,234 @@ +import unittest + +import pytest + +from pystac import Collection +from pystac.errors import DeprecatedWarning +from pystac.extensions.item_assets import AssetDefinition, ItemAssetsExtension +from pystac.item_assets import ItemAssetDefinition +from tests.utils import TestCases + +CLASSIFICATION_COLLECTION_RASTER_URI = TestCases.get_path( + "data-files/classification/collection-item-assets-raster-bands.json" +) + + +class TestItemAssets(unittest.TestCase): + def setUp(self) -> None: + self.maxDiff = None + self.collection = Collection.from_file( + TestCases.get_path("data-files/item-assets/example-landsat8.json") + ) + + def test_example(self) -> None: + collection = self.collection.clone() + + self.assertEqual(len(collection.item_assets), 13) + + self.assertEqual( + collection.item_assets["B1"], + ItemAssetDefinition( + { + "type": "image/tiff; application=geotiff", + "eo:bands": [ + { + "name": "B1", + "common_name": "coastal", + "center_wavelength": 0.44, + "full_width_half_max": 0.02, + } + ], + "title": "Coastal Band (B1)", + "description": "Coastal Band Top Of the Atmosphere", + } + ), + ) + + def test_set_using_dict(self) -> None: + collection = self.collection.clone() + + self.assertEqual(len(collection.item_assets), 13) + + collection.item_assets["Bx"] = { + "type": "image/tiff; application=geotiff", + "eo:bands": [ + { + "name": "B1", + "common_name": "coastal", + "center_wavelength": 0.44, + "full_width_half_max": 0.02, + } + ], + "title": "Coastal Band (B1)", + "description": "Coastal Band Top Of the Atmosphere", + } # type:ignore + + self.assertEqual(collection.item_assets["B1"], collection.item_assets["Bx"]) + + +class TestAssetDefinition(unittest.TestCase): + def setUp(self) -> None: + self.maxDiff = None + self.collection = Collection.from_file( + TestCases.get_path("data-files/item-assets/example-landsat8.json") + ) + + def test_eq(self) -> None: + assert self.collection.item_assets["B1"] != {"title": "Coastal Band (B1)"} + + def test_create(self) -> None: + title = "Coastal Band (B1)" + description = "Coastal Band Top Of the Atmosphere" + media_type = "image/tiff; application=geotiff" + roles = ["data"] + asset_defn = ItemAssetDefinition.create( + title=title, description=description, media_type=media_type, roles=roles + ) + self.assertEqual(asset_defn.title, title) + self.assertEqual(asset_defn.description, description) + self.assertEqual(asset_defn.media_type, media_type) + self.assertEqual(asset_defn.roles, roles) + + def test_title(self) -> None: + asset_defn = ItemAssetDefinition({}) + title = "Very Important Asset" + + asset_defn.title = title + + self.assertEqual(asset_defn.title, title) + self.assertEqual(asset_defn.to_dict()["title"], title) + + def test_description(self) -> None: + asset_defn = ItemAssetDefinition({}) + description = "What an incredibly important asset this is!" + + asset_defn.description = description + + self.assertEqual(asset_defn.description, description) + self.assertEqual(asset_defn.to_dict()["description"], description) + + def test_media_type(self) -> None: + asset_defn = ItemAssetDefinition({}) + media_type = "application/json" + + asset_defn.media_type = media_type + + self.assertEqual(asset_defn.media_type, media_type) + self.assertEqual(asset_defn.to_dict()["type"], media_type) + + def test_roles(self) -> None: + asset_defn = ItemAssetDefinition({}) + roles = ["thumbnail"] + + asset_defn.roles = roles + + self.assertEqual(asset_defn.roles, roles) + self.assertEqual(asset_defn.to_dict()["roles"], roles) + + def test_set_owner(self) -> None: + asset_definition = ItemAssetDefinition( + { + "type": "image/tiff; application=geotiff", + "eo:bands": [ + { + "name": "B1", + "common_name": "coastal", + "center_wavelength": 0.44, + "full_width_half_max": 0.02, + } + ], + "title": "Coastal Band (B1)", + "description": "Coastal Band Top Of the Atmosphere", + } + ) + asset_definition.set_owner(self.collection) + assert asset_definition.owner == self.collection + + +def test_extra_fields(collection: Collection) -> None: + asset_definition = ItemAssetDefinition.create( + title=None, + description=None, + media_type=None, + roles=None, + extra_fields={"raster:bands": [{"nodata": 42}]}, + ) + + collection.item_assets = {"data": asset_definition} + assert collection.item_assets["data"].owner == collection + + collection_as_dict = collection.to_dict() + assert collection_as_dict["item_assets"]["data"]["raster:bands"] == [{"nodata": 42}] + asset = asset_definition.create_asset("asset.tif") + assert asset.extra_fields["raster:bands"] == [{"nodata": 42}] + + collection.item_assets["data"].ext.add("raster") + assert (bands := collection.item_assets["data"].ext.raster.bands) + assert bands[0].nodata == 42 + + assert collection.item_assets["data"].ext.has("raster") + assert collection.ext.has("raster") + + +def test_set_item_asset(collection: Collection) -> None: + asset_definition = ItemAssetDefinition.create( + title=None, + description=None, + media_type=None, + roles=None, + extra_fields={"raster:bands": [{"nodata": 42}]}, + ) + + collection.item_assets["data"] = asset_definition + assert collection.item_assets["data"].owner == collection + + +def test_item_assets_extension_is_deprecated() -> None: + collection = Collection.from_file(CLASSIFICATION_COLLECTION_RASTER_URI) + + assert ItemAssetsExtension.get_schema_uri() not in collection.stac_extensions + + with pytest.warns(DeprecatedWarning, match="top-level property of"): + item_asset_ext = ItemAssetsExtension.ext(collection, add_if_missing=True) + item_asset = item_asset_ext.item_assets["cloud-mask-raster"] + + assert collection.id in repr(item_asset_ext) + + assert item_asset.ext.has("eo") + + with pytest.warns(DeprecatedWarning, match="top-level property of"): + assert collection.ext.item_assets["cloud-mask-raster"].ext.has("eo") + + assert ItemAssetsExtension.get_schema_uri() in collection.stac_extensions + + with pytest.warns(DeprecationWarning): + asset_definition = AssetDefinition( + {"title": "Thumbnail image", "type": "image/jpeg"} + ) + item_asset_ext.item_assets["thumbnail"] = asset_definition + + +def test_item_assets_extension_asset_definition_is_deprecated() -> None: + with pytest.warns( + DeprecationWarning, match="Please use ``pystac.ItemAssetDefinition``" + ): + asset_definition = AssetDefinition( + { + "type": "image/tiff; application=geotiff", + "eo:bands": [ + { + "name": "B1", + "common_name": "coastal", + "center_wavelength": 0.44, + "full_width_half_max": 0.02, + } + ], + "title": "Coastal Band (B1)", + "description": "Coastal Band Top Of the Atmosphere", + } + ) + + assert asset_definition.title == "Coastal Band (B1)" + assert asset_definition.ext.eo.bands + assert asset_definition.ext.eo.bands[0].name == "B1" + assert asset_definition.owner is None