Source code for pystac.extensions.file

"""Implements the :stac-ext:`File Info Extension <file>`."""

from __future__ import annotations

import warnings
from collections.abc import Iterable
from typing import Any, Generic, Literal, TypeVar, Union, cast

from pystac import (
    Asset,
    Catalog,
    Collection,
    ExtensionTypeError,
    Item,
    Link,
    STACObjectType,
)
from pystac.extensions.base import ExtensionManagementMixin, PropertiesExtension
from pystac.extensions.hooks import ExtensionHooks
from pystac.serialization.identify import (
    OldExtensionShortIDs,
    STACJSONDescription,
    STACVersionID,
)
from pystac.utils import StringEnum, get_required, map_opt

T = TypeVar("T", Asset, Link)

SCHEMA_URI = "https://stac-extensions.github.io/file/v2.1.0/schema.json"

PREFIX = "file:"
BYTE_ORDER_PROP = PREFIX + "byte_order"
CHECKSUM_PROP = PREFIX + "checksum"
HEADER_SIZE_PROP = PREFIX + "header_size"
SIZE_PROP = PREFIX + "size"
VALUES_PROP = PREFIX + "values"
LOCAL_PATH_PROP = PREFIX + "local_path"


[docs]class ByteOrder(StringEnum): """List of allows values for the ``"file:byte_order"`` field defined by the :stac-ext:`File Info Extension <file>`.""" LITTLE_ENDIAN = "little-endian" BIG_ENDIAN = "big-endian"
[docs]class MappingObject: """Represents a value map used by assets that are used as classification layers, and give details about the values in the asset and their meanings.""" properties: dict[str, Any] def __init__(self, properties: dict[str, Any]) -> None: self.properties = properties
[docs] def apply(self, values: list[Any], summary: str) -> None: """Sets the properties for this :class:`~MappingObject` instance. Args: values : The value(s) in the file. At least one array element is required. summary : A short description of the value(s). """ self.values = values self.summary = summary
[docs] @classmethod def create(cls, values: list[Any], summary: str) -> MappingObject: """Creates a new :class:`~MappingObject` instance. Args: values : The value(s) in the file. At least one array element is required. summary : A short description of the value(s). """ m = cls({}) m.apply(values=values, summary=summary) return m
@property def values(self) -> list[Any]: """Gets or sets the list of value(s) in the file. At least one array element is required.""" return get_required(self.properties.get("values"), self, "values") @values.setter def values(self, v: list[Any]) -> None: self.properties["values"] = v @property def summary(self) -> str: """Gets or sets the short description of the value(s).""" return get_required(self.properties.get("summary"), self, "summary") @summary.setter def summary(self, v: str) -> None: self.properties["summary"] = v
[docs] @classmethod def from_dict(cls, d: dict[str, Any]) -> MappingObject: return cls.create(**d)
[docs] def to_dict(self) -> dict[str, Any]: return self.properties
[docs]class FileExtension( Generic[T], PropertiesExtension, ExtensionManagementMixin[Union[Catalog, Collection, Item]], ): """A class that can be used to extend the properties of an :class:`~pystac.Asset` or :class:`~pystac.Link` with properties from the :stac-ext:`File Info Extension <file>`. To create an instance of :class:`FileExtension`, use the :meth:`FileExtension.ext` method. For example: .. code-block:: python >>> asset: pystac.Asset = ... >>> file_ext = FileExtension.ext(asset) """ name: Literal["file"] = "file"
[docs] def apply( self, byte_order: ByteOrder | None = None, checksum: str | None = None, header_size: int | None = None, size: int | None = None, values: list[MappingObject] | None = None, local_path: str | None = None, ) -> None: """Applies file extension properties to the extended Item. Args: byte_order : Optional byte order of integer values in the file. One of ``"big-endian"`` or ``"little-endian"``. checksum : Optional multihash for the corresponding file, encoded as hexadecimal (base 16) string with lowercase letters. header_size : Optional header size of the file, in bytes. size : Optional size of the file, in bytes. values : Optional list of :class:`~MappingObject` instances that lists the values that are in the file and describe their meaning. See the :stac-ext:`Mapping Object <file#mapping-object>` docs for an example. If given, at least one array element is required. """ self.byte_order = byte_order self.checksum = checksum self.header_size = header_size self.size = size self.values = values self.local_path = local_path
@property def byte_order(self) -> ByteOrder | None: """Gets or sets the byte order of integer values in the file. One of big-endian or little-endian.""" return self._get_property(BYTE_ORDER_PROP, ByteOrder) @byte_order.setter def byte_order(self, v: ByteOrder | None) -> None: self._set_property(BYTE_ORDER_PROP, v) @property def checksum(self) -> str | None: """Get or sets the multihash for the corresponding file, encoded as hexadecimal (base 16) string with lowercase letters.""" return self._get_property(CHECKSUM_PROP, str) @checksum.setter def checksum(self, v: str | None) -> None: self._set_property(CHECKSUM_PROP, v) @property def header_size(self) -> int | None: """Get or sets the header size of the file, in bytes.""" return self._get_property(HEADER_SIZE_PROP, int) @header_size.setter def header_size(self, v: int | None) -> None: self._set_property(HEADER_SIZE_PROP, v) @property def local_path(self) -> str | None: """Get or sets a relative local path for the asset/link. The ``file:local_path`` field indicates a **relative** path that can be used by clients for different purposes to organize the files locally. For compatibility reasons the name-separator character in paths **must** be ``/`` and the Windows separator ``\\`` is **not** allowed. """ return self._get_property(LOCAL_PATH_PROP, str) @local_path.setter def local_path(self, v: str | None) -> None: self._set_property(LOCAL_PATH_PROP, v, pop_if_none=True) @property def size(self) -> int | None: """Get or sets the size of the file, in bytes.""" return self._get_property(SIZE_PROP, int) @size.setter def size(self, v: int | None) -> None: self._set_property(SIZE_PROP, v) @property def values(self) -> list[MappingObject] | None: """Get or sets the list of :class:`~MappingObject` instances that lists the values that are in the file and describe their meaning. See the :stac-ext:`Mapping Object <file#mapping-object>` docs for an example. If given, at least one array element is required.""" return map_opt( lambda values: [ MappingObject.from_dict(mapping_obj) for mapping_obj in values ], self._get_property(VALUES_PROP, list[dict[str, Any]]), ) @values.setter def values(self, v: list[MappingObject] | None) -> None: self._set_property( VALUES_PROP, map_opt( lambda values: [mapping_obj.to_dict() for mapping_obj in values], v ), )
[docs] @classmethod def get_schema_uri(cls) -> str: return SCHEMA_URI
[docs] @classmethod def ext(cls, obj: Asset | Link, add_if_missing: bool = False) -> FileExtension[T]: """Extends the given STAC Object with properties from the :stac-ext:`File Info Extension <file>`. This extension can be applied to instances of :class:`~pystac.Asset` or :class:`~pystac.Link` """ if isinstance(obj, Asset): cls.ensure_owner_has_extension(obj, add_if_missing) return cast(FileExtension[T], AssetFileExtension(obj)) elif isinstance(obj, Link): cls.ensure_owner_has_extension(obj, add_if_missing) return cast(FileExtension[T], LinkFileExtension(obj)) else: raise ExtensionTypeError(cls._ext_error_message(obj))
[docs]class AssetFileExtension(FileExtension[Asset]): """A concrete implementation of :class:`FileExtension` on an :class:`~pystac.Asset` that extends the Asset fields to include properties defined in the :stac-ext:`File Info Extension <file>`. This class should generally not be instantiated directly. Instead, call :meth:`FileExtension.ext` on an :class:`~pystac.Asset` to extend it. """ asset_href: str """The ``href`` value of the :class:`~pystac.Asset` being extended.""" properties: dict[str, Any] """The :class:`~pystac.Asset` fields, including extension properties.""" additional_read_properties: Iterable[dict[str, Any]] | None = None """If present, this will be a list containing 1 dictionary representing the properties of the owner.""" def __init__(self, asset: Asset): self.asset_href = asset.href self.properties = asset.extra_fields if asset.owner and hasattr(asset.owner, "properties"): self.additional_read_properties = [asset.owner.properties] def __repr__(self) -> str: return f"<AssetFileExtension Asset href={self.asset_href}>"
[docs]class LinkFileExtension(FileExtension[Link]): """A concrete implementation of :class:`FileExtension` on an :class:`~pystac.Link` that extends the Link fields to include properties defined in the :stac-ext:`File Info Extension <file>`. This class should generally not be instantiated directly. Instead, call :meth:`FileExtension.ext` on an :class:`~pystac.Link` to extend it. """ link_href: str """The ``href`` value of the :class:`~pystac.Link` being extended.""" properties: dict[str, Any] """The :class:`~pystac.Link` fields, including extension properties.""" additional_read_properties: Iterable[dict[str, Any]] | None = None """If present, this will be a list containing 1 dictionary representing the properties of the owner.""" def __init__(self, link: Link): self.link_href = link.href self.properties = link.extra_fields if link.owner and hasattr(link.owner, "properties"): self.additional_read_properties = [link.owner.properties] def __repr__(self) -> str: return f"<LinkFileExtension Link href={self.link_href}>"
[docs]class FileExtensionHooks(ExtensionHooks): schema_uri: str = SCHEMA_URI prev_extension_ids = { "file", "https://stac-extensions.github.io/file/v1.0.0/schema.json", "https://stac-extensions.github.io/file/v2.0.0/schema.json", } stac_object_types = { STACObjectType.ITEM, STACObjectType.COLLECTION, STACObjectType.CATALOG, } removed_fields = { "file:bits_per_sample", "file:data_type", "file:nodata", "file:unit", }
[docs] def migrate( self, obj: dict[str, Any], version: STACVersionID, info: STACJSONDescription ) -> None: # The checksum field was previously it's own extension. old_checksum: dict[str, str] | None = None if info.version_range.latest_valid_version() < "v1.0.0-rc.2": if OldExtensionShortIDs.CHECKSUM.value in info.extensions: old_item_checksum = obj["properties"].get("checksum:multihash") if old_item_checksum is not None: if old_checksum is None: old_checksum = {} old_checksum["__item__"] = old_item_checksum for asset_key, asset in obj["assets"].items(): old_asset_checksum = asset.get("checksum:multihash") if old_asset_checksum is not None: if old_checksum is None: old_checksum = {} old_checksum[asset_key] = old_asset_checksum try: obj["stac_extensions"].remove(OldExtensionShortIDs.CHECKSUM.value) except ValueError: pass super().migrate(obj, version, info) if old_checksum is not None: if SCHEMA_URI not in obj["stac_extensions"]: obj["stac_extensions"].append(SCHEMA_URI) for key in old_checksum: if key == "__item__": obj["properties"][CHECKSUM_PROP] = old_checksum[key] else: obj["assets"][key][CHECKSUM_PROP] = old_checksum[key] found_fields = {} for asset_key, asset in obj.get("assets", {}).items(): if values := set(asset.keys()).intersection(self.removed_fields): found_fields[asset_key] = values if found_fields: warnings.warn( f"Assets {list(found_fields.keys())} contain fields: " f"{list(set.union(*found_fields.values()))} which " "were removed from the file extension spec in v2.0.0. Please " "consult the release notes " "(https://github.com/stac-extensions/file/releases/tag/v2.0.0) " "for instructions on how to migrate these fields.", UserWarning, )
FILE_EXTENSION_HOOKS: ExtensionHooks = FileExtensionHooks()