diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 000000000..8dad98088 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,5 @@ +[report] +fail_under = 89 + +[run] +source = pystac diff --git a/CHANGELOG.md b/CHANGELOG.md index 718f27d8f..3bc01a6dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ ### Added +- Raster extension support ([#364](https://github.com/stac-utils/pystac/issues/364)) +- solar_illumination field in eo extension ([#356](https://github.com/stac-utils/pystac/issues/356)) +- Added `Link.canonical` static method for creating links with "canonical" rel type ([#351](https://github.com/stac-utils/pystac/pull/351)) +- Added `RelType` enum containing common `rel` values ([#351](https://github.com/stac-utils/pystac/pull/351)) + ### Changed ### Fixed @@ -26,7 +31,7 @@ - Removed type information from docstrings, since it is redundant with function type annotations ([#342](https://github.com/stac-utils/pystac/pull/342)) -## [1.0.0-beta.1] +## [v1.0.0-beta.1] ### Added diff --git a/docs/api.rst b/docs/api.rst index c19c78656..96f282a6c 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -88,6 +88,13 @@ Provider :members: :undoc-members: +Summaries +~~~~~~~~~ + +.. autoclass:: pystac.Summaries + :members: + :undoc-members: + Item Spec --------- @@ -134,6 +141,13 @@ MediaType :members: :undoc-members: +RelType +~~~~~~~ + +.. autoclass:: pystac.RelType + :members: + :undoc-members: + IO -- @@ -186,46 +200,74 @@ ExtensionTypeError Extensions ---------- -**TEMPORARILY REMOVED** -.. .. autoclass:: pystac.extensions.Extensions -.. :members: -.. :undoc-members: - -ExtensionIndex -~~~~~~~~~~~~~~ +Base Classes +------------ -**TEMPORARILY REMOVED** +Abstract base classes that should be inherited to implement specific extensions. -.. An ExtensionIndex is accessed through the :attr:`STACObject.ext ` property and is the primary way to access information and functionality around STAC extensions. +SummariesExtension +~~~~~~~~~~~~~~~~~~ -.. .. autoclass:: pystac.stac_object.ExtensionIndex -.. :members: __getitem__, __getattr__, enable, implements +.. autoclass:: pystac.extensions.base.SummariesExtension + :members: +PropertiesExtension +~~~~~~~~~~~~~~~~~~~ -EO Extension ------------- +.. autoclass:: pystac.extensions.base.PropertiesExtension + :members: + :show-inheritance: -These classes are representations of the `EO Extension Spec `_. +ExtensionManagementMixin +~~~~~~~~~~~~~~~~~~~~~~~~ -EOItemExt -~~~~~~~~~ +.. autoclass:: pystac.extensions.base.ExtensionManagementMixin + :members: + :show-inheritance: -**TEMPORARILY REMOVED** +Electro-Optical Extension +------------------------- -.. .. autoclass:: pystac.extensions.eo.EOItemExt -.. :members: -.. :undoc-members: -.. :show-inheritance: +These classes are representations of the `EO Extension Spec `_. Band ~~~~ -**TEMPORARILY REMOVED** +.. autoclass:: pystac.extensions.eo.Band + :members: + :undoc-members: -.. .. autoclass:: pystac.extensions.eo.Band -.. :members: -.. :undoc-members: +EOExtension +~~~~~~~~~~~ + +.. autoclass:: pystac.extensions.eo.EOExtension + :members: + :undoc-members: + :show-inheritance: + +ItemEOExtension +~~~~~~~~~~~~~~~ + +.. autoclass:: pystac.extensions.eo.ItemEOExtension + :members: + :undoc-members: + :show-inheritance: + +AssetEOExtension +~~~~~~~~~~~~~~~~ + +.. autoclass:: pystac.extensions.eo.AssetEOExtension + :members: + :undoc-members: + :show-inheritance: +SummariesEOExtension +~~~~~~~~~~~~~~~~~~~~ + +.. autoclass:: pystac.extensions.eo.SummariesEOExtension + :members: + :undoc-members: + :show-inheritance: Label Extension --------------- @@ -242,6 +284,13 @@ LabelItemExt .. :undoc-members: .. :show-inheritance: +LabelRelType +~~~~~~~~~~~~ + +.. autoclass:: pystac.extensions.label.LabelRelType + :members: + :show-inheritance: + LabelType ~~~~~~~~~ @@ -376,6 +425,13 @@ Version Extension Implements the `Version Extension `_. +VersionRelType +~~~~~~~~~~~~~~ + +.. autoclass:: pystac.extensions.version.VersionRelType + :members: + :show-inheritance: + VersionCollectionExt ~~~~~~~~~~~~~~~~~~~~ @@ -459,7 +515,7 @@ PySTAC includes a ``pystac.validation`` package for validating STAC objects, inc from PySTAC objects and directly from JSON. .. automodule:: pystac.validation - :members: validate, validate_dict, validate_all, set_validator, STACValidationError + :members: validate, validate_dict, validate_all, set_validator STACValidator ~~~~~~~~~~~~~ diff --git a/docs/concepts.rst b/docs/concepts.rst index 0e7e27c71..0e3c15861 100644 --- a/docs/concepts.rst +++ b/docs/concepts.rst @@ -309,87 +309,129 @@ The :class:`~pystac.validation.JsonSchemaSTACValidator` takes a :class:`~pystac. Extensions ========== -Accessing Extension functionality ---------------------------------- - -All STAC objects are accessed through ``Catalog``, ``Collection`` and ``Item``, and all extension functionality -is accessed through the ``ext`` property on those objects. For instance, to access the band information -from the ``eo`` extension for an item that implements the extension, you use: +From the documentation on `STAC Spec Extensions +`__: -.. code-block:: python + Extensions to the core STAC specification provide additional JSON fields that can be + used to better describe the data. Most tend to be about describing a particular + domain or type of data, but some imply functionality. - # All of the below are equivalent: - item.ext['eo'].bands - item.ext[pystac.Extensions.EO].bands - item.ext.eo.bands +This library makes an effort to support all extensions that are part of the +`stac-extensions GitHub org +`__, and +we are committed to supporting all STAC Extensions at the "Candidate" maturity level or +above (see the `Extension Maturity +`__ documentation for details). -Notice the ``eo`` property on ``ext`` - this utilizes the `__getattr__ `_ method to delegate the property name to the ``__getitem__`` method, so we can access any registered extension as if it were a property on ``ext``. +Accessing Extension Functionality +--------------------------------- -Extensions wrap the objects they extend. Extensions hold -no values of their own, but instead use Python `properties `_ -to directly modify the values of the objects they wrap. +Extension functionality is encapsulated in classes that are specific to the STAC +Extension (e.g. Electro-Optical, Projection, etc.) and STAC Object +(:class:`~pystac.Collection`, :class:`pystac.Item`, or :class:`pystac.Asset`). All +classes that extend these objects inherit from +:class:`pystac.extensions.base.PropertiesExtension`, and you can use the +``ext`` method on these classes to extend an object. -Any object that is returned by extension methods therefore also wrap components of the STAC objects. -For instance, the ``LabelClasses`` holds a reference to the original ``Item``'s ``label:classes`` property, so that -modifying the ``LabelClasses`` -properties through the setters will modify the item properties directly. For example: +For instance, to extend an item with the :stac-ext:`Electro-Optical Extension ` +you would use :meth:`EOExtension.ext ` as +follows: .. code-block:: python - from pystac.extensions import label + import pystac + from pystac.extensions.eo import EOExtension - label_classes = item.ext.label.label_classes - label_classes[0].classes.append("other_class") - assert "other_class" in item.properties['label:classes'][0]['classes'] + item = Item(...) # See docs for creating an Item + eo_ext = EOExtension.ext(item) -Because these objects wrap the object's dictionary, the __init__ methods need to take the -``dict`` they wrap. Therefore to create a new object, use the class's `.create` method, for example: +This extended instance now gives you access to the properties defined in that extension: .. code-block:: python - item.ext.label.label_classes = [label.LabelClasses.create(['class1', 'class2'], name='label')] + eo_ext.bands + eo_ext.cloud_cover -An `apply` method is available in extension wrappers and any objects that they return. This allows -you to pass in property values pertaining to the extension. These will require arguments for properties -required as part of the extension specification and have `None` default values for optional parameters: +See the documentation for each extension implementation for details on the supported +properties and other functionality. + +Instances of :class:`~pystac.extensions.base.PropertiesExtension` have a +:attr:`~pystac.extensions.base.PropertiesExtension.properties` attribute that gives +access to the properties of the extended object. *This attribute is a reference to the +properties of the* :class:`~pystac.Item` *or* :class:`~pystac.Asset` *being extended and +can therefore mutate those properties.* For instance: .. code-block:: python - eo_ext = item.ext.eo - eo_ext.apply(0.5, bands, cloud_cover=None) # Do not have to specify cloud_cover + item = Item.from_file("tests/data-files/eo/eo-landsat-example.json") + print(item.properties["eo:cloud_cover"]) + # 78 + eo_ext = EOExtension.ext(item) + print(eo_ext.cloud_cover) + # 78 -If you attempt to retrieve an extension wrapper for an extension that the object doesn't implement, PySTAC will -throw a `pystac.extensions.ExtensionError`. + eo_ext.cloud_cover = 45 + print(item.properties["eo:cloud_cover"]) + # 45 -Enabling an extension ---------------------- +There is also a :attr:`~pystac.extensions.base.PropertiesExtension.additional_read_properties` +attribute that, if present, gives read-only access to properties of any objects that own the +extended object. For instance, an extended :class:`pystac.Asset` instance would have +read access to the properties of the :class:`pystac.Item` that owns it (if there is +one). If a property exists in both additional_read_properties and properties, the value +in additional_read_properties will take precedence. -You'll need to enable an extension on an object before using it. For example, if you are creating an Item and want to -apply the label extension, you can do so in two ways. -You can add the extension in the list of extensions when you create the Item: +An ``apply`` method is available on extended objects. This allows you to pass in +property values pertaining to the extension. Properties that are required by the +extension will be required arguments to the ``apply`` method. Optional properties will +have a default value of ``None``: .. code-block:: python - item = Item(id='Labels', - geometry=item.geometry, - bbox=item.bbox, - datetime=datetime.utcnow(), - properties={}, - stac_extensions=[pystac.Extensions.LABEL]) + # Can also omit cloud_cover entirely... + eo_ext.apply(0.5, bands, cloud_cover=None) + -or you can call ``ext.enable`` on an Item (which will work for any item, whether you created it or are modifying it): +If you attempt to extend an object that is not supported by an extension, PySTAC will +throw a :class:`pystac.ExtensionTypeError`. + +Adding an Extension +------------------- + +You can add an extension to a STAC object that does not already implement that extension +using the :meth:`ExtensionManagementMixin.add_to +` method. Any concrete +extension implementations that extend existing STAC objects should inherit from the +:class:`~pystac.extensions.base.ExtensionManagementMixin` class, and will therefore have +this method available. The +:meth:`~pystac.extensions.base.ExtensionManagementMixin.add_to` adds the correct schema +URI to the :attr:`~pystac.STACObject.stac_extensions` list for the object being extended. .. code-block:: python - item = Item(id='Labels', - geometry=item.geometry, - bbox=item.bbox, - datetime=datetime.utcnow(), - properties={}) + # Load a basic item without any extensions + item = Item.from_file("tests/data-files/item/sample-item.json") + print(item.stac_extensions) + # [] + + # Add the Electro-Optical extension + EOExtension.add_to(item) + print(item.stac_extensions) + # ['https://stac-extensions.github.io/eo/v1.0.0/schema.json'] + +Extended Summaries +------------------ + +Extension classes like :class:`~pystac.extensions.eo.EOExtension` may also provide a +``summaries`` static method that can be used to extend the Collection summaries. This +method returns a class inheriting from +:class:`pystac.extensions.base.SummariesExtension` that provides tools for summarizing +the properties defined by that extension. These classes also hold a reference to the +Collection's :class:`pystac.Summaries` instance in the ``summaries`` attribute. - item.ext.enable(pystac.Extensions.LABEL) +See :class:`pystac.extensions.eo.SummariesEOExtension` for an example implementation. Item Asset properties ===================== diff --git a/docs/conf.py b/docs/conf.py index 13b847d75..7597afe4a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -15,9 +15,11 @@ import os import sys import subprocess +from typing import Any, Dict + sys.path.insert(0, os.path.abspath('.')) sys.path.insert(0, os.path.abspath('../')) -from pystac.version import __version__ +from pystac.version import __version__, STACVersion git_branch = subprocess.check_output(['git', 'rev-parse', @@ -59,7 +61,10 @@ extlinks = { 'tutorial': ('https://github.com/azavea/pystac/' - 'tree/{}/docs/tutorials/%s'.format(git_branch), 'tutorial') + 'tree/{}/docs/tutorials/%s'.format(git_branch), 'tutorial'), + 'stac-spec': ('https://github.com/radiantearth/stac-spec/blob/' + 'v{}/%s'.format(STACVersion.DEFAULT_STAC_VERSION), 'path'), + 'stac-ext': ('https://github.com/stac-extensions/%s', '%s extension') } # Add any paths that contain templates here, relative to this directory. @@ -134,7 +139,7 @@ # -- Options for LaTeX output ------------------------------------------------ -latex_elements = { +latex_elements: Dict[str, Any] = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', diff --git a/mypy.ini b/mypy.ini index 132834a06..370f59b32 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,3 +1,4 @@ [mypy] +disallow_untyped_defs = True ignore_missing_imports = True -disallow_untyped_defs = True \ No newline at end of file +show_error_codes = True diff --git a/pystac/__init__.py b/pystac/__init__.py index 091acbf35..70aa39e1f 100644 --- a/pystac/__init__.py +++ b/pystac/__init__.py @@ -18,9 +18,10 @@ get_stac_version, # type:ignore set_stac_version, # type:ignore ) +from pystac.media_type import MediaType # type:ignore +from pystac.rel_type import RelType # type: ignore from pystac.stac_io import StacIO # type:ignore from pystac.stac_object import STACObject, STACObjectType # type:ignore -from pystac.media_type import MediaType # type:ignore from pystac.link import Link, HIERARCHICAL_LINKS # type:ignore from pystac.catalog import Catalog, CatalogType # type:ignore from pystac.collection import ( diff --git a/pystac/cache.py b/pystac/cache.py index c4b4aee78..4b1f801f3 100644 --- a/pystac/cache.py +++ b/pystac/cache.py @@ -63,7 +63,7 @@ def __init__( self, id_keys_to_objects: Optional[Dict[str, "STACObject_Type"]] = None, hrefs_to_objects: Optional[Dict[str, "STACObject_Type"]] = None, - ids_to_collections: Dict[str, "Collection_Type"] = None, + ids_to_collections: Optional[Dict[str, "Collection_Type"]] = None, ): self.id_keys_to_objects = id_keys_to_objects or {} self.hrefs_to_objects = hrefs_to_objects or {} @@ -235,8 +235,12 @@ class CollectionCache: def __init__( self, - cached_ids: Dict[str, Union["Collection_Type", Dict[str, Any]]] = None, - cached_hrefs: Dict[str, Union["Collection_Type", Dict[str, Any]]] = None, + cached_ids: Optional[ + Dict[str, Union["Collection_Type", Dict[str, Any]]] + ] = None, + cached_hrefs: Optional[ + Dict[str, Union["Collection_Type", Dict[str, Any]]] + ] = None, ): self.cached_ids = cached_ids or {} self.cached_hrefs = cached_hrefs or {} @@ -273,8 +277,12 @@ class ResolvedObjectCollectionCache(CollectionCache): def __init__( self, resolved_object_cache: ResolvedObjectCache, - cached_ids: Dict[str, Union["Collection_Type", Dict[str, Any]]] = None, - cached_hrefs: Dict[str, Union["Collection_Type", Dict[str, Any]]] = None, + cached_ids: Optional[ + Dict[str, Union["Collection_Type", Dict[str, Any]]] + ] = None, + cached_hrefs: Optional[ + Dict[str, Union["Collection_Type", Dict[str, Any]]] + ] = None, ): super().__init__(cached_ids, cached_hrefs) self.resolved_object_cache = resolved_object_cache diff --git a/pystac/catalog.py b/pystac/catalog.py index 57d9dbbf4..c6398775d 100644 --- a/pystac/catalog.py +++ b/pystac/catalog.py @@ -77,7 +77,7 @@ def determine_type(cls, stac_json: Dict[str, Any]) -> Optional["CatalogType"]: self_link = None relative = False for link in stac_json["links"]: - if link["rel"] == "self": + if link["rel"] == pystac.RelType.SELF: self_link = link else: relative |= not is_absolute_href(link["href"]) @@ -311,7 +311,7 @@ def get_children(self) -> Iterable[Union["Catalog", "Collection_Type"]]: """ return map( lambda x: cast(Union[pystac.Catalog, pystac.Collection], x), - self.get_stac_objects("child"), + self.get_stac_objects(pystac.RelType.CHILD), ) def get_child_links(self) -> List[Link]: @@ -320,7 +320,7 @@ def get_child_links(self) -> List[Link]: Return: List[Link]: List of links of this catalog with ``rel == 'child'`` """ - return self.get_links("child") + return self.get_links(pystac.RelType.CHILD) def clear_children(self) -> None: """Removes all children from this catalog. @@ -341,7 +341,7 @@ def remove_child(self, child_id: str) -> None: new_links: List[pystac.Link] = [] root = self.get_root() for link in self.links: - if link.rel != "child": + if link.rel != pystac.RelType.CHILD: new_links.append(link) else: link.resolve_stac_object(root=root) @@ -380,7 +380,9 @@ def get_items(self) -> Iterable["Item_Type"]: Return: Iterable[Item]: Generator of items whose parent is this catalog. """ - return map(lambda x: cast(pystac.Item, x), self.get_stac_objects("item")) + return map( + lambda x: cast(pystac.Item, x), self.get_stac_objects(pystac.RelType.ITEM) + ) def clear_items(self) -> None: """Removes all items from this catalog. @@ -394,7 +396,7 @@ def clear_items(self) -> None: item.set_parent(None) item.set_root(None) - self.links = [link for link in self.links if link.rel != "item"] + self.links = [link for link in self.links if link.rel != pystac.RelType.ITEM] def remove_item(self, item_id: str) -> None: """Removes an item from this catalog. @@ -405,7 +407,7 @@ def remove_item(self, item_id: str) -> None: new_links: List[pystac.Link] = [] root = self.get_root() for link in self.links: - if link.rel != "item": + if link.rel != pystac.RelType.ITEM: new_links.append(link) else: link.resolve_stac_object(root=root) @@ -436,12 +438,12 @@ def get_item_links(self) -> List[Link]: Return: List[Link]: List of links of this catalog with ``rel == 'item'`` """ - return self.get_links("item") + return self.get_links(pystac.RelType.ITEM) def to_dict(self, include_self_link: bool = True) -> Dict[str, Any]: links = self.links if not include_self_link: - links = [x for x in links if x.rel != "self"] + links = [x for x in links if x.rel != pystac.RelType.SELF] d: Dict[str, Any] = { "type": self.STAC_OBJECT_TYPE.value.title(), @@ -474,7 +476,7 @@ def clone(self) -> "Catalog": clone._resolved_objects.cache(clone) for link in self.links: - if link.rel == "root": + if link.rel == pystac.RelType.ROOT: # Catalog __init__ sets correct root to clone; don't reset # if the root link points to self root_is_self = link.is_resolved() and link.target is self @@ -636,7 +638,7 @@ def generate_subcatalogs( layout_template = LayoutTemplate(template, defaults=defaults) keep_item_links: List[Link] = [] - item_links = [lk for lk in self.links if lk.rel == "item"] + item_links = [lk for lk in self.links if lk.rel == pystac.RelType.ITEM] for link in item_links: link.resolve_stac_object(root=self.get_root()) item = cast(pystac.Item, link.target) @@ -667,14 +669,16 @@ def generate_subcatalogs( curr_parent = subcat # resolve collection link so when added back points to correct location - col_link = item.get_single_link("collection") + col_link = item.get_single_link(pystac.RelType.COLLECTION) if col_link is not None: col_link.resolve_stac_object() curr_parent.add_item(item) # keep only non-item links and item links that have not been moved elsewhere - self.links = [lk for lk in self.links if lk.rel != "item"] + keep_item_links + self.links = [ + lk for lk in self.links if lk.rel != pystac.RelType.ITEM + ] + keep_item_links return result @@ -770,10 +774,12 @@ def validate_all(self) -> None: for item in self.get_items(): item.validate() - def _object_links(self) -> List[str]: - return ["child", "item"] + ( - pystac.EXTENSION_HOOKS.get_extended_object_links(self) or [] - ) + def _object_links(self) -> List[Union[str, pystac.RelType]]: + return [ + pystac.RelType.CHILD, + pystac.RelType.ITEM, + *pystac.EXTENSION_HOOKS.get_extended_object_links(self), + ] def map_items( self, @@ -924,11 +930,11 @@ def from_dict( ) for link in links: - if link["rel"] == "root": + if link["rel"] == pystac.RelType.ROOT: # Remove the link that's generated in Catalog's constructor. - cat.remove_links("root") + cat.remove_links(pystac.RelType.ROOT) - if link["rel"] != "self" or href is None: + if link["rel"] != pystac.RelType.SELF or href is None: cat.add_link(Link.from_dict(link)) return cat diff --git a/pystac/collection.py b/pystac/collection.py index 117754db8..ababda2ae 100644 --- a/pystac/collection.py +++ b/pystac/collection.py @@ -431,9 +431,9 @@ def to_dict(self) -> Dict[str, Any]: return {"minimum": self.minimum, "maximum": self.maximum} @classmethod - def from_dict(cls, d: Dict[str, Any], typ: Type[T] = Any) -> "RangeSummary[T]": - minimum: Optional[T] = get_required(d.get("minimum"), "RangeSummary", "minimum") - maximum: Optional[T] = get_required(d.get("maximum"), "RangeSummary", "maximum") + def from_dict(cls, d: Dict[str, Any]) -> "RangeSummary[T]": + minimum: T = get_required(d.get("minimum"), "RangeSummary", "minimum") + maximum: T = get_required(d.get("maximum"), "RangeSummary", "maximum") return cls(minimum=minimum, maximum=maximum) @@ -483,7 +483,7 @@ def remove(self, prop_key: str) -> None: self.schemas.pop(prop_key, None) self.other.pop(prop_key, None) - def is_empty(self): + def is_empty(self) -> bool: return not ( any(self.lists) or any(self.ranges) or any(self.schemas) or any(self.other) ) @@ -642,7 +642,7 @@ def clone(self) -> "Collection": clone._resolved_objects.cache(clone) for link in self.links: - if link.rel == "root": + if link.rel == pystac.RelType.ROOT: # Collection __init__ sets correct root to clone; don't reset # if the root link points to self root_is_self = link.is_resolved() and link.target is self @@ -706,11 +706,11 @@ def from_dict( ) for link in links: - if link["rel"] == "root": + if link["rel"] == pystac.RelType.ROOT: # Remove the link that's generated in Catalog's constructor. - collection.remove_links("root") + collection.remove_links(pystac.RelType.ROOT) - if link["rel"] != "self" or href is None: + if link["rel"] != pystac.RelType.SELF or href is None: collection.add_link(Link.from_dict(link)) if assets is not None: diff --git a/pystac/extensions/base.py b/pystac/extensions/base.py index 191f4521e..4620e9930 100644 --- a/pystac/extensions/base.py +++ b/pystac/extensions/base.py @@ -1,17 +1,27 @@ from abc import ABC, abstractmethod from typing import Generic, Iterable, List, Optional, Dict, Any, Type, TypeVar, Union -import pystac +from pystac import Collection, RangeSummary, STACObject, Summaries class SummariesExtension: - def __init__(self, collection: pystac.Collection) -> None: + """Base class for extending the properties in :attr:`pystac.Collection.summaries` + to include properties defined by a STAC Extension. + + This class should generally not be instantiated directly. Instead, create an + extension-specific class that inherits from this class and instantiate that. See + :class:`~pystac.extensions.eo.SummariesEOExtension` for an example.""" + + summaries: Summaries + """The summaries for the :class:`~pystac.Collection` being extended.""" + + def __init__(self, collection: Collection) -> None: self.summaries = collection.summaries def _set_summary( self, prop_key: str, - v: Optional[Union[List[Any], pystac.RangeSummary[Any], Dict[str, Any]]], + v: Optional[Union[List[Any], RangeSummary[Any], Dict[str, Any]]], ) -> None: if v is None: self.summaries.remove(prop_key) @@ -23,11 +33,32 @@ def _set_summary( class PropertiesExtension(ABC): + """Abstract base class for extending the properties of an :class:`~pystac.Item` + to include properties defined by a STAC Extension. + + This class should not be instantiated directly. Instead, create an + extension-specific class that inherits from this class and instantiate that. See + :class:`~pystac.extensions.eo.PropertiesEOExtension` for an example. + """ + properties: Dict[str, Any] + """The properties that this extension wraps. + + The extension which implements PropertiesExtension can use ``_get_property`` and + ``_set_property`` to get and set values on this instance. Note that _set_properties + mutates the properties directly.""" + additional_read_properties: Optional[Iterable[Dict[str, Any]]] = None + """Additional read-only properties accessible from the extended object. + + These are used when extending an :class:`~pystac.Asset` to give access to the + properties of the owning :class:`~pystac.Item`. If a property exists in both + ``additional_read_properties`` and ``properties``, the value in + ``additional_read_properties`` will take precedence. + """ - def _get_property(self, prop_name: str, typ: Type[P] = Type[Any]) -> Optional[P]: - result: Optional[typ] = self.properties.get(prop_name) + def _get_property(self, prop_name: str, typ: Type[P]) -> Optional[P]: + result = self.properties.get(prop_name) if result is not None: return result if self.additional_read_properties is not None: @@ -46,17 +77,30 @@ def _set_property( self.properties[prop_name] = v -S = TypeVar("S", bound=pystac.STACObject) +S = TypeVar("S", bound=STACObject) class ExtensionManagementMixin(Generic[S], ABC): + """Abstract base class with methods for adding and removing extensions from STAC + Objects. This class is generic over the type of object being extended (e.g. + :class:`~pystac.Item`). + + Concrete extension implementations should inherit from this class and either + provide a concrete type or a bounded type variable. + + See :class:`~pystac.extensions.eo.EOExtension` for an example implementation. + """ + @classmethod @abstractmethod def get_schema_uri(cls) -> str: + """Gets the schema URI associated with this extension.""" pass @classmethod def add_to(cls, obj: S) -> None: + """Add the schema URI for this extension to the + :attr:`pystac.STACObject.stac_extensions` list for the given object.""" if obj.stac_extensions is None: obj.stac_extensions = [cls.get_schema_uri()] else: @@ -64,6 +108,8 @@ def add_to(cls, obj: S) -> None: @classmethod def remove_from(cls, obj: S) -> None: + """Remove the schema URI for this extension from the + :attr:`pystac.STACObject.stac_extensions` list for the given object.""" if obj.stac_extensions is not None: obj.stac_extensions = [ uri for uri in obj.stac_extensions if uri != cls.get_schema_uri() @@ -71,6 +117,8 @@ def remove_from(cls, obj: S) -> None: @classmethod def has_extension(cls, obj: S) -> bool: + """Check if the given object implements this extension by checking + :attr:`pystac.STACObject.stac_extensions` for this extension's schema URI.""" return ( obj.stac_extensions is not None and cls.get_schema_uri() in obj.stac_extensions diff --git a/pystac/extensions/datacube.py b/pystac/extensions/datacube.py index 118366c9f..4dcc0d0a2 100644 --- a/pystac/extensions/datacube.py +++ b/pystac/extensions/datacube.py @@ -386,4 +386,4 @@ class DatacubeExtensionHooks(ExtensionHooks): ) -DATACUBE_EXTENSION_HOOKS = DatacubeExtensionHooks() +DATACUBE_EXTENSION_HOOKS: ExtensionHooks = DatacubeExtensionHooks() diff --git a/pystac/extensions/eo.py b/pystac/extensions/eo.py index 026635250..5401560ad 100644 --- a/pystac/extensions/eo.py +++ b/pystac/extensions/eo.py @@ -4,7 +4,18 @@ """ import re -from typing import Any, Dict, Generic, List, Optional, Set, Tuple, TypeVar, cast +from typing import ( + Any, + Dict, + Generic, + Iterable, + List, + Optional, + Set, + Tuple, + TypeVar, + cast, +) import pystac from pystac.collection import RangeSummary @@ -20,16 +31,16 @@ T = TypeVar("T", pystac.Item, pystac.Asset) -SCHEMA_URI = "https://stac-extensions.github.io/eo/v1.0.0/schema.json" +SCHEMA_URI: str = "https://stac-extensions.github.io/eo/v1.0.0/schema.json" -BANDS_PROP = "eo:bands" -CLOUD_COVER_PROP = "eo:cloud_cover" +BANDS_PROP: str = "eo:bands" +CLOUD_COVER_PROP: str = "eo:cloud_cover" class Band: """Represents Band information attached to an Item that implements the eo extension. - Use Band.create to create a new Band. + Use :meth:`Band.create` to create a new Band. """ def __init__(self, properties: Dict[str, Any]) -> None: @@ -42,6 +53,7 @@ def apply( description: Optional[str] = None, center_wavelength: Optional[float] = None, full_width_half_max: Optional[float] = None, + solar_illumination: Optional[float] = None, ) -> None: """ Sets the properties for this Band. @@ -49,18 +61,21 @@ def apply( Args: name : The name of the band (e.g., "B01", "B02", "B1", "B5", "QA"). common_name : The name commonly used to refer to the band to make it - easier to search for bands across instruments. See the `list of - accepted common names `_. + easier to search for bands across instruments. See the :stac-ext:`list + of accepted common names `. description : Description to fully explain the band. center_wavelength : The center wavelength of the band, in micrometers (μm). full_width_half_max : Full width at half maximum (FWHM). The width of the band, as measured at half the maximum transmission, in micrometers (μm). + solar_illumination: The solar illumination of the band, + as measured at half the maximum transmission, in W/m2/micrometers. """ # noqa self.name = name self.common_name = common_name self.description = description self.center_wavelength = center_wavelength self.full_width_half_max = full_width_half_max + self.solar_illumination = solar_illumination @classmethod def create( @@ -70,6 +85,7 @@ def create( description: Optional[str] = None, center_wavelength: Optional[float] = None, full_width_half_max: Optional[float] = None, + solar_illumination: Optional[float] = None, ) -> "Band": """ Creates a new band. @@ -77,12 +93,14 @@ def create( Args: name : The name of the band (e.g., "B01", "B02", "B1", "B5", "QA"). common_name : The name commonly used to refer to the band to make it easier - to search for bands across instruments. See the `list of accepted common names - `_. + to search for bands across instruments. See the :stac-ext:`list of + accepted common names `. description : Description to fully explain the band. center_wavelength : The center wavelength of the band, in micrometers (μm). full_width_half_max : Full width at half maximum (FWHM). The width of the band, as measured at half the maximum transmission, in micrometers (μm). + solar_illumination: The solar illumination of the band, + as measured at half the maximum transmission, in W/m2/micrometers. """ # noqa b = cls({}) b.apply( @@ -91,6 +109,7 @@ def create( description=description, center_wavelength=center_wavelength, full_width_half_max=full_width_half_max, + solar_illumination=solar_illumination, ) return b @@ -110,8 +129,8 @@ def name(self, v: str) -> None: @property def common_name(self) -> Optional[str]: """Get or sets the name commonly used to refer to the band to make it easier - to search for bands across instruments. See the `list of accepted common names - `_. + to search for bands across instruments. See the :stac-ext:`list of accepted + common names `. Returns: Optional[str] @@ -175,6 +194,23 @@ def full_width_half_max(self, v: Optional[float]) -> None: else: self.properties.pop("full_width_half_max", None) + @property + def solar_illumination(self) -> Optional[float]: + """Get or sets the solar illumination of the band, + as measured at half the maximum transmission, in W/m2/micrometers. + + Returns: + [float] + """ + return self.properties.get("solar_illumination") + + @solar_illumination.setter + def solar_illumination(self, v: Optional[float]) -> None: + if v is not None: + self.properties["solar_illumination"] = v + else: + self.properties.pop("solar_illumination", None) + def __repr__(self) -> str: return "".format(self.name) @@ -191,7 +227,8 @@ def band_range(common_name: str) -> Optional[Tuple[float, float]]: """Gets the band range for a common band name. Args: - common_name : The common band name. Must be one of the `list of accepted common names `_. + common_name : The common band name. Must be one of the :stac-ext:`list of + accepted common names `. Returns: Tuple[float, float] or None: The band range for this name as (min, max), or @@ -223,7 +260,8 @@ def band_description(common_name: str) -> Optional[str]: """Returns a description of the band for one with a common name. Args: - common_name : The common band name. Must be one of the `list of accepted common names `_. + common_name : The common band name. Must be one of the :stac-ext:`list of + accepted common names `. Returns: str or None: If a recognized common name, returns a description including the @@ -238,56 +276,55 @@ def band_description(common_name: str) -> Optional[str]: class EOExtension( Generic[T], PropertiesExtension, ExtensionManagementMixin[pystac.Item] ): - """EOItemExt is the extension of the Item in the eo extension which - represents a snapshot of the earth for a single date and time. - - Args: - item : The item to be extended. - - Attributes: - item : The Item that is being extended. - - Note: - Using EOItemExt to directly wrap an item will add the 'eo' extension ID to - the item's stac_extensions. + """An abstract class that can be used to extend the properties of an + :class:`~pystac.Item` with properties from the :stac-ext:`Electro-Optical + Extension `. This class is generic over the type of STAC Object to be + extended (e.g. :class:`~pystac.Item`, :class:`~pystac.Collection`). + + This class will generally not be used directly. Instead, use the concrete + implementation associated with the STAC Object you want to extend (e.g. + :class:`~ItemEOExtension` to extend an :class:`~pystac.Item`). """ - def apply(self, bands: List[Band], cloud_cover: Optional[float] = None) -> None: + def apply( + self, bands: Optional[List[Band]] = None, cloud_cover: Optional[float] = None + ) -> None: """Applies label extension properties to the extended Item. Args: - bands : a list of :class:`~pystac.Band` objects that represent - the available bands. + bands : A list of available bands where each item is a :class:`~Band` + object. If given, requires at least one band. cloud_cover : The estimate of cloud cover as a percentage - (0-100) of the entire scene. If not available the field should not be - provided. + (0-100) of the entire scene. If not available the field should not + be provided. """ self.bands = bands self.cloud_cover = cloud_cover @property def bands(self) -> Optional[List[Band]]: - """Get or sets a list of :class:`~pystac.Band` objects that represent - the available bands. + """Gets or sets a list of available bands where each item is a :class:`~Band` + object (or ``None`` if no bands have been set). If not available the field + should not be provided. """ return self._get_bands() - def _get_bands(self) -> Optional[List[Band]]: - return map_opt( - lambda bands: [Band(b) for b in bands], - self._get_property(BANDS_PROP, List[Dict[str, Any]]), - ) - @bands.setter def bands(self, v: Optional[List[Band]]) -> None: self._set_property( BANDS_PROP, map_opt(lambda bands: [b.to_dict() for b in bands], v) ) + def _get_bands(self) -> Optional[List[Band]]: + return map_opt( + lambda bands: [Band(b) for b in bands], + self._get_property(BANDS_PROP, List[Dict[str, Any]]), + ) + @property def cloud_cover(self) -> Optional[float]: - """Get or sets the estimate of cloud cover as a percentage (0-100) of the - entire scene. If not available the field should not be provided. + """Get or sets the estimate of cloud cover as a percentage + (0-100) of the entire scene. If not available the field should not be provided. Returns: float or None @@ -304,6 +341,16 @@ def get_schema_uri(cls) -> str: @staticmethod def ext(obj: T) -> "EOExtension[T]": + """Extends the given STAC Object with properties from the :stac-ext:`Electro-Optical + Extension `. + + This extension can be applied to instances of :class:`~pystac.Item` or + :class:`~pystac.Asset`. + + Raises: + + pystac.ExtensionTypeError : If an invalid object type is passed. + """ if isinstance(obj, pystac.Item): return cast(EOExtension[T], ItemEOExtension(obj)) elif isinstance(obj, pystac.Asset): @@ -315,10 +362,25 @@ def ext(obj: T) -> "EOExtension[T]": @staticmethod def summaries(obj: pystac.Collection) -> "SummariesEOExtension": + """Returns the extended summaries object for the given collection.""" return SummariesEOExtension(obj) class ItemEOExtension(EOExtension[pystac.Item]): + """A concrete implementation of :class:`EOExtension` on an :class:`~pystac.Item` + that extends the properties of the Item to include properties defined in the + :stac-ext:`Electro-Optical Extension `. + + This class should generally not be instantiated directly. Instead, call + :meth:`EOExtension.ext` on an :class:`~pystac.Item` to extend it. + """ + + item: pystac.Item + """The :class:`~pystac.Item` being extended.""" + + properties: Dict[str, Any] + """The :class:`~pystac.Item` properties, including extension properties.""" + def __init__(self, item: pystac.Item): self.item = item self.properties = item.properties @@ -349,6 +411,24 @@ def __repr__(self) -> str: class AssetEOExtension(EOExtension[pystac.Asset]): + """A concrete implementation of :class:`EOExtension` on an :class:`~pystac.Asset` + that extends the Asset fields to include properties defined in the + :stac-ext:`Electro-Optical 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 + """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: Optional[Iterable[Dict[str, Any]]] = None + """If present, this will be a list containing 1 dictionary representing the + properties of the owning :class:`~pystac.Item`.""" + def __init__(self, asset: pystac.Asset): self.asset_href = asset.href self.properties = asset.properties @@ -356,15 +436,21 @@ def __init__(self, asset: pystac.Asset): self.additional_read_properties = [asset.owner.properties] def __repr__(self) -> str: - return "".format(self.asset_href) + return "".format(self.asset_href) class SummariesEOExtension(SummariesExtension): + """A concrete implementation of :class:`~SummariesExtension` that extends + the ``summaries`` field of a :class:`~pystac.Collection` to include properties + defined in the :stac-ext:`Electro-Optical Extension `. + """ + @property def bands(self) -> Optional[List[Band]]: """Get or sets a list of :class:`~pystac.Band` objects that represent the available bands. """ + return map_opt( lambda bands: [Band(b) for b in bands], self.summaries.get_list(BANDS_PROP, Dict[str, Any]), diff --git a/pystac/extensions/file.py b/pystac/extensions/file.py index 264ad5fdf..8f3027785 100644 --- a/pystac/extensions/file.py +++ b/pystac/extensions/file.py @@ -188,6 +188,7 @@ def data_type(self) -> Optional[List[FileDataType]]: Returns: FileDataType """ + return map_opt( lambda x: [FileDataType(t) for t in x], self.summaries.get_list(DATA_TYPE_PROP, str), @@ -261,4 +262,4 @@ def migrate( obj["assets"][key][CHECKSUM_PROP] = old_checksum[key] -FILE_EXTENSION_HOOKS = FileExtensionHooks() +FILE_EXTENSION_HOOKS: ExtensionHooks = FileExtensionHooks() diff --git a/pystac/extensions/hooks.py b/pystac/extensions/hooks.py index 33a7583d6..6f3a3866f 100644 --- a/pystac/extensions/hooks.py +++ b/pystac/extensions/hooks.py @@ -1,6 +1,6 @@ from abc import ABC, abstractmethod from functools import lru_cache -from typing import Any, Dict, Iterable, List, Optional, Set, TYPE_CHECKING +from typing import Any, Dict, Iterable, List, Optional, Set, TYPE_CHECKING, Union import pystac from pystac.serialization.identify import STACJSONDescription, STACVersionID @@ -18,10 +18,10 @@ def schema_uri(self) -> str: @property @abstractmethod - def prev_extension_ids(self) -> List[str]: - """A list of previous extension IDs (schema URIs or old short ids) + def prev_extension_ids(self) -> Set[str]: + """A set of previous extension IDs (schema URIs or old short ids) that should be migrated to the latest schema URI in the 'stac_extensions' - property. Override with a class attribute so that the list of previous + property. Override with a class attribute so that the set of previous IDs is only created once. """ pass @@ -37,7 +37,9 @@ def _get_stac_object_types(self) -> Set[str]: """Translation of stac_object_types to strings, cached""" return set([x.value for x in self.stac_object_types]) - def get_object_links(self, obj: "STACObject_Type") -> Optional[List[str]]: + def get_object_links( + self, obj: "STACObject_Type" + ) -> Optional[List[Union[str, pystac.RelType]]]: return None def migrate( @@ -78,8 +80,10 @@ def remove_extension_hooks(self, extension_id: str) -> None: if extension_id in self.hooks: del self.hooks[extension_id] - def get_extended_object_links(self, obj: "STACObject_Type") -> List[str]: - result: Optional[List[str]] = None + def get_extended_object_links( + self, obj: "STACObject_Type" + ) -> List[Union[str, pystac.RelType]]: + result: Optional[List[Union[str, pystac.RelType]]] = None for ext in obj.stac_extensions: if ext in self.hooks: ext_result = self.hooks[ext].get_object_links(obj) diff --git a/pystac/extensions/item_assets.py b/pystac/extensions/item_assets.py index a5f2b44be..5b362017d 100644 --- a/pystac/extensions/item_assets.py +++ b/pystac/extensions/item_assets.py @@ -27,7 +27,7 @@ def __init__(self, properties: Dict[str, Any]) -> None: @property def title(self) -> Optional[str]: - self.properties.get(ASSET_TITLE_PROP) + return self.properties.get(ASSET_TITLE_PROP) @title.setter def title(self, v: Optional[str]) -> None: @@ -38,7 +38,7 @@ def title(self, v: Optional[str]) -> None: @property def description(self) -> Optional[str]: - self.properties.get(ASSET_DESC_PROP) + return self.properties.get(ASSET_DESC_PROP) @description.setter def description(self, v: Optional[str]) -> None: @@ -49,7 +49,7 @@ def description(self, v: Optional[str]) -> None: @property def media_type(self) -> Optional[str]: - self.properties.get(ASSET_TYPE_PROP) + return self.properties.get(ASSET_TYPE_PROP) @media_type.setter def media_type(self, v: Optional[str]) -> None: @@ -60,7 +60,7 @@ def media_type(self, v: Optional[str]) -> None: @property def roles(self) -> Optional[List[str]]: - self.properties.get(ASSET_ROLES_PROP) + return self.properties.get(ASSET_ROLES_PROP) @roles.setter def roles(self, v: Optional[List[str]]) -> None: @@ -78,7 +78,7 @@ def create_asset(self, href: str) -> pystac.Asset: roles=self.roles, properties={ k: v - for k, v in self.properties + for k, v in self.properties.items() if k not in set( [ @@ -98,7 +98,7 @@ def __init__(self, collection: pystac.Collection) -> None: @property def item_assets(self) -> Dict[str, AssetDefinition]: - result = get_required( + result: Dict[str, Any] = get_required( self.collection.extra_fields.get(ITEM_ASSETS_PROP), self, ITEM_ASSETS_PROP ) return {k: AssetDefinition(v) for k, v in result.items()} @@ -141,4 +141,4 @@ def migrate( super().migrate(obj, version, info) -ITEM_ASSETS_EXTENSION_HOOKS = ItemAssetsExtensionHooks() +ITEM_ASSETS_EXTENSION_HOOKS: ExtensionHooks = ItemAssetsExtensionHooks() diff --git a/pystac/extensions/label.py b/pystac/extensions/label.py index 49ff258d7..ae91688d2 100644 --- a/pystac/extensions/label.py +++ b/pystac/extensions/label.py @@ -14,6 +14,21 @@ SCHEMA_URI = "https://stac-extensions.github.io/label/v1.0.0/schema.json" +class LabelRelType(str, Enum): + """A list of rel types defined in the Label Extension. + + See the`Label Extension Links + `__ documentation + for details. + """ + + def __str__(self) -> str: + return str(self.value) + + SOURCE = "source" + """Used to indicate a link to the source item to which a label item applies.""" + + class LabelType(str, Enum): """Enumerates valid label types (RASTER or VECTOR).""" @@ -693,7 +708,7 @@ def add_source( "source", source_item, title=title, - media_type="application/json", + media_type=pystac.MediaType.JSON, properties=properties, ) self.obj.add_link(link) @@ -773,9 +788,11 @@ class LabelExtensionHooks(ExtensionHooks): prev_extension_ids: Set[str] = set(["label"]) stac_object_types: Set[pystac.STACObjectType] = set([pystac.STACObjectType.ITEM]) - def get_object_links(self, so: pystac.STACObject) -> Optional[List[str]]: + def get_object_links( + self, so: pystac.STACObject + ) -> Optional[List[Union[str, pystac.RelType]]]: if isinstance(so, pystac.Item): - return ["source"] + return [LabelRelType.SOURCE] return None def migrate( diff --git a/pystac/extensions/pointcloud.py b/pystac/extensions/pointcloud.py index fc922cd55..f8ec27e34 100644 --- a/pystac/extensions/pointcloud.py +++ b/pystac/extensions/pointcloud.py @@ -563,4 +563,4 @@ class PointcloudExtensionHooks(ExtensionHooks): stac_object_types: Set[pystac.STACObjectType] = set([pystac.STACObjectType.ITEM]) -POINTCLOUD_EXTENSION_HOOKS = PointcloudExtensionHooks() +POINTCLOUD_EXTENSION_HOOKS: ExtensionHooks = PointcloudExtensionHooks() diff --git a/pystac/extensions/projection.py b/pystac/extensions/projection.py index 2f58de818..ac525fe38 100644 --- a/pystac/extensions/projection.py +++ b/pystac/extensions/projection.py @@ -294,4 +294,4 @@ class ProjectionExtensionHooks(ExtensionHooks): stac_object_types: Set[pystac.STACObjectType] = set([pystac.STACObjectType.ITEM]) -PROJECTION_EXTENSION_HOOKS = ProjectionExtensionHooks() +PROJECTION_EXTENSION_HOOKS: ExtensionHooks = ProjectionExtensionHooks() diff --git a/pystac/extensions/raster.py b/pystac/extensions/raster.py new file mode 100644 index 000000000..6fd1eba78 --- /dev/null +++ b/pystac/extensions/raster.py @@ -0,0 +1,737 @@ +"""Implements the Raster extension. + +https://github.com/stac-extensions/raster +""" + +import enum +from typing import Any, Dict, Generic, Iterable, List, Optional, TypeVar, cast + +import pystac +from pystac.extensions.base import ( + ExtensionManagementMixin, + PropertiesExtension, + SummariesExtension, +) +from pystac.utils import get_opt, get_required, map_opt + +T = TypeVar("T", pystac.Item, pystac.Asset) + +SCHEMA_URI = "https://stac-extensions.github.io/raster/v1.0.0/schema.json" + +BANDS_PROP = "raster:bands" + + +class Sampling(str, enum.Enum): + def __str__(self) -> str: + return str(self.value) + + AREA = "area" + POINT = "point" + + +class DataType(str, enum.Enum): + def __str__(self) -> str: + return str(self.value) + + INT8 = "int8" + INT16 = "int16" + INT32 = "int32" + INT64 = "int64" + UINT8 = "uint8" + UINT16 = "uint16" + UINT32 = "uint32" + UINT64 = "uint64" + FLOAT16 = "float16" + FLOAT32 = "float32" + FLOAT64 = "float64" + CINT16 = "cint16" + CINT32 = "cint32" + CFLOAT32 = "cfloat32" + CFLOAT64 = "cfloat64" + OTHER = "other" + + +class Statistics: + """Represents statistics information attached to a band in the raster extension. + + Use Statistics.create to create a new Statistics instance. + """ + + def __init__(self, properties: Dict[str, Optional[float]]) -> None: + self.properties = properties + + def apply( + self, + minimum: Optional[float] = None, + maximum: Optional[float] = None, + mean: Optional[float] = None, + stddev: Optional[float] = None, + valid_percent: Optional[float] = None, + ) -> None: + """ + Sets the properties for this raster Band. + + Args: + minimum : Minimum value of all the pixels in the band. + maximum : Maximum value of all the pixels in the band. + mean : Mean value of all the pixels in the band. + stddev : Standard Deviation value of all the pixels in the band. + valid_percent : Percentage of valid (not nodata) pixel. + """ # noqa + self.minimum = minimum + self.maximum = maximum + self.mean = mean + self.stddev = stddev + self.valid_percent = valid_percent + + @classmethod + def create( + cls, + minimum: Optional[float] = None, + maximum: Optional[float] = None, + mean: Optional[float] = None, + stddev: Optional[float] = None, + valid_percent: Optional[float] = None, + ) -> "Statistics": + """ + Creates a new band. + + Args: + minimum : Minimum value of all the pixels in the band. + maximum : Maximum value of all the pixels in the band. + mean : Mean value of all the pixels in the band. + stddev : Standard Deviation value of all the pixels in the band. + valid_percent : Percentage of valid (not nodata) pixel. + """ # noqa + b = cls({}) + b.apply( + minimum=minimum, + maximum=maximum, + mean=mean, + stddev=stddev, + valid_percent=valid_percent, + ) + return b + + @property + def minimum(self) -> Optional[float]: + """Get or sets the minimum pixel value + + Returns: + Optional[float] + """ + return self.properties.get("minimum") + + @minimum.setter + def minimum(self, v: Optional[float]) -> None: + if v is not None: + self.properties["minimum"] = v + else: + self.properties.pop("minimum", None) + + @property + def maximum(self) -> Optional[float]: + """Get or sets the maximum pixel value + + Returns: + Optional[float] + """ + return self.properties.get("maximum") + + @maximum.setter + def maximum(self, v: Optional[float]) -> None: + if v is not None: + self.properties["maximum"] = v + else: + self.properties.pop("maximum", None) + + @property + def mean(self) -> Optional[float]: + """Get or sets the mean pixel value + + Returns: + Optional[float] + """ + return self.properties.get("mean") + + @mean.setter + def mean(self, v: Optional[float]) -> None: + if v is not None: + self.properties["mean"] = v + else: + self.properties.pop("mean", None) + + @property + def stddev(self) -> Optional[float]: + """Get or sets the standard deviation pixel value + + Returns: + Optional[float] + """ + return self.properties.get("stddev") + + @stddev.setter + def stddev(self, v: Optional[float]) -> None: + if v is not None: + self.properties["stddev"] = v + else: + self.properties.pop("stddev", None) + + @property + def valid_percent(self) -> Optional[float]: + """Get or sets the Percentage of valid (not nodata) pixel + + Returns: + Optional[float] + """ + return self.properties.get("valid_percent") + + @valid_percent.setter + def valid_percent(self, v: Optional[float]) -> None: + if v is not None: + self.properties["valid_percent"] = v + else: + self.properties.pop("valid_percent", None) + + def to_dict(self) -> Dict[str, Any]: + """Returns the dictionary representing the JSON of those Statistics. + + Returns: + dict: The wrapped dict of the Statistics that can be written out as JSON. + """ + return self.properties + + @staticmethod + def from_dict(d: Dict[str, Any]) -> "Statistics": + """Constructs an Statistics from a dict. + + Returns: + Statistics: The Statistics deserialized from the JSON dict. + """ + return Statistics(properties=d) + + +class Histogram: + """Represents pixel distribution information attached to a band in the raster extension. + + Use Band.create to create a new Band. + """ + + def __init__(self, properties: Dict[str, Any]) -> None: + self.properties = properties + + def apply( + self, + count: int, + min: float, + max: float, + buckets: List[int], + ) -> None: + """ + Sets the properties for this raster Band. + + Args: + count : number of buckets of the distribution. + min : minimum value of the distribution. + Also the mean value of the first bucket. + max : maximum value of the distribution. + Also the mean value of the last bucket. + buckets : Array of integer indicating the number + of pixels included in the bucket. + """ # noqa + self.count = count + self.min = min + self.max = max + self.buckets = buckets + + @classmethod + def create( + cls, + count: int, + min: float, + max: float, + buckets: List[int], + ) -> "Histogram": + """ + Creates a new band. + + Args: + count : number of buckets of the distribution. + min : minimum value of the distribution. + Also the mean value of the first bucket. + max : maximum value of the distribution. + Also the mean value of the last bucket. + buckets : Array of integer indicating the number + of pixels included in the bucket. + """ # noqa + b = cls({}) + b.apply( + count=count, + min=min, + max=max, + buckets=buckets, + ) + return b + + @property + def count(self) -> int: + """Get or sets the number of buckets of the distribution. + + Returns: + int + """ + return get_required(self.properties["count"], self, "count") + + @count.setter + def count(self, v: int) -> None: + self.properties["count"] = v + + @property + def min(self) -> float: + """Get or sets the minimum value of the distribution. + + Returns: + float + """ + return get_required(self.properties["min"], self, "min") + + @min.setter + def min(self, v: float) -> None: + self.properties["min"] = v + + @property + def max(self) -> float: + """Get or sets the maximum value of the distribution. + + Returns: + float + """ + return get_required(self.properties["max"], self, "max") + + @max.setter + def max(self, v: float) -> None: + self.properties["max"] = v + + @property + def buckets(self) -> List[int]: + """Get or sets the Array of integer indicating + the number of pixels included in the bucket. + + Returns: + List[int] + """ + return get_required(self.properties["buckets"], self, "buckets") + + @buckets.setter + def buckets(self, v: List[int]) -> None: + self.properties["buckets"] = v + + def to_dict(self) -> Dict[str, Any]: + """Returns the dictionary representing the JSON of this histogram. + + Returns: + dict: The wrapped dict of the Histogram that can be written out as JSON. + """ + return self.properties + + @staticmethod + def from_dict(d: Dict[str, Any]) -> "Histogram": + """Constructs an Histogram from a dict. + + Returns: + Histogram: The Histogram deserialized from the JSON dict. + """ + return Histogram(properties=d) + + +class RasterBand: + """Represents a Raster Band information attached to an Item + that implements the raster extension. + + Use Band.create to create a new Band. + """ + + def __init__(self, properties: Dict[str, Any]) -> None: + self.properties = properties + + def apply( + self, + nodata: Optional[float] = None, + sampling: Optional[Sampling] = None, + data_type: Optional[DataType] = None, + bits_per_sample: Optional[float] = None, + spatial_resolution: Optional[float] = None, + statistics: Optional[Statistics] = None, + unit: Optional[str] = None, + scale: Optional[float] = None, + offset: Optional[float] = None, + histogram: Optional[Histogram] = None, + ) -> None: + """ + Sets the properties for this raster Band. + + Args: + nodata : Pixel values used to identify pixels that are nodata in the assets. + sampling : One of area or point. Indicates whether a pixel value should be assumed + to represent a sampling over the region of the pixel or a point sample at the center of the pixel. + data_type :The data type of the band. + One of the data types as described in . + bits_per_sample : The actual number of bits used for this band. + Normally only present when the number of bits is non-standard for the datatype, + such as when a 1 bit TIFF is represented as byte + spatial_resolution : Average spatial resolution (in meters) of the pixels in the band. + statistics: Statistics of all the pixels in the band + unit: unit denomination of the pixel value + scale: multiplicator factor of the pixel value to transform into the value + (i.e. translate digital number to reflectance). + offset: number to be added to the pixel value (after scaling) to transform into the value + (i.e. translate digital number to reflectance). + histogram: Histogram distribution information of the pixels values in the band + """ # noqa + self.nodata = nodata + self.sampling = sampling + self.data_type = data_type + self.bits_per_sample = bits_per_sample + self.spatial_resolution = spatial_resolution + self.statistics = statistics + self.unit = unit + self.scale = scale + self.offset = offset + self.histogram = histogram + + @classmethod + def create( + cls, + nodata: Optional[float] = None, + sampling: Optional[Sampling] = None, + data_type: Optional[DataType] = None, + bits_per_sample: Optional[float] = None, + spatial_resolution: Optional[float] = None, + statistics: Optional[Statistics] = None, + unit: Optional[str] = None, + scale: Optional[float] = None, + offset: Optional[float] = None, + histogram: Optional[Histogram] = None, + ) -> "RasterBand": + """ + Creates a new band. + + Args: + nodata : Pixel values used to identify pixels that are nodata in the assets. + sampling : One of area or point. Indicates whether a pixel value should be assumed + to represent a sampling over the region of the pixel or a point sample at the center of the pixel. + data_type :The data type of the band. + One of the data types as described in . + bits_per_sample : The actual number of bits used for this band. + Normally only present when the number of bits is non-standard for the datatype, + such as when a 1 bit TIFF is represented as byte + spatial_resolution : Average spatial resolution (in meters) of the pixels in the band. + statistics: Statistics of all the pixels in the band + unit: unit denomination of the pixel value + scale: multiplicator factor of the pixel value to transform into the value + (i.e. translate digital number to reflectance). + offset: number to be added to the pixel value (after scaling) to transform into the value + (i.e. translate digital number to reflectance). + histogram: Histogram distribution information of the pixels values in the band + """ # noqa + b = cls({}) + b.apply( + nodata=nodata, + sampling=sampling, + data_type=data_type, + bits_per_sample=bits_per_sample, + spatial_resolution=spatial_resolution, + statistics=statistics, + unit=unit, + scale=scale, + offset=offset, + histogram=histogram, + ) + return b + + @property + def nodata(self) -> Optional[float]: + """Get or sets the nodata pixel value + + Returns: + Optional[float] + """ + return self.properties.get("nodata") + + @nodata.setter + def nodata(self, v: Optional[float]) -> None: + if v is not None: + self.properties["nodata"] = v + else: + self.properties.pop("nodata", None) + + @property + def sampling(self) -> Optional[Sampling]: + """Get or sets the property indicating whether a pixel value should be assumed + to represent a sampling over the region of the pixel or a point sample + at the center of the pixel. + + Returns: + Optional[Sampling] + """ # noqa + return self.properties.get("sampling") + + @sampling.setter + def sampling(self, v: Optional[Sampling]) -> None: + if v is not None: + self.properties["sampling"] = v + else: + self.properties.pop("sampling", None) + + @property + def data_type(self) -> Optional[DataType]: + """Get or sets the data type of the band. + + Returns: + Optional[DataType] + """ + return self.properties.get("data_type") + + @data_type.setter + def data_type(self, v: Optional[DataType]) -> None: + if v is not None: + self.properties["data_type"] = v + else: + self.properties.pop("data_type", None) + + @property + def bits_per_sample(self) -> Optional[float]: + """Get or sets the actual number of bits used for this band. + + Returns: + float + """ + return self.properties.get("bits_per_sample") + + @bits_per_sample.setter + def bits_per_sample(self, v: Optional[float]) -> None: + if v is not None: + self.properties["bits_per_sample"] = v + else: + self.properties.pop("bits_per_sample", None) + + @property + def spatial_resolution(self) -> Optional[float]: + """Get or sets the average spatial resolution (in meters) of the pixels in the band. + + Returns: + [float] + """ + return self.properties.get("spatial_resolution") + + @spatial_resolution.setter + def spatial_resolution(self, v: Optional[float]) -> None: + if v is not None: + self.properties["spatial_resolution"] = v + else: + self.properties.pop("spatial_resolution", None) + + @property + def statistics(self) -> Optional[Statistics]: + """Get or sets the average spatial resolution (in meters) of the pixels in the band. + + Returns: + [Statistics] + """ + return Statistics.from_dict(get_opt(self.properties.get("statistics"))) + + @statistics.setter + def statistics(self, v: Optional[Statistics]) -> None: + if v is not None: + self.properties["statistics"] = v.to_dict() + else: + self.properties.pop("statistics", None) + + @property + def unit(self) -> Optional[str]: + """Get or sets the unit denomination of the pixel value + + Returns: + [str] + """ + return self.properties.get("unit") + + @unit.setter + def unit(self, v: Optional[str]) -> None: + if v is not None: + self.properties["unit"] = v + else: + self.properties.pop("unit", None) + + @property + def scale(self) -> Optional[float]: + """Get or sets the multiplicator factor of the pixel value to transform + into the value (i.e. translate digital number to reflectance). + + Returns: + [float] + """ + return self.properties.get("scale") + + @scale.setter + def scale(self, v: Optional[float]) -> None: + if v is not None: + self.properties["scale"] = v + else: + self.properties.pop("scale", None) + + @property + def offset(self) -> Optional[float]: + """Get or sets the number to be added to the pixel value (after scaling) + to transform into the value (i.e. translate digital number to reflectance). + + Returns: + [float] + """ + return self.properties.get("offset") + + @offset.setter + def offset(self, v: Optional[float]) -> None: + if v is not None: + self.properties["offset"] = v + else: + self.properties.pop("offset", None) + + @property + def histogram(self) -> Optional[Histogram]: + """Get or sets the histogram distribution information of the pixels values in the band + + Returns: + [Histogram] + """ + return Histogram.from_dict(get_opt(self.properties.get("histogram"))) + + @histogram.setter + def histogram(self, v: Optional[Histogram]) -> None: + if v is not None: + self.properties["histogram"] = v.to_dict() + else: + self.properties.pop("histogram", None) + + def __repr__(self) -> str: + return "" + + def to_dict(self) -> Dict[str, Any]: + """Returns the dictionary representing the JSON of this Band. + + Returns: + dict: The wrapped dict of the Band that can be written out as JSON. + """ + return self.properties + + +class RasterExtension( + Generic[T], PropertiesExtension, ExtensionManagementMixin[pystac.Item] +): + """An abstract class that can be used to extend the properties of an + :class:`~pystac.Item` or :class:`~pystac.Asset` 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`). + + This class will generally not be used directly. Instead, use the concrete + implementation associated with the STAC Object you want to extend (e.g. + :class:`~ItemRasterExtension` to extend an :class:`~pystac.Item`). + """ + + def apply(self, bands: List[RasterBand]) -> None: + """Applies raster extension properties to the extended :class:`pystac.Item` or + :class:`pystac.Asset`. + + Args: + bands : a list of :class:`~pystac.RasterBand` objects that represent + the available raster bands. + """ + self.bands = bands + + @property + def bands(self) -> Optional[List[RasterBand]]: + """Gets or sets a list of available bands where each item is a :class:`~RasterBand` + object (or ``None`` if no bands have been set). If not available the field + should not be provided. + """ + return self._get_bands() + + @bands.setter + def bands(self, v: Optional[List[RasterBand]]) -> None: + self._set_property( + BANDS_PROP, map_opt(lambda bands: [b.to_dict() for b in bands], v) + ) + + def _get_bands(self) -> Optional[List[RasterBand]]: + return map_opt( + lambda bands: [RasterBand(b) for b in bands], + self._get_property(BANDS_PROP, List[Dict[str, Any]]), + ) + + @classmethod + def get_schema_uri(cls) -> str: + return SCHEMA_URI + + @staticmethod + def ext(obj: T) -> "RasterExtension[T]": + if isinstance(obj, pystac.Asset): + return cast(RasterExtension[T], AssetRasterExtension(obj)) + else: + raise pystac.ExtensionTypeError( + f"Raster extension does not apply to type {type(obj)}" + ) + + @staticmethod + def summaries(obj: pystac.Collection) -> "SummariesRasterExtension": + return SummariesRasterExtension(obj) + + +class AssetRasterExtension(RasterExtension[pystac.Asset]): + """A concrete implementation of :class:`RasterExtension` on an :class:`~pystac.Asset` + that extends the Asset fields to include properties defined in the + :stac-ext:`Raster Extension `. + + This class should generally not be instantiated directly. Instead, call + :meth:`RasterExtension.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: Optional[Iterable[Dict[str, Any]]] = None + """If present, this will be a list containing 1 dictionary representing the + properties of the owning :class:`~pystac.Item`.""" + + def __init__(self, asset: pystac.Asset): + self.asset_href = asset.href + self.properties = asset.properties + if asset.owner and isinstance(asset.owner, pystac.Item): + self.additional_read_properties = [asset.owner.properties] + + def __repr__(self) -> str: + return "".format(self.asset_href) + + +class SummariesRasterExtension(SummariesExtension): + """A concrete implementation of :class:`~SummariesExtension` that extends + the ``summaries`` field of a :class:`~pystac.Collection` to include properties + defined in the :stac-ext:`Raster Extension `. + """ + + @property + def bands(self) -> Optional[List[RasterBand]]: + """Get or sets a list of :class:`~pystac.Band` objects that represent + the available bands. + """ + return map_opt( + lambda bands: [RasterBand(b) for b in bands], + self.summaries.get_list(BANDS_PROP, Dict[str, Any]), + ) + + @bands.setter + def bands(self, v: Optional[List[RasterBand]]) -> None: + self._set_summary(BANDS_PROP, map_opt(lambda x: [b.to_dict() for b in x], v)) diff --git a/pystac/extensions/sar.py b/pystac/extensions/sar.py index 8c1a5f5b1..089a567dc 100644 --- a/pystac/extensions/sar.py +++ b/pystac/extensions/sar.py @@ -8,8 +8,7 @@ import pystac from pystac.serialization.identify import STACJSONDescription, STACVersionID -from pystac.extensions.base import ExtensionManagementMixin -from pystac.extensions.projection import ProjectionExtension +from pystac.extensions.base import ExtensionManagementMixin, PropertiesExtension from pystac.extensions.hooks import ExtensionHooks from pystac.utils import get_required, map_opt @@ -59,7 +58,7 @@ class ObservationDirection(enum.Enum): class SarExtension( - Generic[T], ProjectionExtension[T], ExtensionManagementMixin[pystac.Item] + Generic[T], PropertiesExtension, ExtensionManagementMixin[pystac.Item] ): """SarItemExt extends Item to add sar properties to a STAC Item. diff --git a/pystac/extensions/sat.py b/pystac/extensions/sat.py index 4c61a3527..2f583fdfe 100644 --- a/pystac/extensions/sat.py +++ b/pystac/extensions/sat.py @@ -133,4 +133,4 @@ class SatExtensionHooks(ExtensionHooks): stac_object_types: Set[pystac.STACObjectType] = set([pystac.STACObjectType.ITEM]) -SAT_EXTENSION_HOOKS = SatExtensionHooks() +SAT_EXTENSION_HOOKS: ExtensionHooks = SatExtensionHooks() diff --git a/pystac/extensions/scientific.py b/pystac/extensions/scientific.py index 9174ef6a5..6e8d6aecc 100644 --- a/pystac/extensions/scientific.py +++ b/pystac/extensions/scientific.py @@ -233,4 +233,4 @@ class ScientificExtensionHooks(ExtensionHooks): ) -SCIENTIFIC_EXTENSION_HOOKS = ScientificExtensionHooks() +SCIENTIFIC_EXTENSION_HOOKS: ExtensionHooks = ScientificExtensionHooks() diff --git a/pystac/extensions/timestamps.py b/pystac/extensions/timestamps.py index 7516ff47d..211049900 100644 --- a/pystac/extensions/timestamps.py +++ b/pystac/extensions/timestamps.py @@ -154,4 +154,4 @@ class TimestampsExtensionHooks(ExtensionHooks): stac_object_types: Set[pystac.STACObjectType] = set([pystac.STACObjectType.ITEM]) -TIMESTAMPS_EXTENSION_HOOKS = TimestampsExtensionHooks() +TIMESTAMPS_EXTENSION_HOOKS: ExtensionHooks = TimestampsExtensionHooks() diff --git a/pystac/extensions/version.py b/pystac/extensions/version.py index 85ee2aa08..ffa2ee3c3 100644 --- a/pystac/extensions/version.py +++ b/pystac/extensions/version.py @@ -2,7 +2,7 @@ https://github.com/stac-extensions/version """ - +from enum import Enum from pystac.utils import get_required, map_opt from typing import Generic, List, Optional, Set, TypeVar, Union, cast @@ -22,13 +22,27 @@ VERSION: str = "version" DEPRECATED: str = "deprecated" -# Link "rel" attribute values. -LATEST: str = "latest-version" -PREDECESSOR: str = "predecessor-version" -SUCCESSOR: str = "successor-version" -# Media type for links. -MEDIA_TYPE: str = "application/json" +class VersionRelType(str, Enum): + """A list of rel types defined in the Version Extension. + + See the `Version Extension Relation types + `__ documentation + for details.""" + + def __str__(self) -> str: + return str(self.value) + + LATEST = "latest-version" + """Indicates a link pointing to a resource containing the latest version.""" + + PREDECESSOR = "predecessor-version" + """Indicates a link pointing to a resource containing the predecessor version in the + version history.""" + + SUCCESSOR = "successor-version" + """Indicates a link pointing to a resource containing the successor version in the + version history.""" class VersionExtension( @@ -110,41 +124,49 @@ def deprecated(self, v: Optional[bool]) -> None: def latest(self) -> Optional[T]: """Get or sets the most recent version.""" return map_opt( - lambda x: cast(T, x), next(iter(self.obj.get_stac_objects(LATEST)), None) + lambda x: cast(T, x), + next(iter(self.obj.get_stac_objects(VersionRelType.LATEST)), None), ) @latest.setter def latest(self, item: Optional[T]) -> None: - self.obj.clear_links(LATEST) + self.obj.clear_links(VersionRelType.LATEST) if item is not None: - self.obj.add_link(pystac.Link(LATEST, item, MEDIA_TYPE)) + self.obj.add_link( + pystac.Link(VersionRelType.LATEST, item, pystac.MediaType.JSON) + ) @property def predecessor(self) -> Optional[T]: """Get or sets the previous item.""" return map_opt( lambda x: cast(T, x), - next(iter(self.obj.get_stac_objects(PREDECESSOR)), None), + next(iter(self.obj.get_stac_objects(VersionRelType.PREDECESSOR)), None), ) @predecessor.setter def predecessor(self, item: Optional[T]) -> None: - self.obj.clear_links(PREDECESSOR) + self.obj.clear_links(VersionRelType.PREDECESSOR) if item is not None: - self.obj.add_link(pystac.Link(PREDECESSOR, item, MEDIA_TYPE)) + self.obj.add_link( + pystac.Link(VersionRelType.PREDECESSOR, item, pystac.MediaType.JSON) + ) @property def successor(self) -> Optional[T]: """Get or sets the next item.""" return map_opt( - lambda x: cast(T, x), next(iter(self.obj.get_stac_objects(SUCCESSOR)), None) + lambda x: cast(T, x), + next(iter(self.obj.get_stac_objects(VersionRelType.SUCCESSOR)), None), ) @successor.setter def successor(self, item: Optional[T]) -> None: - self.obj.clear_links(SUCCESSOR) + self.obj.clear_links(VersionRelType.SUCCESSOR) if item is not None: - self.obj.add_link(pystac.Link(SUCCESSOR, item, MEDIA_TYPE)) + self.obj.add_link( + pystac.Link(VersionRelType.SUCCESSOR, item, pystac.MediaType.JSON) + ) @classmethod def get_schema_uri(cls) -> str: @@ -193,7 +215,11 @@ class VersionExtensionHooks(ExtensionHooks): def get_object_links(self, so: pystac.STACObject) -> Optional[List[str]]: if isinstance(so, pystac.Collection) or isinstance(so, pystac.Item): - return [LATEST, PREDECESSOR, SUCCESSOR] + return [ + VersionRelType.LATEST, + VersionRelType.PREDECESSOR, + VersionRelType.SUCCESSOR, + ] return None diff --git a/pystac/extensions/view.py b/pystac/extensions/view.py index 77c26b34d..4101b9f7a 100644 --- a/pystac/extensions/view.py +++ b/pystac/extensions/view.py @@ -195,4 +195,4 @@ class ViewExtensionHooks(ExtensionHooks): stac_object_types: Set[pystac.STACObjectType] = set([pystac.STACObjectType.ITEM]) -VIEW_EXTENSION_HOOKS = ViewExtensionHooks() +VIEW_EXTENSION_HOOKS: ExtensionHooks = ViewExtensionHooks() diff --git a/pystac/item.py b/pystac/item.py index 54df1dca8..17beed693 100644 --- a/pystac/item.py +++ b/pystac/item.py @@ -822,7 +822,7 @@ def set_collection(self, collection: Optional[Collection]) -> "Item": Returns: Item: self """ - self.remove_links("collection") + self.remove_links(pystac.RelType.COLLECTION) self.collection_id = None if collection is not None: self.add_link(Link.collection(collection)) @@ -837,7 +837,7 @@ def get_collection(self) -> Optional[Collection]: Collection or None: If this item belongs to a collection, returns a reference to the collection. Otherwise returns None. """ - collection_link = self.get_single_link("collection") + collection_link = self.get_single_link(pystac.RelType.COLLECTION) if collection_link is None: return None else: @@ -846,7 +846,7 @@ def get_collection(self) -> Optional[Collection]: def to_dict(self, include_self_link: bool = True) -> Dict[str, Any]: links = self.links if not include_self_link: - links = [x for x in links if x.rel != "self"] + links = [x for x in links if x.rel != pystac.RelType.SELF] assets = {k: v.to_dict() for k, v in self.assets.items()} @@ -897,8 +897,11 @@ def clone(self) -> "Item": return clone - def _object_links(self) -> List[str]: - return ["collection"] + (pystac.EXTENSION_HOOKS.get_extended_object_links(self)) + def _object_links(self) -> List[Union[str, pystac.RelType]]: + return [ + pystac.RelType.COLLECTION, + *pystac.EXTENSION_HOOKS.get_extended_object_links(self), + ] @classmethod def from_dict( @@ -944,7 +947,7 @@ def from_dict( has_self_link = False for link in links: - has_self_link |= link["rel"] == "self" + has_self_link |= link["rel"] == pystac.RelType.SELF item.add_link(Link.from_dict(link)) if not has_self_link and href is not None: diff --git a/pystac/layout.py b/pystac/layout.py index 64343707a..ebdde282b 100644 --- a/pystac/layout.py +++ b/pystac/layout.py @@ -74,7 +74,9 @@ class LayoutTemplate: # Special template vars specific to Items ITEM_TEMPLATE_VARS = ["date", "year", "month", "day", "collection"] - def __init__(self, template: str, defaults: Dict[str, str] = None) -> None: + def __init__( + self, template: str, defaults: Optional[Dict[str, str]] = None + ) -> None: self.template = template self.defaults = defaults or {} diff --git a/pystac/link.py b/pystac/link.py index db7ff68a4..514dd847b 100644 --- a/pystac/link.py +++ b/pystac/link.py @@ -10,11 +10,18 @@ from pystac.catalog import Catalog as Catalog_Type from pystac.collection import Collection as Collection_Type -HIERARCHICAL_LINKS = ["root", "child", "parent", "collection", "item", "items"] +HIERARCHICAL_LINKS = [ + pystac.RelType.ROOT, + pystac.RelType.CHILD, + pystac.RelType.PARENT, + pystac.RelType.COLLECTION, + pystac.RelType.ITEM, + pystac.RelType.ITEMS, +] class Link: - """A link is connects a :class:`~pystac.STACObject` to another entity. + """A link connects a :class:`~pystac.STACObject` to another entity. The target of a link can be either another STACObject, or an HREF. When serialized, links always refer to the HREF of the target. @@ -28,7 +35,8 @@ class Link: ideally the lazy deserialization of STACObjects is transparent to clients of PySTAC. Args: - rel : The relation of the link (e.g. 'child', 'item') + rel : The relation of the link (e.g. 'child', 'item'). Registered rel Types + are preferred. See :class:`~pystac.RelType` for common media types. target : The target of the link. If the link is unresolved, or the link is to something that is not a STACObject, the target is an HREF. If resolved, the target is a STACObject. @@ -40,7 +48,8 @@ class Link: object JSON. Attributes: - rel : The relation of the link (e.g. 'child', 'item') + rel : The relation of the link (e.g. 'child', 'item'). Registered rel Types + are preferred. See :class:`~pystac.RelType` for common media types. target : The target of the link. If the link is unresolved, or the link is to something that is not a STACObject, the target is an HREF. If resolved, the target is a STACObject. @@ -60,7 +69,7 @@ class Link: def __init__( self, - rel: str, + rel: Union[str, pystac.RelType], target: Union[str, "STACObject_Type"], media_type: Optional[str] = None, title: Optional[str] = None, @@ -111,10 +120,10 @@ def get_href(self) -> Optional[str]: if href and is_absolute_href(href) and self.owner and self.owner.get_root(): root = self.owner.get_root() - rel_links = ( - HIERARCHICAL_LINKS - + pystac.EXTENSION_HOOKS.get_extended_object_links(self.owner) - ) + rel_links = [ + *HIERARCHICAL_LINKS, + *pystac.EXTENSION_HOOKS.get_extended_object_links(self.owner), + ] # if a hierarchical link with an owner and root, and relative catalog if root and root.is_relative() and self.rel in rel_links: owner_href = self.owner.get_self_href() @@ -213,7 +222,7 @@ def resolve_stac_object(self, root: Optional["Catalog_Type"] = None) -> "Link": if ( self.owner - and self.rel in ["child", "item"] + and self.rel in [pystac.RelType.CHILD, pystac.RelType.ITEM] and isinstance(self.owner, pystac.Catalog) ): self.target.set_parent(self.owner) @@ -299,29 +308,46 @@ def from_dict(d: Dict[str, Any]) -> "Link": @staticmethod def root(c: "Catalog_Type") -> "Link": """Creates a link to a root Catalog or Collection.""" - return Link("root", c, media_type="application/json") + return Link(pystac.RelType.ROOT, c, media_type=pystac.MediaType.JSON) @staticmethod def parent(c: "Catalog_Type") -> "Link": """Creates a link to a parent Catalog or Collection.""" - return Link("parent", c, media_type="application/json") + return Link(pystac.RelType.PARENT, c, media_type=pystac.MediaType.JSON) @staticmethod def collection(c: "Collection_Type") -> "Link": """Creates a link to an item's Collection.""" - return Link("collection", c, media_type="application/json") + return Link(pystac.RelType.COLLECTION, c, media_type=pystac.MediaType.JSON) @staticmethod def self_href(href: str) -> "Link": """Creates a self link to a file's location.""" - return Link("self", href, media_type="application/json") + return Link(pystac.RelType.SELF, href, media_type=pystac.MediaType.JSON) @staticmethod def child(c: "Catalog_Type", title: Optional[str] = None) -> "Link": """Creates a link to a child Catalog or Collection.""" - return Link("child", c, title=title, media_type="application/json") + return Link( + pystac.RelType.CHILD, c, title=title, media_type=pystac.MediaType.JSON + ) @staticmethod def item(item: "Item_Type", title: Optional[str] = None) -> "Link": """Creates a link to an Item.""" - return Link("item", item, title=title, media_type="application/json") + return Link( + pystac.RelType.ITEM, item, title=title, media_type=pystac.MediaType.JSON + ) + + @staticmethod + def canonical( + item_or_collection: Union["Item_Type", "Collection_Type"], + title: Optional[str] = None, + ) -> "Link": + """Creates a canonical link to an Item or Collection.""" + return Link( + pystac.RelType.CANONICAL, + item_or_collection, + title=title, + media_type=pystac.MediaType.JSON, + ) diff --git a/pystac/rel_type.py b/pystac/rel_type.py new file mode 100644 index 000000000..87db25d04 --- /dev/null +++ b/pystac/rel_type.py @@ -0,0 +1,32 @@ +from enum import Enum + + +class RelType(str, Enum): + """A list of common rel types that can be used in STAC Link metadata. + See :stac-spec:`"Using Relation Types ` + in the STAC Best Practices for guidelines on using relation types. You may also want + to refer to the "Relation type" documentation for + :stac-spec:`Catalogs `, + :stac-spec:`Collections `, + or :stac-spec:`Items ` for relation types + specific to those STAC objects. + """ + + def __str__(self) -> str: + return str(self.value) + + ALTERNATE = "alternate" + CANONICAL = "canonical" + CHILD = "child" + COLLECTION = "collection" + ITEM = "item" + ITEMS = "items" + LICENSE = "license" + DERIVED_FROM = "derived_from" + NEXT = "next" + PARENT = "parent" + PREV = "prev" + PREVIEW = "preview" + ROOT = "root" + SELF = "self" + VIA = "via" diff --git a/pystac/serialization/common_properties.py b/pystac/serialization/common_properties.py index d7501c904..0b296d6e1 100644 --- a/pystac/serialization/common_properties.py +++ b/pystac/serialization/common_properties.py @@ -68,7 +68,7 @@ def merge_common_properties( links = cast(List[Dict[str, Any]], item_dict["links"]) collection_link = next( - (link for link in links if link["rel"] == "collection"), None + (link for link in links if link["rel"] == pystac.RelType.COLLECTION), None ) if collection_link is not None: collection_href = cast(Dict[str, Any], collection_link).get("href") diff --git a/pystac/serialization/identify.py b/pystac/serialization/identify.py index 3ba86dad4..93b3e3ad8 100644 --- a/pystac/serialization/identify.py +++ b/pystac/serialization/identify.py @@ -401,7 +401,7 @@ def identify_stac_object(json_dict: Dict[str, Any]) -> STACJSONDescription: # self links became non-required in 0.7.0 if not version_range.is_earlier_than("0.7.0") and not any( filter( - lambda l: cast(Dict[str, Any], l)["rel"] == "self", + lambda l: cast(Dict[str, Any], l)["rel"] == pystac.RelType.SELF, json_dict["links"], ) ): diff --git a/pystac/stac_io.py b/pystac/stac_io.py index adb1b9dd0..d76f1f8aa 100644 --- a/pystac/stac_io.py +++ b/pystac/stac_io.py @@ -25,7 +25,7 @@ try: import orjson # type: ignore except ImportError: - orjson = None + orjson = None # type: ignore[assignment] if TYPE_CHECKING: from pystac.stac_object import STACObject as STACObject_Type @@ -170,6 +170,7 @@ class DefaultStacIO(StacIO): def read_text( self, source: Union[str, "Link_Type"], *args: Any, **kwargs: Any ) -> str: + href: Optional[str] if isinstance(source, str): href = source else: @@ -193,6 +194,7 @@ def read_text_from_href(self, href: str, *args: Any, **kwargs: Any) -> str: def write_text( self, dest: Union[str, "Link_Type"], txt: str, *args: Any, **kwargs: Any ) -> None: + href: Optional[str] if isinstance(dest, str): href = dest else: diff --git a/pystac/stac_object.py b/pystac/stac_object.py index aa14b7453..b1bd66eaf 100644 --- a/pystac/stac_object.py +++ b/pystac/stac_object.py @@ -1,6 +1,6 @@ from abc import ABC, abstractmethod from enum import Enum -from typing import Any, Dict, Iterable, List, Optional, cast, TYPE_CHECKING +from typing import Any, Dict, Iterable, List, Optional, cast, TYPE_CHECKING, Union import pystac from pystac import STACError @@ -26,13 +26,17 @@ class STACObject(ABC): has links e.g. (Catalogs, Collections, or Items). A STACObject has common functionality, can be converted to and from Python ``dicts`` representing JSON, and can be cloned or copied. - - Attributes: - links : A list of :class:`~pystac.Link` objects representing - all links associated with this STACObject. """ id: str + """The ID of the STAC Object.""" + + links: List[Link] + """A list of :class:`~pystac.Link` objects representing all links associated with + this STAC Object.""" + + stac_extensions: List[str] + """A list of schema URIs for STAC Extensions implemented by this STAC Object.""" STAC_OBJECT_TYPE: STACObjectType @@ -73,7 +77,7 @@ def add_links(self, links: List[Link]) -> None: for link in links: self.add_link(link) - def remove_links(self, rel: str) -> None: + def remove_links(self, rel: Union[str, pystac.RelType]) -> None: """Remove links to this object's set of links that match the given ``rel``. Args: @@ -82,7 +86,7 @@ def remove_links(self, rel: str) -> None: self.links = [link for link in self.links if link.rel != rel] - def get_single_link(self, rel: str) -> Optional[Link]: + def get_single_link(self, rel: Union[str, pystac.RelType]) -> Optional[Link]: """Get single link that match the given ``rel``. Args: @@ -91,7 +95,7 @@ def get_single_link(self, rel: str) -> Optional[Link]: return next((link for link in self.links if link.rel == rel), None) - def get_links(self, rel: Optional[str] = None) -> List[Link]: + def get_links(self, rel: Optional[Union[str, pystac.RelType]] = None) -> List[Link]: """Gets the :class:`~pystac.Link` instances associated with this object. Args: @@ -107,7 +111,7 @@ def get_links(self, rel: Optional[str] = None) -> List[Link]: else: return [link for link in self.links if link.rel == rel] - def clear_links(self, rel: Optional[str] = None) -> None: + def clear_links(self, rel: Optional[Union[str, pystac.RelType]] = None) -> None: """Clears all :class:`~pystac.Link` instances associated with this object. Args: @@ -126,7 +130,7 @@ def get_root_link(self) -> Optional[Link]: :class:`~pystac.Link` or None: The root link for this object, or ``None`` if no root link is set. """ - return self.get_single_link("root") + return self.get_single_link(pystac.RelType.ROOT) @property def self_href(self) -> str: @@ -157,7 +161,7 @@ def get_self_href(self) -> Optional[str]: have the HREF the file was read from set as it's self HREF. All self links have absolute (as opposed to relative) HREFs. """ - self_link = self.get_single_link("self") + self_link = self.get_single_link(pystac.RelType.SELF) if self_link: return cast(str, self_link.target) else: @@ -179,7 +183,7 @@ def set_self_href(self, href: Optional[str]) -> None: cast(STACObject, self) ) - self.remove_links("self") + self.remove_links(pystac.RelType.SELF) if href is not None: self.add_link(Link.self_href(href)) @@ -216,7 +220,14 @@ def set_root(self, root: Optional["Catalog_Type"]) -> None: object to set. Passing in None will clear the root. """ root_link_index = next( - iter([i for i, link in enumerate(self.links) if link.rel == "root"]), None + iter( + [ + i + for i, link in enumerate(self.links) + if link.rel == pystac.RelType.ROOT + ] + ), + None, ) # Remove from old root resolution cache @@ -226,7 +237,7 @@ def set_root(self, root: Optional["Catalog_Type"]) -> None: cast(pystac.Catalog, root_link.target)._resolved_objects.remove(self) if root is None: - self.remove_links("root") + self.remove_links(pystac.RelType.ROOT) else: new_root_link = Link.root(root) if root_link_index is not None: @@ -246,7 +257,7 @@ def get_parent(self) -> Optional["Catalog_Type"]: The parent object for this object, or ``None`` if no root link is set. """ - parent_link = self.get_single_link("parent") + parent_link = self.get_single_link(pystac.RelType.PARENT) if parent_link: return cast(pystac.Catalog, parent_link.resolve_stac_object().target) else: @@ -261,11 +272,13 @@ def set_parent(self, parent: Optional["Catalog_Type"]) -> None: object to set. Passing in None will clear the parent. """ - self.remove_links("parent") + self.remove_links(pystac.RelType.PARENT) if parent is not None: self.add_link(Link.parent(parent)) - def get_stac_objects(self, rel: str) -> Iterable["STACObject"]: + def get_stac_objects( + self, rel: Union[str, pystac.RelType] + ) -> Iterable["STACObject"]: """Gets the :class:`~pystac.STACObject` instances that are linked to by links with their ``rel`` property matching the passed in argument. @@ -369,15 +382,20 @@ def full_copy( target = cached_target else: target_parent = None - if link.rel in ["child", "item"] and isinstance( - clone, pystac.Catalog + if ( + link.rel + in [ + pystac.RelType.CHILD, + pystac.RelType.ITEM, + ] + and isinstance(clone, pystac.Catalog) ): target_parent = clone copied_target = target.full_copy(root=root, parent=target_parent) if root is not None: root._resolved_objects.cache(copied_target) target = copied_target - if link.rel in ["child", "item"]: + if link.rel in [pystac.RelType.CHILD, pystac.RelType.ITEM]: target.set_root(root) if isinstance(clone, pystac.Catalog): target.set_parent(clone) @@ -392,7 +410,9 @@ def resolve_links(self) -> None: This method mutates the entire catalog tree. """ - link_rels = set(self._object_links()) | set(["root", "parent"]) + link_rels = set(self._object_links()) | set( + [pystac.RelType.ROOT, pystac.RelType.PARENT] + ) for link in self.links: if link.rel in link_rels: diff --git a/pystac/validation/__init__.py b/pystac/validation/__init__.py index 29dca4048..bd53df84a 100644 --- a/pystac/validation/__init__.py +++ b/pystac/validation/__init__.py @@ -146,7 +146,7 @@ def validate_all( links = cast(List[Dict[str, Any]], stac_dict.get("links")) for link in links: rel = link.get("rel") - if rel in ["item", "child"]: + if rel in [pystac.RelType.ITEM, pystac.RelType.CHILD]: link_href = make_absolute_href( cast(str, link.get("href")), start_href=href ) diff --git a/scripts/lint b/scripts/lint index af5b14940..4fd78388b 100755 --- a/scripts/lint +++ b/scripts/lint @@ -2,6 +2,12 @@ set -e +echo +echo " -- CHECKING TYPES WITH MYPY --" +echo + +mypy docs pystac setup.py tests + echo echo " -- CHECKING TYPES WITH PYRIGHT --" echo diff --git a/setup.py b/setup.py index 6851f848a..7993d8eb1 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from setuptools import setup, find_packages from glob import glob -__version__ = load_source('pystac.version', 'pystac/version.py').__version__ +__version__ = load_source('pystac.version', 'pystac/version.py').__version__ # type: ignore from os.path import ( basename, diff --git a/tests/data-files/eo/eo-landsat-example.json b/tests/data-files/eo/eo-landsat-example.json index 6fc5c3c16..6ecd622d9 100644 --- a/tests/data-files/eo/eo-landsat-example.json +++ b/tests/data-files/eo/eo-landsat-example.json @@ -65,7 +65,8 @@ "name": "B1", "common_name": "coastal", "center_wavelength": 0.44, - "full_width_half_max": 0.02 + "full_width_half_max": 0.02, + "solar_illumination": 2000 } ] }, diff --git a/tests/data-files/raster/gdalinfo.json b/tests/data-files/raster/gdalinfo.json new file mode 100644 index 000000000..09ca67966 --- /dev/null +++ b/tests/data-files/raster/gdalinfo.json @@ -0,0 +1,1321 @@ +{ + "description":"\/home\/emathot\/Downloads\/PT01S00_842547E119_8697242018100100000000MS00_GG001002003\/PT01S00_842547E119_8697242018100100000000MS00_GG001002003.tif", + "driverShortName":"GTiff", + "driverLongName":"GeoTIFF", + "files":[ + "\/home\/emathot\/Downloads\/PT01S00_842547E119_8697242018100100000000MS00_GG001002003\/PT01S00_842547E119_8697242018100100000000MS00_GG001002003.tif", + "\/home\/emathot\/Downloads\/PT01S00_842547E119_8697242018100100000000MS00_GG001002003\/PT01S00_842547E119_8697242018100100000000MS00_GG001002003.tif.aux.xml" + ], + "size":[ + 8966, + 4411 + ], + "coordinateSystem":{ + "wkt":"PROJCRS[\"WGS 84 \/ UTM zone 50S\",\n BASEGEOGCRS[\"WGS 84\",\n DATUM[\"World Geodetic System 1984\",\n ELLIPSOID[\"WGS 84\",6378137,298.257223563,\n LENGTHUNIT[\"metre\",1]]],\n PRIMEM[\"Greenwich\",0,\n ANGLEUNIT[\"degree\",0.0174532925199433]],\n ID[\"EPSG\",4326]],\n CONVERSION[\"UTM zone 50S\",\n METHOD[\"Transverse Mercator\",\n ID[\"EPSG\",9807]],\n PARAMETER[\"Latitude of natural origin\",0,\n ANGLEUNIT[\"degree\",0.0174532925199433],\n ID[\"EPSG\",8801]],\n PARAMETER[\"Longitude of natural origin\",117,\n ANGLEUNIT[\"degree\",0.0174532925199433],\n ID[\"EPSG\",8802]],\n PARAMETER[\"Scale factor at natural origin\",0.9996,\n SCALEUNIT[\"unity\",1],\n ID[\"EPSG\",8805]],\n PARAMETER[\"False easting\",500000,\n LENGTHUNIT[\"metre\",1],\n ID[\"EPSG\",8806]],\n PARAMETER[\"False northing\",10000000,\n LENGTHUNIT[\"metre\",1],\n ID[\"EPSG\",8807]]],\n CS[Cartesian,2],\n AXIS[\"(E)\",east,\n ORDER[1],\n LENGTHUNIT[\"metre\",1]],\n AXIS[\"(N)\",north,\n ORDER[2],\n LENGTHUNIT[\"metre\",1]],\n USAGE[\n SCOPE[\"unknown\"],\n AREA[\"World - S hemisphere - 114°E to 120°E - by country\"],\n BBOX[-80,114,0,120]],\n ID[\"EPSG\",32750]]", + "dataAxisToSRSAxisMapping":[ + 1, + 2 + ] + }, + "geoTransform":[ + 805980.0, + 3.0, + 0.0, + 9913371.0, + 0.0, + -3.0 + ], + "metadata":{ + "":{ + "AREA_OR_POINT":"Area", + "TIFFTAG_DATETIME":"2018:10:01 01:54:33" + }, + "IMAGE_STRUCTURE":{ + "COMPRESSION":"LZW", + "INTERLEAVE":"PIXEL" + } + }, + "cornerCoordinates":{ + "upperLeft":[ + 805980.0, + 9913371.0 + ], + "lowerLeft":[ + 805980.0, + 9900138.0 + ], + "lowerRight":[ + 832878.0, + 9900138.0 + ], + "upperRight":[ + 832878.0, + 9913371.0 + ], + "center":[ + 819429.0, + 9906754.5 + ] + }, + "wgs84Extent":{ + "type":"Polygon", + "coordinates":[ + [ + [ + 119.748958, + -0.7828514 + ], + [ + 119.7490419, + -0.9024355 + ], + [ + 119.9904918, + -0.9022436 + ], + [ + 119.9904006, + -0.7826849 + ], + [ + 119.748958, + -0.7828514 + ] + ] + ] + }, + "bands":[ + { + "band":1, + "block":[ + 256, + 256 + ], + "type":"UInt16", + "colorInterpretation":"Red", + "min":1962.0, + "max":32925.0, + "minimum":1962.0, + "maximum":32925.0, + "mean":8498.94, + "stdDev":5056.129, + "histogram":{ + "count":256, + "min":1901.2882352941181, + "max":32985.711764705877, + "buckets":[ + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 3, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 4, + 0, + 7, + 571, + 6105, + 21712, + 63711, + 185135, + 459039, + 786635, + 922499, + 806764, + 787321, + 1241184, + 1738447, + 1795927, + 1741773, + 1649908, + 1409164, + 1127530, + 867848, + 678713, + 543633, + 447979, + 398363, + 362465, + 344935, + 322773, + 267707, + 211558, + 178444, + 157196, + 142929, + 134913, + 125780, + 118716, + 113601, + 106456, + 101488, + 95104, + 90665, + 86981, + 82680, + 79008, + 74044, + 70913, + 66420, + 62732, + 60000, + 56261, + 53861, + 50155, + 47653, + 44656, + 42616, + 40440, + 38163, + 36762, + 34432, + 33226, + 31156, + 29851, + 28565, + 27478, + 26603, + 24854, + 24262, + 23365, + 22590, + 22039, + 20959, + 20580, + 20178, + 19567, + 18948, + 18388, + 17929, + 17553, + 16946, + 16609, + 16275, + 16075, + 15664, + 15646, + 15319, + 15269, + 15028, + 14700, + 14779, + 14518, + 14355, + 14368, + 14463, + 14211, + 13989, + 13919, + 13827, + 14215, + 13868, + 13701, + 13836, + 13724, + 13447, + 13360, + 13382, + 13489, + 12932, + 13023, + 12867, + 13156, + 12880, + 12926, + 12985, + 12906, + 13142, + 13109, + 12956, + 12751, + 12886, + 12742, + 12643, + 12658, + 12823, + 12733, + 12325, + 12646, + 12746, + 12675, + 12538, + 12807, + 12927, + 12655, + 12909, + 13069, + 12847, + 12441, + 12266, + 12204, + 11908, + 11990, + 12125, + 11924, + 12051, + 12263, + 11944, + 11567, + 11567, + 11458, + 11316, + 11221, + 11333, + 11522, + 11352, + 11402, + 11495, + 11440, + 11338, + 11211, + 11416, + 11509, + 11407, + 11404, + 11459, + 11629, + 11575, + 11667, + 12038, + 11635, + 11707, + 11425, + 11513, + 11283, + 11364, + 11619, + 11508, + 11744, + 11762, + 11902, + 12025, + 11935, + 11896, + 12081, + 12215, + 12463, + 12712, + 13391, + 13871, + 14168, + 15013, + 16254, + 16395, + 17029, + 20130, + 24377, + 24925, + 27321, + 31060, + 28692, + 28276, + 29373, + 32245, + 35879, + 37705, + 41327, + 39338, + 35440, + 33572, + 29527, + 21107, + 18133, + 20080, + 22042, + 17810, + 16249, + 16463, + 11636, + 5833, + 2143, + 1201, + 1150, + 1162, + 1078, + 739, + 453, + 358, + 429, + 462, + 302, + 52, + 3, + 0, + 0, + 0, + 1 + ] + }, + "noDataValue":0.0, + "overviews":[ + { + "size":[ + 2989, + 1471 + ] + }, + { + "size":[ + 997, + 491 + ] + }, + { + "size":[ + 333, + 164 + ] + } + ], + "metadata":{ + "":{ + "STATISTICS_MAXIMUM":"32925", + "STATISTICS_MEAN":"8498.9400644319", + "STATISTICS_MINIMUM":"1962", + "STATISTICS_STDDEV":"5056.1292002722", + "STATISTICS_VALID_PERCENT":"61.09" + } + } + }, + { + "band":2, + "block":[ + 256, + 256 + ], + "type":"UInt16", + "colorInterpretation":"Green", + "min":3884.0, + "max":22063.0, + "minimum":3884.0, + "maximum":22063.0, + "mean":7185.212, + "stdDev":3799.456, + "histogram":{ + "count":256, + "min":3848.354901960784, + "max":22098.645098039211, + "buckets":[ + 10, + 183, + 1521, + 4252, + 5172, + 5787, + 8311, + 14536, + 29935, + 68057, + 118064, + 169294, + 244133, + 419815, + 683957, + 956129, + 1104763, + 1038066, + 957227, + 873969, + 824050, + 816860, + 811809, + 775924, + 748772, + 729164, + 714577, + 669613, + 630867, + 584633, + 520962, + 465590, + 412711, + 375556, + 332967, + 304301, + 284258, + 256709, + 236190, + 218986, + 207167, + 192887, + 184126, + 177185, + 165694, + 158650, + 154762, + 152932, + 149835, + 147305, + 139429, + 120363, + 108595, + 103083, + 94420, + 88159, + 83210, + 80140, + 75519, + 71998, + 69961, + 66472, + 64710, + 62611, + 61054, + 58365, + 56561, + 55527, + 52592, + 51285, + 49701, + 48674, + 46559, + 45127, + 44535, + 42274, + 41186, + 40443, + 39314, + 37366, + 35745, + 35182, + 32961, + 32646, + 31871, + 30238, + 29242, + 28131, + 27875, + 26668, + 25941, + 25336, + 24298, + 23633, + 23245, + 22804, + 21828, + 21533, + 21202, + 20256, + 19767, + 18775, + 18645, + 17801, + 17478, + 17259, + 16563, + 16427, + 15757, + 15638, + 15122, + 14849, + 14677, + 14179, + 14047, + 14095, + 13588, + 13292, + 12938, + 13147, + 12352, + 12224, + 12065, + 11724, + 11501, + 11277, + 11366, + 11214, + 10859, + 10983, + 10590, + 10505, + 10621, + 10434, + 10292, + 10123, + 10081, + 9846, + 9753, + 9646, + 9664, + 9352, + 9432, + 9319, + 9184, + 9002, + 9446, + 9037, + 9032, + 8906, + 8760, + 9002, + 8873, + 8758, + 8742, + 8843, + 8567, + 8843, + 8507, + 8493, + 8673, + 8469, + 8436, + 8404, + 8537, + 8392, + 8471, + 8557, + 8167, + 8378, + 8324, + 8320, + 8219, + 8187, + 8476, + 8333, + 8228, + 8169, + 8146, + 8184, + 8318, + 8457, + 8484, + 8535, + 8606, + 8396, + 8565, + 8599, + 8695, + 8699, + 8633, + 8868, + 8718, + 9009, + 9159, + 9286, + 9203, + 9407, + 9850, + 9769, + 10238, + 10634, + 11191, + 11855, + 12990, + 14936, + 16739, + 19574, + 22429, + 23245, + 25463, + 30710, + 33379, + 34497, + 34980, + 43269, + 50837, + 48583, + 49231, + 54850, + 51336, + 50119, + 57398, + 59037, + 55055, + 47147, + 42814, + 41439, + 40178, + 42036, + 41922, + 42330, + 41835, + 40322, + 36937, + 34926, + 34203, + 32335, + 23094, + 11803, + 4957, + 4041, + 5301, + 7638, + 9410, + 10022, + 8949, + 4806, + 3622, + 3470, + 3726, + 3532, + 2229, + 838, + 117, + 14 + ] + }, + "noDataValue":0.0, + "overviews":[ + { + "size":[ + 2989, + 1471 + ] + }, + { + "size":[ + 997, + 491 + ] + }, + { + "size":[ + 333, + 164 + ] + } + ], + "metadata":{ + "":{ + "STATISTICS_MAXIMUM":"22063", + "STATISTICS_MEAN":"7185.2123645206", + "STATISTICS_MINIMUM":"3884", + "STATISTICS_STDDEV":"3799.4562788636", + "STATISTICS_VALID_PERCENT":"61.09" + } + } + }, + { + "band":3, + "block":[ + 256, + 256 + ], + "type":"UInt16", + "colorInterpretation":"Blue", + "min":1061.0, + "max":29693.0, + "minimum":1061.0, + "maximum":29693.0, + "mean":5829.584, + "stdDev":4683.065, + "histogram":{ + "count":256, + "min":1004.858823529412, + "max":29749.141176470592, + "buckets":[ + 1, + 0, + 0, + 0, + 2, + 0, + 0, + 0, + 0, + 0, + 0, + 2, + 0, + 108, + 3217, + 11335, + 152731, + 1127018, + 1211097, + 1236911, + 1363134, + 1443526, + 1267870, + 969314, + 756790, + 638508, + 595552, + 624031, + 664768, + 668971, + 652703, + 629435, + 599060, + 556142, + 515645, + 474414, + 425902, + 385427, + 350875, + 322580, + 293642, + 271835, + 245400, + 225779, + 205788, + 188632, + 177908, + 166865, + 157731, + 149549, + 143095, + 133944, + 125462, + 118610, + 109669, + 102918, + 97857, + 93669, + 87646, + 82795, + 78993, + 74535, + 69425, + 66393, + 62828, + 58941, + 55705, + 53740, + 50474, + 47663, + 45871, + 43824, + 41617, + 39644, + 38407, + 35961, + 34389, + 32789, + 31598, + 30413, + 29273, + 28129, + 26419, + 25637, + 24468, + 23908, + 22710, + 22353, + 21624, + 21268, + 20801, + 20407, + 19964, + 19449, + 18890, + 18419, + 18360, + 17579, + 17211, + 16908, + 16397, + 16202, + 16040, + 15989, + 15611, + 15667, + 15663, + 15329, + 14886, + 15027, + 14868, + 14660, + 14252, + 14677, + 14618, + 14348, + 14152, + 14165, + 14061, + 14263, + 14326, + 14124, + 14283, + 13994, + 14085, + 13914, + 14258, + 14296, + 14207, + 13903, + 13732, + 13614, + 13689, + 13784, + 14008, + 13880, + 13671, + 13670, + 13546, + 13331, + 13367, + 13207, + 13100, + 12902, + 12987, + 13191, + 12989, + 12868, + 12856, + 12698, + 12663, + 12223, + 12606, + 12215, + 12275, + 12321, + 12378, + 12215, + 12283, + 12279, + 12246, + 12133, + 12271, + 12217, + 12038, + 12102, + 12376, + 12185, + 12097, + 11969, + 12227, + 12111, + 12052, + 12175, + 12332, + 12092, + 11919, + 12028, + 12063, + 12040, + 11573, + 11730, + 11556, + 11502, + 11806, + 11540, + 11597, + 11708, + 11725, + 11862, + 11750, + 11811, + 11677, + 11195, + 11371, + 11178, + 10970, + 10807, + 10953, + 10683, + 10578, + 10490, + 10589, + 10575, + 10581, + 10432, + 10470, + 10490, + 10625, + 10780, + 11176, + 12016, + 11561, + 12565, + 14701, + 16242, + 18880, + 19552, + 23131, + 20822, + 19998, + 22521, + 24620, + 24693, + 29698, + 35048, + 31291, + 24583, + 24065, + 16275, + 10165, + 12458, + 14547, + 11381, + 10573, + 11441, + 8333, + 4570, + 1297, + 165, + 126, + 118, + 92, + 125, + 134, + 144, + 147, + 41, + 8, + 1, + 1, + 0, + 0, + 0, + 0, + 1 + ] + }, + "noDataValue":0.0, + "overviews":[ + { + "size":[ + 2989, + 1471 + ] + }, + { + "size":[ + 997, + 491 + ] + }, + { + "size":[ + 333, + 164 + ] + } + ], + "metadata":{ + "":{ + "STATISTICS_MAXIMUM":"29693", + "STATISTICS_MEAN":"5829.583942362", + "STATISTICS_MINIMUM":"1061", + "STATISTICS_STDDEV":"4683.0650025253", + "STATISTICS_VALID_PERCENT":"61.09" + } + } + }, + { + "band":4, + "block":[ + 256, + 256 + ], + "type":"UInt16", + "colorInterpretation":"Undefined", + "min":1685.0, + "max":31836.0, + "minimum":1685.0, + "maximum":31836.0, + "mean":6785.251, + "stdDev":3842.432, + "histogram":{ + "count":256, + "min":1625.8803921568631, + "max":31895.119607843139, + "buckets":[ + 22, + 14733, + 678948, + 2007074, + 1538451, + 979742, + 623770, + 349430, + 168251, + 60346, + 26401, + 22191, + 22006, + 25520, + 34458, + 40830, + 44848, + 54856, + 63762, + 68301, + 71386, + 69643, + 63481, + 60694, + 62528, + 65271, + 67237, + 72587, + 74443, + 78565, + 86651, + 99318, + 114376, + 137341, + 165447, + 207016, + 256104, + 313009, + 381862, + 442953, + 500981, + 538729, + 564808, + 561998, + 546706, + 525309, + 505496, + 478111, + 454176, + 427516, + 409082, + 384970, + 361573, + 343085, + 327098, + 308187, + 294358, + 281729, + 270641, + 264240, + 254691, + 248139, + 243469, + 240776, + 236947, + 233962, + 230176, + 228832, + 222362, + 217914, + 212073, + 204692, + 194483, + 182967, + 171798, + 159921, + 144872, + 130796, + 117153, + 105453, + 91660, + 79551, + 69512, + 60404, + 53959, + 46849, + 41999, + 37832, + 35346, + 32861, + 31211, + 30317, + 29309, + 28340, + 27524, + 26708, + 26785, + 26899, + 26976, + 26722, + 27112, + 26526, + 26537, + 25625, + 25701, + 25582, + 24712, + 24723, + 23504, + 23343, + 23167, + 23346, + 23141, + 23882, + 23343, + 23371, + 23339, + 23783, + 24275, + 24128, + 23942, + 23791, + 23248, + 22798, + 22769, + 22851, + 22262, + 22017, + 21166, + 20420, + 20092, + 19923, + 19977, + 19426, + 19468, + 18436, + 18053, + 17701, + 17243, + 17034, + 16722, + 16019, + 15159, + 14860, + 14550, + 14326, + 13999, + 13487, + 12878, + 12361, + 12020, + 11471, + 10678, + 10643, + 10284, + 10255, + 9709, + 9221, + 8683, + 8327, + 7719, + 7463, + 7179, + 7055, + 6785, + 6432, + 6123, + 5863, + 5349, + 5113, + 4859, + 4412, + 4376, + 4173, + 3996, + 3814, + 3517, + 3398, + 3392, + 3082, + 2847, + 2585, + 2422, + 2180, + 2184, + 1896, + 1790, + 1717, + 1687, + 1662, + 1553, + 1429, + 1126, + 1158, + 1076, + 1085, + 891, + 852, + 667, + 672, + 628, + 563, + 455, + 447, + 334, + 356, + 301, + 272, + 206, + 157, + 117, + 90, + 90, + 103, + 76, + 55, + 33, + 23, + 17, + 7, + 3, + 2, + 3, + 1, + 3, + 0, + 1, + 1, + 1, + 0, + 2, + 1, + 0, + 2, + 1, + 2, + 0, + 3, + 1, + 2, + 2, + 1, + 3, + 2, + 1, + 0, + 0, + 2, + 0, + 0, + 1, + 0, + 0, + 1, + 0, + 2 + ] + }, + "noDataValue":0.0, + "overviews":[ + { + "size":[ + 2989, + 1471 + ] + }, + { + "size":[ + 997, + 491 + ] + }, + { + "size":[ + 333, + 164 + ] + } + ], + "metadata":{ + "":{ + "STATISTICS_MAXIMUM":"31836", + "STATISTICS_MEAN":"6785.2511255265", + "STATISTICS_MINIMUM":"1685", + "STATISTICS_STDDEV":"3842.4324936707", + "STATISTICS_VALID_PERCENT":"61.09" + } + } + } + ] +} \ No newline at end of file diff --git a/tests/data-files/raster/raster-planet-example.json b/tests/data-files/raster/raster-planet-example.json new file mode 100644 index 000000000..c76d807d5 --- /dev/null +++ b/tests/data-files/raster/raster-planet-example.json @@ -0,0 +1,1250 @@ +{ + "stac_extensions": [ + "https://stac-extensions.github.io/sat/v1.0.0/schema.json", + "https://stac-extensions.github.io/view/v1.0.0/schema.json", + "https://stac-extensions.github.io/projection/v1.0.0/schema.json", + "https://stac-extensions.github.io/eo/v1.0.0/schema.json", + "https://stac-extensions.github.io/processing/v1.0.0/schema.json", + "https://stac-extensions.github.io/raster/v1.0.0/schema.json" + ], + "stac_version": "1.0.0-rc.4", + "links": [], + "assets": { + "data": { + "type": "image/tiff; application=geotiff;", + "roles": [ + "data" + ], + "href": "PT01S00_842547E119_8697242018100100000000MS00_GG001002003/PT01S00_842547E119_8697242018100100000000MS00_GG001002003.tif", + "eo:bands": [ + { + "name": "band-1", + "common_name": "red", + "center_wavelength": 0.63 + }, + { + "name": "band-2", + "common_name": "green", + "center_wavelength": 0.545 + }, + { + "name": "band-3", + "common_name": "blue", + "center_wavelength": 0.485 + }, + { + "name": "band-4", + "common_name": "nir", + "center_wavelength": 0.82 + } + ], + "proj:shape": [ + 8966, + 4411 + ], + "raster:bands": [ + { + "sampling": "area", + "name": "Red TOA reflectance", + "unit": "W⋅sr−1⋅m−2⋅nm−1", + "data_type": "uint16", + "scale": 0.01, + "offset": 0, + "nodata": 0, + "spatial_resolution": 3, + "statistics": { + "maximum": 32925, + "mean": 8498.9400644319, + "minimum": 1962, + "stddev": 5056.1292002722, + "valid_percent": 61.09 + }, + "histogram": { + "count": 256, + "min": 1901.288235294118, + "max": 32985.71176470588, + "buckets": [ + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 3, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 4, + 0, + 7, + 571, + 6105, + 21712, + 63711, + 185135, + 459039, + 786635, + 922499, + 806764, + 787321, + 1241184, + 1738447, + 1795927, + 1741773, + 1649908, + 1409164, + 1127530, + 867848, + 678713, + 543633, + 447979, + 398363, + 362465, + 344935, + 322773, + 267707, + 211558, + 178444, + 157196, + 142929, + 134913, + 125780, + 118716, + 113601, + 106456, + 101488, + 95104, + 90665, + 86981, + 82680, + 79008, + 74044, + 70913, + 66420, + 62732, + 60000, + 56261, + 53861, + 50155, + 47653, + 44656, + 42616, + 40440, + 38163, + 36762, + 34432, + 33226, + 31156, + 29851, + 28565, + 27478, + 26603, + 24854, + 24262, + 23365, + 22590, + 22039, + 20959, + 20580, + 20178, + 19567, + 18948, + 18388, + 17929, + 17553, + 16946, + 16609, + 16275, + 16075, + 15664, + 15646, + 15319, + 15269, + 15028, + 14700, + 14779, + 14518, + 14355, + 14368, + 14463, + 14211, + 13989, + 13919, + 13827, + 14215, + 13868, + 13701, + 13836, + 13724, + 13447, + 13360, + 13382, + 13489, + 12932, + 13023, + 12867, + 13156, + 12880, + 12926, + 12985, + 12906, + 13142, + 13109, + 12956, + 12751, + 12886, + 12742, + 12643, + 12658, + 12823, + 12733, + 12325, + 12646, + 12746, + 12675, + 12538, + 12807, + 12927, + 12655, + 12909, + 13069, + 12847, + 12441, + 12266, + 12204, + 11908, + 11990, + 12125, + 11924, + 12051, + 12263, + 11944, + 11567, + 11567, + 11458, + 11316, + 11221, + 11333, + 11522, + 11352, + 11402, + 11495, + 11440, + 11338, + 11211, + 11416, + 11509, + 11407, + 11404, + 11459, + 11629, + 11575, + 11667, + 12038, + 11635, + 11707, + 11425, + 11513, + 11283, + 11364, + 11619, + 11508, + 11744, + 11762, + 11902, + 12025, + 11935, + 11896, + 12081, + 12215, + 12463, + 12712, + 13391, + 13871, + 14168, + 15013, + 16254, + 16395, + 17029, + 20130, + 24377, + 24925, + 27321, + 31060, + 28692, + 28276, + 29373, + 32245, + 35879, + 37705, + 41327, + 39338, + 35440, + 33572, + 29527, + 21107, + 18133, + 20080, + 22042, + 17810, + 16249, + 16463, + 11636, + 5833, + 2143, + 1201, + 1150, + 1162, + 1078, + 739, + 453, + 358, + 429, + 462, + 302, + 52, + 3, + 0, + 0, + 0, + 1 + ] + } + }, + { + "sampling": "area", + "name": "Green TOA reflectance", + "unit": "W⋅sr−1⋅m−2⋅nm−1", + "data_type": "uint16", + "scale": 0.0000196236732904, + "offset": 0, + "nodata": 0, + "spatial_resolution": 3, + "statistics": { + "maximum": 22063, + "mean": 7185.2123645206, + "minimum": 3884, + "stddev": 3799.4562788636, + "valid_percent": 61.09 + }, + "histogram": { + "count": 256, + "min": 3848.354901960784, + "max": 22098.64509803921, + "buckets": [ + 10, + 183, + 1521, + 4252, + 5172, + 5787, + 8311, + 14536, + 29935, + 68057, + 118064, + 169294, + 244133, + 419815, + 683957, + 956129, + 1104763, + 1038066, + 957227, + 873969, + 824050, + 816860, + 811809, + 775924, + 748772, + 729164, + 714577, + 669613, + 630867, + 584633, + 520962, + 465590, + 412711, + 375556, + 332967, + 304301, + 284258, + 256709, + 236190, + 218986, + 207167, + 192887, + 184126, + 177185, + 165694, + 158650, + 154762, + 152932, + 149835, + 147305, + 139429, + 120363, + 108595, + 103083, + 94420, + 88159, + 83210, + 80140, + 75519, + 71998, + 69961, + 66472, + 64710, + 62611, + 61054, + 58365, + 56561, + 55527, + 52592, + 51285, + 49701, + 48674, + 46559, + 45127, + 44535, + 42274, + 41186, + 40443, + 39314, + 37366, + 35745, + 35182, + 32961, + 32646, + 31871, + 30238, + 29242, + 28131, + 27875, + 26668, + 25941, + 25336, + 24298, + 23633, + 23245, + 22804, + 21828, + 21533, + 21202, + 20256, + 19767, + 18775, + 18645, + 17801, + 17478, + 17259, + 16563, + 16427, + 15757, + 15638, + 15122, + 14849, + 14677, + 14179, + 14047, + 14095, + 13588, + 13292, + 12938, + 13147, + 12352, + 12224, + 12065, + 11724, + 11501, + 11277, + 11366, + 11214, + 10859, + 10983, + 10590, + 10505, + 10621, + 10434, + 10292, + 10123, + 10081, + 9846, + 9753, + 9646, + 9664, + 9352, + 9432, + 9319, + 9184, + 9002, + 9446, + 9037, + 9032, + 8906, + 8760, + 9002, + 8873, + 8758, + 8742, + 8843, + 8567, + 8843, + 8507, + 8493, + 8673, + 8469, + 8436, + 8404, + 8537, + 8392, + 8471, + 8557, + 8167, + 8378, + 8324, + 8320, + 8219, + 8187, + 8476, + 8333, + 8228, + 8169, + 8146, + 8184, + 8318, + 8457, + 8484, + 8535, + 8606, + 8396, + 8565, + 8599, + 8695, + 8699, + 8633, + 8868, + 8718, + 9009, + 9159, + 9286, + 9203, + 9407, + 9850, + 9769, + 10238, + 10634, + 11191, + 11855, + 12990, + 14936, + 16739, + 19574, + 22429, + 23245, + 25463, + 30710, + 33379, + 34497, + 34980, + 43269, + 50837, + 48583, + 49231, + 54850, + 51336, + 50119, + 57398, + 59037, + 55055, + 47147, + 42814, + 41439, + 40178, + 42036, + 41922, + 42330, + 41835, + 40322, + 36937, + 34926, + 34203, + 32335, + 23094, + 11803, + 4957, + 4041, + 5301, + 7638, + 9410, + 10022, + 8949, + 4806, + 3622, + 3470, + 3726, + 3532, + 2229, + 838, + 117, + 14 + ] + } + }, + { + "sampling": "area", + "name": "Blue TOA reflectance", + "unit": "W⋅sr−1⋅m−2⋅nm−1", + "data_type": "uint16", + "scale": 0.0000218499248607, + "offset": 0, + "nodata": 0, + "spatial_resolution": 3, + "statistics": { + "maximum": 29693, + "mean": 5829.583942362, + "minimum": 1061, + "stddev": 4683.0650025253, + "valid_percent": 61.09 + }, + "histogram": { + "count": 256, + "min": 1004.858823529412, + "max": 29749.14117647059, + "buckets": [ + 1, + 0, + 0, + 0, + 2, + 0, + 0, + 0, + 0, + 0, + 0, + 2, + 0, + 108, + 3217, + 11335, + 152731, + 1127018, + 1211097, + 1236911, + 1363134, + 1443526, + 1267870, + 969314, + 756790, + 638508, + 595552, + 624031, + 664768, + 668971, + 652703, + 629435, + 599060, + 556142, + 515645, + 474414, + 425902, + 385427, + 350875, + 322580, + 293642, + 271835, + 245400, + 225779, + 205788, + 188632, + 177908, + 166865, + 157731, + 149549, + 143095, + 133944, + 125462, + 118610, + 109669, + 102918, + 97857, + 93669, + 87646, + 82795, + 78993, + 74535, + 69425, + 66393, + 62828, + 58941, + 55705, + 53740, + 50474, + 47663, + 45871, + 43824, + 41617, + 39644, + 38407, + 35961, + 34389, + 32789, + 31598, + 30413, + 29273, + 28129, + 26419, + 25637, + 24468, + 23908, + 22710, + 22353, + 21624, + 21268, + 20801, + 20407, + 19964, + 19449, + 18890, + 18419, + 18360, + 17579, + 17211, + 16908, + 16397, + 16202, + 16040, + 15989, + 15611, + 15667, + 15663, + 15329, + 14886, + 15027, + 14868, + 14660, + 14252, + 14677, + 14618, + 14348, + 14152, + 14165, + 14061, + 14263, + 14326, + 14124, + 14283, + 13994, + 14085, + 13914, + 14258, + 14296, + 14207, + 13903, + 13732, + 13614, + 13689, + 13784, + 14008, + 13880, + 13671, + 13670, + 13546, + 13331, + 13367, + 13207, + 13100, + 12902, + 12987, + 13191, + 12989, + 12868, + 12856, + 12698, + 12663, + 12223, + 12606, + 12215, + 12275, + 12321, + 12378, + 12215, + 12283, + 12279, + 12246, + 12133, + 12271, + 12217, + 12038, + 12102, + 12376, + 12185, + 12097, + 11969, + 12227, + 12111, + 12052, + 12175, + 12332, + 12092, + 11919, + 12028, + 12063, + 12040, + 11573, + 11730, + 11556, + 11502, + 11806, + 11540, + 11597, + 11708, + 11725, + 11862, + 11750, + 11811, + 11677, + 11195, + 11371, + 11178, + 10970, + 10807, + 10953, + 10683, + 10578, + 10490, + 10589, + 10575, + 10581, + 10432, + 10470, + 10490, + 10625, + 10780, + 11176, + 12016, + 11561, + 12565, + 14701, + 16242, + 18880, + 19552, + 23131, + 20822, + 19998, + 22521, + 24620, + 24693, + 29698, + 35048, + 31291, + 24583, + 24065, + 16275, + 10165, + 12458, + 14547, + 11381, + 10573, + 11441, + 8333, + 4570, + 1297, + 165, + 126, + 118, + 92, + 125, + 134, + 144, + 147, + 41, + 8, + 1, + 1, + 0, + 0, + 0, + 0, + 1 + ] + } + }, + { + "sampling": "area", + "name": "NIR TOA reflectance", + "unit": "W⋅sr−1⋅m−2⋅nm−1", + "data_type": "uint16", + "scale": 0.0000218499248607, + "offset": 0, + "nodata": 0, + "spatial_resolution": 3, + "statistics": { + "maximum": 31836, + "mean": 6785.2511255265, + "minimum": 1685, + "stddev": 3842.4324936707, + "valid_percent": 61.09 + }, + "histogram": { + "count": 256, + "min": 1625.880392156863, + "max": 31895.11960784314, + "buckets": [ + 22, + 14733, + 678948, + 2007074, + 1538451, + 979742, + 623770, + 349430, + 168251, + 60346, + 26401, + 22191, + 22006, + 25520, + 34458, + 40830, + 44848, + 54856, + 63762, + 68301, + 71386, + 69643, + 63481, + 60694, + 62528, + 65271, + 67237, + 72587, + 74443, + 78565, + 86651, + 99318, + 114376, + 137341, + 165447, + 207016, + 256104, + 313009, + 381862, + 442953, + 500981, + 538729, + 564808, + 561998, + 546706, + 525309, + 505496, + 478111, + 454176, + 427516, + 409082, + 384970, + 361573, + 343085, + 327098, + 308187, + 294358, + 281729, + 270641, + 264240, + 254691, + 248139, + 243469, + 240776, + 236947, + 233962, + 230176, + 228832, + 222362, + 217914, + 212073, + 204692, + 194483, + 182967, + 171798, + 159921, + 144872, + 130796, + 117153, + 105453, + 91660, + 79551, + 69512, + 60404, + 53959, + 46849, + 41999, + 37832, + 35346, + 32861, + 31211, + 30317, + 29309, + 28340, + 27524, + 26708, + 26785, + 26899, + 26976, + 26722, + 27112, + 26526, + 26537, + 25625, + 25701, + 25582, + 24712, + 24723, + 23504, + 23343, + 23167, + 23346, + 23141, + 23882, + 23343, + 23371, + 23339, + 23783, + 24275, + 24128, + 23942, + 23791, + 23248, + 22798, + 22769, + 22851, + 22262, + 22017, + 21166, + 20420, + 20092, + 19923, + 19977, + 19426, + 19468, + 18436, + 18053, + 17701, + 17243, + 17034, + 16722, + 16019, + 15159, + 14860, + 14550, + 14326, + 13999, + 13487, + 12878, + 12361, + 12020, + 11471, + 10678, + 10643, + 10284, + 10255, + 9709, + 9221, + 8683, + 8327, + 7719, + 7463, + 7179, + 7055, + 6785, + 6432, + 6123, + 5863, + 5349, + 5113, + 4859, + 4412, + 4376, + 4173, + 3996, + 3814, + 3517, + 3398, + 3392, + 3082, + 2847, + 2585, + 2422, + 2180, + 2184, + 1896, + 1790, + 1717, + 1687, + 1662, + 1553, + 1429, + 1126, + 1158, + 1076, + 1085, + 891, + 852, + 667, + 672, + 628, + 563, + 455, + 447, + 334, + 356, + 301, + 272, + 206, + 157, + 117, + 90, + 90, + 103, + 76, + 55, + 33, + 23, + 17, + 7, + 3, + 2, + 3, + 1, + 3, + 0, + 1, + 1, + 1, + 0, + 2, + 1, + 0, + 2, + 1, + 2, + 0, + 3, + 1, + 2, + 2, + 1, + 3, + 2, + 1, + 0, + 0, + 2, + 0, + 0, + 1, + 0, + 0, + 1, + 0, + 2 + ] + } + } + ], + "size": 180076568 + }, + "metadata": { + "type": "application/xml", + "roles": [ + "metadata" + ], + "href": "PT01S00_842547E119_8697242018100100000000MS00_GG001002003/vendor_metadata/20181001_015433_1027_3B_AnalyticMS_metadata.xml", + "size": 10420 + }, + "overview": { + "type": "image/jpeg", + "roles": [ + "overview" + ], + "title": "Scene Overview", + "href": "PT01S00_842547E119_8697242018100100000000MS00_GG001002003/PT01S00_842547E119_8697242018100100000000MS00_GG001002003-br.jpg", + "size": 846975 + }, + "thumbnail": { + "type": "image/jpeg", + "roles": [ + "thumbnail" + ], + "title": "Scene Thumbnail", + "href": "PT01S00_842547E119_8697242018100100000000MS00_GG001002003/PT01S00_842547E119_8697242018100100000000MS00_GG001002003-th.jpg", + "size": 38126 + } + }, + "type": "Feature", + "id": "PT01S00_842547E119_8697242018100100000000MS00_GG001002003", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 119.749003733, + -0.782842043003 + ], + [ + 119.990422256, + -0.782842043003 + ], + [ + 119.990422256, + -0.902255498348 + ], + [ + 119.749003733, + -0.902255498348 + ], + [ + 119.749003733, + -0.782842043003 + ] + ] + ] + }, + "properties": { + "datetime": "2018-10-01T01:54:33+00:00", + "mission": "planetscope", + "platform": "planetscope", + "instruments": [ + "ps2" + ], + "gsd": 3, + "sat:orbit_state": "descending", + "sat:absolute_orbit": 1736074, + "view:sun_elevation": 60.99449, + "view:sun_azimuth": 94.82318, + "view:incidence_angle": 0.1531593, + "view:off_nadir": 0.1102388, + "proj:epsg": 32750, + "processing:level": "L3B", + "title": "Planetscope PS2 L3B 01/10/2018 01:54:33", + "disaster:call_id": 857 + }, + "bbox": [ + 119.749003733, + -0.902255498348, + 119.990422256, + -0.782842043003 + ] +} \ No newline at end of file diff --git a/tests/data-files/raster/raster-sentinel2-example.json b/tests/data-files/raster/raster-sentinel2-example.json new file mode 100644 index 000000000..600ba7c8c --- /dev/null +++ b/tests/data-files/raster/raster-sentinel2-example.json @@ -0,0 +1,730 @@ +{ + "type": "Feature", + "stac_version": "1.0.0-rc.4", + "stac_extensions": [ + "https://stac-extensions.github.io/eo/v1.0.0/schema.json", + "https://stac-extensions.github.io/view/v1.0.0/schema.json", + "https://stac-extensions.github.io/projection/v1.0.0/schema.json", + "https://stac-extensions.github.io/raster/v1.0.0/schema.json" + ], + "id": "S2B_33SVB_20210221_0_L2A", + "bbox": [ + 13.86148243891681, + 36.95257399124932, + 15.111074610520053, + 37.94752813015372 + ], + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 13.876381589019879, + 36.95257399124932 + ], + [ + 13.86148243891681, + 37.942072015005024 + ], + [ + 15.111074610520053, + 37.94752813015372 + ], + [ + 15.109620666835209, + 36.95783951241028 + ], + [ + 13.876381589019879, + 36.95257399124932 + ] + ] + ] + }, + "properties": { + "datetime": "2021-02-21T10:00:17Z", + "platform": "sentinel-2b", + "constellation": "sentinel-2", + "instruments": [ + "msi" + ], + "gsd": 10, + "view:off_nadir": 0, + "proj:epsg": 32633, + "sentinel:utm_zone": 33, + "sentinel:latitude_band": "S", + "sentinel:grid_square": "VB", + "sentinel:sequence": "0", + "sentinel:product_id": "S2B_MSIL2A_20210221T095029_N0214_R079_T33SVB_20210221T115149", + "sentinel:data_coverage": 100, + "eo:cloud_cover": 21.22, + "sentinel:valid_cloud_cover": true + }, + "assets": { + "thumbnail": { + "title": "Thumbnail", + "type": "image/png", + "roles": [ + "thumbnail" + ], + "href": "https://roda.sentinel-hub.com/sentinel-s2-l1c/tiles/33/S/VB/2021/2/21/0/preview.jpg" + }, + "overview": { + "title": "True color image", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "roles": [ + "overview" + ], + "gsd": 10, + "eo:bands": [ + { + "name": "B04", + "common_name": "red", + "center_wavelength": 0.6645, + "full_width_half_max": 0.038 + }, + { + "name": "B03", + "common_name": "green", + "center_wavelength": 0.56, + "full_width_half_max": 0.045 + }, + { + "name": "B02", + "common_name": "blue", + "center_wavelength": 0.4966, + "full_width_half_max": 0.098 + } + ], + "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/33/S/VB/2021/2/S2B_33SVB_20210221_0_L2A/L2A_PVI.tif", + "proj:shape": [ + 343, + 343 + ], + "proj:transform": [ + 320, + 0, + 399960, + 0, + -320, + 4200000, + 0, + 0, + 1 + ] + }, + "info": { + "title": "Original JSON metadata", + "type": "application/json", + "roles": [ + "metadata" + ], + "href": "https://roda.sentinel-hub.com/sentinel-s2-l2a/tiles/33/S/VB/2021/2/21/0/tileInfo.json" + }, + "metadata": { + "title": "Original XML metadata", + "type": "application/xml", + "roles": [ + "metadata" + ], + "href": "https://roda.sentinel-hub.com/sentinel-s2-l2a/tiles/33/S/VB/2021/2/21/0/metadata.xml" + }, + "visual": { + "title": "True color image", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "roles": [ + "overview" + ], + "gsd": 10, + "eo:bands": [ + { + "name": "B04", + "common_name": "red", + "center_wavelength": 0.6645, + "full_width_half_max": 0.038 + }, + { + "name": "B03", + "common_name": "green", + "center_wavelength": 0.56, + "full_width_half_max": 0.045 + }, + { + "name": "B02", + "common_name": "blue", + "center_wavelength": 0.4966, + "full_width_half_max": 0.098 + } + ], + "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/33/S/VB/2021/2/S2B_33SVB_20210221_0_L2A/TCI.tif", + "proj:shape": [ + 10980, + 10980 + ], + "proj:transform": [ + 10, + 0, + 399960, + 0, + -10, + 4200000, + 0, + 0, + 1 + ] + }, + "B01": { + "title": "Band 1 (coastal) BOA reflectance", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "roles": [ + "data" + ], + "gsd": 60, + "eo:bands": [ + { + "name": "B01", + "common_name": "coastal", + "center_wavelength": 0.4439, + "full_width_half_max": 0.027 + } + ], + "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/33/S/VB/2021/2/S2B_33SVB_20210221_0_L2A/B01.tif", + "proj:shape": [ + 1830, + 1830 + ], + "proj:transform": [ + 60, + 0, + 399960, + 0, + -60, + 4200000, + 0, + 0, + 1 + ], + "raster:bands": [ + { + "data_type": "uint16", + "spatial_resolution": 60, + "nodata": 0, + "statistics": { + "minimum": 1, + "maximum": 20567, + "mean": 2339.4759595597, + "stddev": 3026.6973619954, + "valid_percent": 99.83 + }, + "unit": "W/m²/sr/μm" + } + ] + }, + "B02": { + "title": "Band 2 (blue) BOA reflectance", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "roles": [ + "data" + ], + "gsd": 10, + "eo:bands": [ + { + "name": "B02", + "common_name": "blue", + "center_wavelength": 0.4966, + "full_width_half_max": 0.098 + } + ], + "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/33/S/VB/2021/2/S2B_33SVB_20210221_0_L2A/B02.tif", + "proj:shape": [ + 10980, + 10980 + ], + "proj:transform": [ + 10, + 0, + 399960, + 0, + -10, + 4200000, + 0, + 0, + 1 + ], + "raster:bands": [ + { + "data_type": "uint16", + "spatial_resolution": 10, + "nodata": 0, + "statistics": { + "minimum": 1, + "maximum": 19264, + "mean": 2348.069117847, + "stddev": 2916.5446249911, + "valid_percent": 99.99 + }, + "unit": "W/m²/sr/μm" + } + ] + }, + "B03": { + "title": "Band 3 (green) BOA reflectance", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "roles": [ + "data" + ], + "gsd": 10, + "eo:bands": [ + { + "name": "B03", + "common_name": "green", + "center_wavelength": 0.56, + "full_width_half_max": 0.045 + } + ], + "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/33/S/VB/2021/2/S2B_33SVB_20210221_0_L2A/B03.tif", + "proj:shape": [ + 10980, + 10980 + ], + "proj:transform": [ + 10, + 0, + 399960, + 0, + -10, + 4200000, + 0, + 0, + 1 + ], + "raster:bands": [ + { + "data_type": "uint16", + "spatial_resolution": 10, + "nodata": 0, + "statistics": { + "minimum": 1, + "maximum": 18064, + "mean": 2384.4680007438, + "stddev": 2675.410513295, + "valid_percent": 99.999 + }, + "unit": "W/m²/sr/μm" + } + ] + }, + "B04": { + "title": "Band 4 (red) BOA reflectance", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "roles": [ + "data" + ], + "gsd": 10, + "eo:bands": [ + { + "name": "B04", + "common_name": "red", + "center_wavelength": 0.6645, + "full_width_half_max": 0.038 + } + ], + "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/33/S/VB/2021/2/S2B_33SVB_20210221_0_L2A/B04.tif", + "proj:shape": [ + 10980, + 10980 + ], + "proj:transform": [ + 10, + 0, + 399960, + 0, + -10, + 4200000, + 0, + 0, + 1 + ], + "raster:bands": [ + { + "data_type": "uint16", + "spatial_resolution": 10, + "nodata": 0, + "statistics": { + "minimum": 1, + "maximum": 17200, + "mean": 2273.9667970732, + "stddev": 2618.272802792, + "valid_percent": 99.999 + }, + "unit": "W/m²/sr/μm" + } + ] + }, + "B05": { + "title": "Band 5 BOA reflectance", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "roles": [ + "data" + ], + "gsd": 20, + "eo:bands": [ + { + "name": "B05", + "center_wavelength": 0.7039, + "full_width_half_max": 0.019 + } + ], + "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/33/S/VB/2021/2/S2B_33SVB_20210221_0_L2A/B05.tif", + "proj:shape": [ + 5490, + 5490 + ], + "proj:transform": [ + 20, + 0, + 399960, + 0, + -20, + 4200000, + 0, + 0, + 1 + ], + "raster:bands": [ + { + "data_type": "uint16", + "spatial_resolution": 20, + "nodata": 0, + "statistics": { + "minimum": 1, + "maximum": 16842, + "mean": 2634.1490243416, + "stddev": 2634.1490243416, + "valid_percent": 99.999 + }, + "unit": "W/m²/sr/μm" + } + ] + }, + "B06": { + "title": "Band 6 BOA reflectance", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "roles": [ + "data" + ], + "gsd": 20, + "eo:bands": [ + { + "name": "B06", + "center_wavelength": 0.7402, + "full_width_half_max": 0.018 + } + ], + "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/33/S/VB/2021/2/S2B_33SVB_20210221_0_L2A/B06.tif", + "proj:shape": [ + 5490, + 5490 + ], + "proj:transform": [ + 20, + 0, + 399960, + 0, + -20, + 4200000, + 0, + 0, + 1 + ], + "raster:bands": [ + { + "data_type": "uint16", + "spatial_resolution": 20, + "nodata": 0, + "statistics": { + "minimum": 1, + "maximum": 16502, + "mean": 3329.8844628619, + "stddev": 2303.0096294469, + "valid_percent": 99.999 + }, + "unit": "W/m²/sr/μm" + } + ] + }, + "B07": { + "title": "Band 7 BOA reflectance", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "roles": [ + "data" + ], + "gsd": 20, + "eo:bands": [ + { + "name": "B07", + "center_wavelength": 0.7825, + "full_width_half_max": 0.028 + } + ], + "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/33/S/VB/2021/2/S2B_33SVB_20210221_0_L2A/B07.tif", + "proj:shape": [ + 5490, + 5490 + ], + "proj:transform": [ + 20, + 0, + 399960, + 0, + -20, + 4200000, + 0, + 0, + 1 + ] + }, + "B08": { + "title": "Band 8 (nir) BOA reflectance", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "roles": [ + "data" + ], + "gsd": 10, + "eo:bands": [ + { + "name": "B08", + "common_name": "nir", + "center_wavelength": 0.8351, + "full_width_half_max": 0.145 + } + ], + "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/33/S/VB/2021/2/S2B_33SVB_20210221_0_L2A/B08.tif", + "proj:shape": [ + 10980, + 10980 + ], + "proj:transform": [ + 10, + 0, + 399960, + 0, + -10, + 4200000, + 0, + 0, + 1 + ] + }, + "B8A": { + "title": "Band 8A BOA reflectance", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "roles": [ + "data" + ], + "gsd": 20, + "eo:bands": [ + { + "name": "B8A", + "center_wavelength": 0.8648, + "full_width_half_max": 0.033 + } + ], + "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/33/S/VB/2021/2/S2B_33SVB_20210221_0_L2A/B8A.tif", + "proj:shape": [ + 5490, + 5490 + ], + "proj:transform": [ + 20, + 0, + 399960, + 0, + -20, + 4200000, + 0, + 0, + 1 + ] + }, + "B09": { + "title": "Band 9 BOA reflectance", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "roles": [ + "data" + ], + "gsd": 60, + "eo:bands": [ + { + "name": "B09", + "center_wavelength": 0.945, + "full_width_half_max": 0.026 + } + ], + "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/33/S/VB/2021/2/S2B_33SVB_20210221_0_L2A/B09.tif", + "proj:shape": [ + 1830, + 1830 + ], + "proj:transform": [ + 60, + 0, + 399960, + 0, + -60, + 4200000, + 0, + 0, + 1 + ] + }, + "B11": { + "title": "Band 11 (swir16) BOA reflectance", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "roles": [ + "data" + ], + "gsd": 20, + "eo:bands": [ + { + "name": "B11", + "common_name": "swir16", + "center_wavelength": 1.6137, + "full_width_half_max": 0.143 + } + ], + "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/33/S/VB/2021/2/S2B_33SVB_20210221_0_L2A/B11.tif", + "proj:shape": [ + 5490, + 5490 + ], + "proj:transform": [ + 20, + 0, + 399960, + 0, + -20, + 4200000, + 0, + 0, + 1 + ] + }, + "B12": { + "title": "Band 12 (swir22) BOA reflectance", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "roles": [ + "data" + ], + "gsd": 20, + "eo:bands": [ + { + "name": "B12", + "common_name": "swir22", + "center_wavelength": 2.22024, + "full_width_half_max": 0.242 + } + ], + "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/33/S/VB/2021/2/S2B_33SVB_20210221_0_L2A/B12.tif", + "proj:shape": [ + 5490, + 5490 + ], + "proj:transform": [ + 20, + 0, + 399960, + 0, + -20, + 4200000, + 0, + 0, + 1 + ] + }, + "AOT": { + "title": "Aerosol Optical Thickness (AOT)", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "roles": [ + "data" + ], + "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/33/S/VB/2021/2/S2B_33SVB_20210221_0_L2A/AOT.tif", + "proj:shape": [ + 1830, + 1830 + ], + "proj:transform": [ + 60, + 0, + 399960, + 0, + -60, + 4200000, + 0, + 0, + 1 + ] + }, + "WVP": { + "title": "Water Vapour (WVP)", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "roles": [ + "data" + ], + "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/33/S/VB/2021/2/S2B_33SVB_20210221_0_L2A/WVP.tif", + "proj:shape": [ + 10980, + 10980 + ], + "proj:transform": [ + 10, + 0, + 399960, + 0, + -10, + 4200000, + 0, + 0, + 1 + ] + }, + "SCL": { + "title": "Scene Classification Map (SCL)", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "roles": [ + "data" + ], + "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/33/S/VB/2021/2/S2B_33SVB_20210221_0_L2A/SCL.tif", + "proj:shape": [ + 5490, + 5490 + ], + "proj:transform": [ + 20, + 0, + 399960, + 0, + -20, + 4200000, + 0, + 0, + 1 + ] + } + }, + "virtual:assets": { + "SIR": { + "title": "Shortwave Infra-red", + "raster:range": [ + 0, + 10000 + ], + "href": [ + "#B12", + "#B8A", + "#B04" + ] + } + }, + "links": [] +} \ No newline at end of file diff --git a/tests/extensions/test_custom.py b/tests/extensions/test_custom.py index f3939b040..76fc5444f 100644 --- a/tests/extensions/test_custom.py +++ b/tests/extensions/test_custom.py @@ -34,7 +34,7 @@ def apply(self, test_prop: Optional[str]) -> None: @property def test_prop(self) -> Optional[str]: - self._get_property(TEST_PROP, str) + return self._get_property(TEST_PROP, str) @test_prop.setter def test_prop(self, v: Optional[str]) -> None: @@ -139,7 +139,7 @@ def migrate( class CustomExtensionTest(unittest.TestCase): - def setUp(self): + def setUp(self) -> None: pystac.EXTENSION_HOOKS.add_extension_hooks(CustomExtensionHooks()) def tearDown(self) -> None: @@ -147,5 +147,5 @@ def tearDown(self) -> None: # TODO: Test custom extensions and extension hooks - def test_migrates(self): + def test_migrates(self) -> None: pass diff --git a/tests/extensions/test_eo.py b/tests/extensions/test_eo.py index 0fe73ab10..e953c15f1 100644 --- a/tests/extensions/test_eo.py +++ b/tests/extensions/test_eo.py @@ -9,6 +9,33 @@ from tests.utils import TestCases, test_to_from_dict +class BandsTest(unittest.TestCase): + def setUp(self) -> None: + self.maxDiff = None + + def test_create(self) -> None: + band = Band.create( + name="B01", + common_name="red", + description=Band.band_description("red"), + center_wavelength=0.65, + full_width_half_max=0.1, + ) + + self.assertEqual(band.name, "B01") + self.assertEqual(band.common_name, "red") + self.assertEqual(band.description, "Common name: red, Range: 0.6 to 0.7") + self.assertEqual(band.center_wavelength, 0.65) + self.assertEqual(band.full_width_half_max, 0.1) + + self.assertEqual(band.__repr__(), "") + + def test_band_description_unknown_band(self) -> None: + desc = Band.band_description("rainbow") + + self.assertIsNone(desc) + + class EOTest(unittest.TestCase): LANDSAT_EXAMPLE_URI = TestCases.get_path("data-files/eo/eo-landsat-example.json") BANDS_IN_ITEM_URI = TestCases.get_path( @@ -16,21 +43,21 @@ class EOTest(unittest.TestCase): ) EO_COLLECTION_URI = TestCases.get_path("data-files/eo/eo-collection.json") - def setUp(self): + def setUp(self) -> None: self.maxDiff = None - def test_to_from_dict(self): + def test_to_from_dict(self) -> None: with open(self.LANDSAT_EXAMPLE_URI) as f: item_dict = json.load(f) test_to_from_dict(self, Item, item_dict) - def test_validate_eo(self): + def test_validate_eo(self) -> None: item = pystac.read_file(self.LANDSAT_EXAMPLE_URI) item2 = pystac.read_file(self.BANDS_IN_ITEM_URI) item.validate() item2.validate() - def test_bands(self): + def test_bands(self) -> None: item = pystac.Item.from_file(self.BANDS_IN_ITEM_URI) # Get @@ -56,7 +83,7 @@ def test_bands(self): self.assertEqual(len(EOExtension.ext(item).bands or []), 3) item.validate() - def test_asset_bands(self): + def test_asset_bands(self) -> None: item = pystac.Item.from_file(self.LANDSAT_EXAMPLE_URI) # Get @@ -66,6 +93,7 @@ def test_asset_bands(self): assert asset_bands is not None self.assertEqual(len(asset_bands), 1) self.assertEqual(asset_bands[0].name, "B1") + self.assertEqual(asset_bands[0].solar_illumination, 2000) index_asset = item.assets["index"] asset_bands = EOExtension.ext(index_asset).bands @@ -88,9 +116,21 @@ def test_asset_bands(self): # Check adding a new asset new_bands = [ - Band.create(name="red", description=Band.band_description("red")), - Band.create(name="green", description=Band.band_description("green")), - Band.create(name="blue", description=Band.band_description("blue")), + Band.create( + name="red", + description=Band.band_description("red"), + solar_illumination=1900, + ), + Band.create( + name="green", + description=Band.band_description("green"), + solar_illumination=1950, + ), + Band.create( + name="blue", + description=Band.band_description("blue"), + solar_illumination=2000, + ), ] asset = pystac.Asset(href="some/path.tif", media_type=pystac.MediaType.GEOTIFF) EOExtension.ext(asset).bands = new_bands @@ -98,7 +138,7 @@ def test_asset_bands(self): self.assertEqual(len(item.assets["test"].properties["eo:bands"]), 3) - def test_cloud_cover(self): + def test_cloud_cover(self) -> None: item = pystac.Item.from_file(self.LANDSAT_EXAMPLE_URI) # Get @@ -125,13 +165,14 @@ def test_cloud_cover(self): item.validate() - def test_summaries(self): + def test_summaries(self) -> None: col = pystac.Collection.from_file(self.EO_COLLECTION_URI) eo_summaries = EOExtension.summaries(col) # Get cloud_cover_summaries = eo_summaries.cloud_cover + assert cloud_cover_summaries is not None self.assertEqual(cloud_cover_summaries.minimum, 0.0) self.assertEqual(cloud_cover_summaries.maximum, 80.0) @@ -148,7 +189,7 @@ def test_summaries(self): self.assertEqual(len(col_dict["summaries"]["eo:bands"]), 1) self.assertEqual(col_dict["summaries"]["eo:cloud_cover"]["minimum"], 1.0) - def test_read_pre_09_fields_into_common_metadata(self): + def test_read_pre_09_fields_into_common_metadata(self) -> None: eo_item = pystac.Item.from_file( TestCases.get_path( "data-files/examples/0.8.1/item-spec/examples/" "landsat8-sample.json" @@ -158,7 +199,7 @@ def test_read_pre_09_fields_into_common_metadata(self): self.assertEqual(eo_item.common_metadata.platform, "landsat-8") self.assertEqual(eo_item.common_metadata.instruments, ["oli_tirs"]) - def test_reads_asset_bands_in_pre_1_0_version(self): + def test_reads_asset_bands_in_pre_1_0_version(self) -> None: item = pystac.Item.from_file( TestCases.get_path( "data-files/examples/0.9.0/item-spec/examples/" "landsat8-sample.json" @@ -170,7 +211,7 @@ def test_reads_asset_bands_in_pre_1_0_version(self): self.assertEqual(len(bands or []), 1) self.assertEqual(get_opt(bands)[0].common_name, "cirrus") - def test_reads_gsd_in_pre_1_0_version(self): + def test_reads_gsd_in_pre_1_0_version(self) -> None: eo_item = pystac.Item.from_file( TestCases.get_path( "data-files/examples/0.9.0/item-spec/examples/" "landsat8-sample.json" @@ -178,3 +219,23 @@ def test_reads_gsd_in_pre_1_0_version(self): ) self.assertEqual(eo_item.common_metadata.gsd, 30.0) + + def test_item_apply(self) -> None: + item = pystac.Item.from_file(self.LANDSAT_EXAMPLE_URI) + eo_ext = EOExtension.ext(item) + test_band = Band.create(name="test") + + self.assertEqual(eo_ext.cloud_cover, 78) + self.assertNotIn(test_band, eo_ext.bands or []) + + eo_ext.apply(bands=[test_band], cloud_cover=15) + assert eo_ext.bands is not None + + self.assertEqual(test_band.to_dict(), eo_ext.bands[0].to_dict()) + self.assertEqual(eo_ext.cloud_cover, 15) + + def test_extend_invalid_object(self) -> None: + link = pystac.Link("child", "https://some-domain.com/some/path/to.json") + + with self.assertRaises(pystac.ExtensionTypeError): + EOExtension.ext(link) # type: ignore diff --git a/tests/extensions/test_file.py b/tests/extensions/test_file.py index b0952ecf0..ff41e087e 100644 --- a/tests/extensions/test_file.py +++ b/tests/extensions/test_file.py @@ -9,19 +9,19 @@ class FileTest(unittest.TestCase): FILE_EXAMPLE_URI = TestCases.get_path("data-files/file/file-example.json") - def setUp(self): + def setUp(self) -> None: self.maxDiff = None - def test_to_from_dict(self): + def test_to_from_dict(self) -> None: with open(self.FILE_EXAMPLE_URI) as f: item_dict = json.load(f) test_to_from_dict(self, pystac.Item, item_dict) - def test_validate_file(self): + def test_validate_file(self) -> None: item = pystac.Item.from_file(self.FILE_EXAMPLE_URI) item.validate() - def test_asset_size(self): + def test_asset_size(self) -> None: item = pystac.Item.from_file(self.FILE_EXAMPLE_URI) asset = item.assets["thumbnail"] @@ -34,7 +34,7 @@ def test_asset_size(self): self.assertEqual(new_size, FileExtension.ext(asset).size) item.validate() - def test_asset_checksum(self): + def test_asset_checksum(self) -> None: item = pystac.Item.from_file(self.FILE_EXAMPLE_URI) asset = item.assets["thumbnail"] @@ -50,7 +50,7 @@ def test_asset_checksum(self): self.assertEqual(new_checksum, FileExtension.ext(asset).checksum) item.validate() - def test_asset_data_type(self): + def test_asset_data_type(self) -> None: item = pystac.Item.from_file(self.FILE_EXAMPLE_URI) asset = item.assets["thumbnail"] @@ -63,7 +63,7 @@ def test_asset_data_type(self): self.assertEqual(new_data_type, FileExtension.ext(asset).data_type) item.validate() - def test_asset_nodata(self): + def test_asset_nodata(self) -> None: item = pystac.Item.from_file(self.FILE_EXAMPLE_URI) asset = item.assets["thumbnail"] @@ -76,7 +76,7 @@ def test_asset_nodata(self): self.assertEqual(new_nodata, FileExtension.ext(asset).nodata) item.validate() - def test_migrates_old_checksum(self): + def test_migrates_old_checksum(self) -> None: example_path = TestCases.get_path( "data-files/examples/1.0.0-beta.2/" "extensions/checksum/examples/sentinel1.json" diff --git a/tests/extensions/test_label.py b/tests/extensions/test_label.py index 26ec5d37f..f36c213bd 100644 --- a/tests/extensions/test_label.py +++ b/tests/extensions/test_label.py @@ -12,6 +12,7 @@ LabelOverview, LabelStatistics, LabelType, + LabelRelType, ) import pystac.validation from pystac.utils import get_opt @@ -19,7 +20,7 @@ class LabelTest(unittest.TestCase): - def setUp(self): + def setUp(self) -> None: self.maxDiff = None self.label_example_1_uri = TestCases.get_path( "data-files/label/label-example-1.json" @@ -28,13 +29,16 @@ def setUp(self): "data-files/label/label-example-2.json" ) + def test_rel_types(self) -> None: + self.assertEqual(str(LabelRelType.SOURCE), "source") + def test_to_from_dict(self) -> None: with open(self.label_example_1_uri, encoding="utf-8") as f: label_example_1_dict = json.load(f) test_to_from_dict(self, Item, label_example_1_dict) - def test_from_file(self): + def test_from_file(self) -> None: label_example_1 = Item.from_file(self.label_example_1_uri) overviews = get_opt(LabelExtension.ext(label_example_1).label_overviews) @@ -47,7 +51,7 @@ def test_from_file(self): label_example_2.validate() - def test_from_file_pre_081(self): + def test_from_file_pre_081(self) -> None: d = pystac.StacIO.default().read_json(self.label_example_1_uri) d["stac_version"] = "0.8.0-rc1" @@ -63,7 +67,7 @@ def test_from_file_pre_081(self): self.assertEqual(len(LabelExtension.ext(label_example_1).label_tasks or []), 2) - def test_get_sources(self): + def test_get_sources(self) -> None: cat = TestCases.test_case_1() items = cat.get_all_items() @@ -89,9 +93,10 @@ def test_validate_label(self) -> None: cat_read = Catalog.from_file(os.path.join(cat_dir, "catalog.json")) label_item_read = cat_read.get_item("area-2-2-labels", recursive=True) + assert label_item_read is not None label_item_read.validate() - def test_read_label_item_owns_asset(self): + def test_read_label_item_owns_asset(self) -> None: item = next( x for x in TestCases.test_case_2().get_all_items() @@ -101,7 +106,7 @@ def test_read_label_item_owns_asset(self): for asset_key in item.assets: self.assertEqual(item.assets[asset_key].owner, item) - def test_label_description(self): + def test_label_description(self) -> None: label_item = pystac.Item.from_file(self.label_example_1_uri) # Get @@ -116,7 +121,7 @@ def test_label_description(self): ) label_item.validate() - def test_label_type(self): + def test_label_type(self) -> None: label_item = pystac.Item.from_file(self.label_example_1_uri) # Get @@ -129,7 +134,7 @@ def test_label_type(self): self.assertEqual(LabelType.RASTER, label_item.properties["label:type"]) label_item.validate() - def test_label_properties(self): + def test_label_properties(self) -> None: label_item = pystac.Item.from_file(self.label_example_1_uri) label_item2 = pystac.Item.from_file(self.label_example_2_uri) @@ -145,7 +150,7 @@ def test_label_properties(self): self.assertEqual(["prop1", "prop2"], label_item.properties["label:properties"]) label_item.validate() - def test_label_classes(self): + def test_label_classes(self) -> None: # Get label_item = pystac.Item.from_file(self.label_example_1_uri) label_classes = LabelExtension.ext(label_item).label_classes @@ -171,7 +176,7 @@ def test_label_classes(self): label_item.validate() - def test_label_tasks(self): + def test_label_tasks(self) -> None: label_item = pystac.Item.from_file(self.label_example_1_uri) # Get @@ -184,7 +189,7 @@ def test_label_tasks(self): self.assertEqual(["classification"], label_item.properties["label:tasks"]) label_item.validate() - def test_label_methods(self): + def test_label_methods(self) -> None: label_item = pystac.Item.from_file(self.label_example_1_uri) # Get @@ -199,7 +204,7 @@ def test_label_methods(self): ) label_item.validate() - def test_label_overviews(self): + def test_label_overviews(self) -> None: # Get label_item = pystac.Item.from_file(self.label_example_1_uri) label_ext = LabelExtension.ext(label_item) @@ -215,14 +220,18 @@ def test_label_overviews(self): label_counts = get_opt(label_overviews[0].counts) self.assertEqual(label_counts[1].count, 17) - get_opt(label_ext.label_overviews)[0].counts[1].count = 18 + fisrt_overview_counts = get_opt(label_ext.label_overviews)[0].counts + assert fisrt_overview_counts is not None + fisrt_overview_counts[1].count = 18 self.assertEqual( label_item.properties["label:overviews"][0]["counts"][1]["count"], 18 ) label_statistics = get_opt(label_overviews[1].statistics) self.assertEqual(label_statistics[0].name, "mean") - get_opt(label_ext.label_overviews)[1].statistics[0].name = "avg" + second_overview_statistics = get_opt(label_ext.label_overviews)[1].statistics + assert second_overview_statistics is not None + second_overview_statistics[0].name = "avg" self.assertEqual( label_item.properties["label:overviews"][1]["statistics"][0]["name"], "avg" ) diff --git a/tests/extensions/test_pointcloud.py b/tests/extensions/test_pointcloud.py index c43d3e32b..e0f7370c0 100644 --- a/tests/extensions/test_pointcloud.py +++ b/tests/extensions/test_pointcloud.py @@ -17,19 +17,19 @@ class PointcloudTest(unittest.TestCase): - def setUp(self): + def setUp(self) -> None: self.maxDiff = None self.example_uri = TestCases.get_path("data-files/pointcloud/example-laz.json") self.example_uri_no_statistics = TestCases.get_path( "data-files/pointcloud/example-laz-no-statistics.json" ) - def test_to_from_dict(self): + def test_to_from_dict(self) -> None: with open(self.example_uri) as f: d = json.load(f) test_to_from_dict(self, pystac.Item, d) - def test_apply(self): + def test_apply(self) -> None: item = next(iter(TestCases.test_case_2().get_all_items())) self.assertFalse(PointcloudExtension.has_extension(item)) @@ -43,11 +43,11 @@ def test_apply(self): ) self.assertTrue(PointcloudExtension.has_extension(item)) - def test_validate_pointcloud(self): + def test_validate_pointcloud(self) -> None: item = pystac.read_file(self.example_uri) item.validate() - def test_count(self): + def test_count(self) -> None: pc_item = pystac.Item.from_file(self.example_uri) # Get @@ -69,7 +69,7 @@ def test_count(self): PointcloudExtension.ext(pc_item).count = "not_an_int" # type:ignore pc_item.validate() - def test_type(self): + def test_type(self) -> None: pc_item = pystac.Item.from_file(self.example_uri) # Get @@ -84,7 +84,7 @@ def test_type(self): # Validate pc_item.validate() - def test_encoding(self): + def test_encoding(self) -> None: pc_item = pystac.Item.from_file(self.example_uri) # Get @@ -99,7 +99,7 @@ def test_encoding(self): # Validate pc_item.validate() - def test_schemas(self): + def test_schemas(self) -> None: pc_item = pystac.Item.from_file(self.example_uri) # Get @@ -117,14 +117,14 @@ def test_schemas(self): # Validate pc_item.validate() - def test_statistics(self): + def test_statistics(self) -> None: pc_item = pystac.Item.from_file(self.example_uri) # Get self.assertIn("pc:statistics", pc_item.properties) - pc_statistics = [ - s.to_dict() for s in PointcloudExtension.ext(pc_item).statistics - ] + statistics = PointcloudExtension.ext(pc_item).statistics + assert statistics is not None + pc_statistics = [s.to_dict() for s in statistics] self.assertEqual(pc_statistics, pc_item.properties["pc:statistics"]) # Set @@ -150,7 +150,7 @@ def test_statistics(self): # Validate pc_item.validate - def test_density(self): + def test_density(self) -> None: pc_item = pystac.Item.from_file(self.example_uri) # Get self.assertIn("pc:density", pc_item.properties) @@ -163,7 +163,7 @@ def test_density(self): # Validate pc_item.validate() - def test_pointcloud_schema(self): + def test_pointcloud_schema(self) -> None: props: Dict[str, Any] = { "name": "test", "size": 8, @@ -197,7 +197,7 @@ def test_pointcloud_schema(self): with self.assertRaises(STACError): empty_schema.type - def test_pointcloud_statistics(self): + def test_pointcloud_statistics(self) -> None: props: Dict[str, Any] = { "average": 1, "count": 1, @@ -251,11 +251,11 @@ def test_pointcloud_statistics(self): with self.assertRaises(STACError): empty_stat.name - def test_statistics_accessor_when_no_stats(self): + def test_statistics_accessor_when_no_stats(self) -> None: pc_item = pystac.Item.from_file(self.example_uri_no_statistics) self.assertEqual(PointcloudExtension.ext(pc_item).statistics, None) - def test_asset_extension(self): + def test_asset_extension(self) -> None: asset = Asset( "https://github.com/PDAL/PDAL/blob" "/a6c986f68458e92414a66c664408bee4737bbb08/test/data/laz" @@ -273,7 +273,7 @@ def test_asset_extension(self): self.assertEqual(ext.properties, asset.properties) self.assertEqual(ext.additional_read_properties, [pc_item.properties]) - def test_ext(self): + def test_ext(self) -> None: pc_item = pystac.Item.from_file(self.example_uri_no_statistics) PointcloudExtension.ext(pc_item) asset = Asset( diff --git a/tests/extensions/test_projection.py b/tests/extensions/test_projection.py index 77be9f3ae..20ad81d7a 100644 --- a/tests/extensions/test_projection.py +++ b/tests/extensions/test_projection.py @@ -71,18 +71,18 @@ class ProjectionTest(unittest.TestCase): - def setUp(self): + def setUp(self) -> None: self.maxDiff = None self.example_uri = TestCases.get_path( "data-files/projection/example-landsat8.json" ) - def test_to_from_dict(self): + def test_to_from_dict(self) -> None: with open(self.example_uri) as f: d = json.load(f) test_to_from_dict(self, pystac.Item, d) - def test_apply(self): + def test_apply(self) -> None: item = next(iter(TestCases.test_case_2().get_all_items())) self.assertFalse(ProjectionExtension.has_extension(item)) @@ -98,7 +98,7 @@ def test_apply(self): transform=[30.0, 0.0, 224985.0, 0.0, -30.0, 6790215.0, 0.0, 0.0, 1.0], ) - def test_partial_apply(self): + def test_partial_apply(self) -> None: proj_item = pystac.Item.from_file(self.example_uri) ProjectionExtension.ext(proj_item).apply(epsg=1111) @@ -106,11 +106,11 @@ def test_partial_apply(self): self.assertEqual(ProjectionExtension.ext(proj_item).epsg, 1111) proj_item.validate() - def test_validate_proj(self): + def test_validate_proj(self) -> None: item = pystac.Item.from_file(self.example_uri) item.validate() - def test_epsg(self): + def test_epsg(self) -> None: proj_item = pystac.Item.from_file(self.example_uri) # Get @@ -119,6 +119,7 @@ def test_epsg(self): self.assertEqual(proj_epsg, proj_item.properties["proj:epsg"]) # Set + assert proj_epsg is not None ProjectionExtension.ext(proj_item).epsg = proj_epsg + 100 self.assertEqual(proj_epsg + 100, proj_item.properties["proj:epsg"]) @@ -142,7 +143,7 @@ def test_epsg(self): # Validate proj_item.validate() - def test_wkt2(self): + def test_wkt2(self) -> None: proj_item = pystac.Item.from_file(self.example_uri) # Get @@ -177,7 +178,7 @@ def test_wkt2(self): # Validate proj_item.validate() - def test_projjson(self): + def test_projjson(self) -> None: proj_item = pystac.Item.from_file(self.example_uri) # Get @@ -196,9 +197,9 @@ def test_projjson(self): ProjectionExtension.ext(asset_no_prop).projjson, ProjectionExtension.ext(proj_item).projjson, ) - self.assertEqual( - ProjectionExtension.ext(asset_prop).projjson["id"]["code"], 9999 - ) + asset_prop_json = ProjectionExtension.ext(asset_prop).projjson + assert asset_prop_json is not None + self.assertEqual(asset_prop_json["id"]["code"], 9999) # Set to Asset asset_value = deepcopy(PROJJSON) @@ -208,9 +209,9 @@ def test_projjson(self): ProjectionExtension.ext(asset_no_prop).projjson, ProjectionExtension.ext(proj_item).projjson, ) - self.assertEqual( - ProjectionExtension.ext(asset_no_prop).projjson["id"]["code"], 7777 - ) + asset_no_prop_json = ProjectionExtension.ext(asset_no_prop).projjson + assert asset_no_prop_json is not None + self.assertEqual(asset_no_prop_json["id"]["code"], 7777) # Validate proj_item.validate() @@ -220,7 +221,7 @@ def test_projjson(self): ProjectionExtension.ext(proj_item).projjson = {"bad": "data"} proj_item.validate() - def test_geometry(self): + def test_geometry(self) -> None: proj_item = pystac.Item.from_file(self.example_uri) # Get @@ -239,10 +240,9 @@ def test_geometry(self): ProjectionExtension.ext(asset_no_prop).geometry, ProjectionExtension.ext(proj_item).geometry, ) - self.assertEqual( - ProjectionExtension.ext(asset_prop).geometry["coordinates"][0][0], - [0.0, 0.0], - ) + asset_prop_geometry = ProjectionExtension.ext(asset_prop).geometry + assert asset_prop_geometry is not None + self.assertEqual(asset_prop_geometry["coordinates"][0][0], [0.0, 0.0]) # Set to Asset asset_value: Dict[str, Any] = {"type": "Point", "coordinates": [1.0, 2.0]} @@ -261,7 +261,7 @@ def test_geometry(self): ProjectionExtension.ext(proj_item).geometry = {"bad": "data"} proj_item.validate() - def test_bbox(self): + def test_bbox(self) -> None: proj_item = pystac.Item.from_file(self.example_uri) # Get @@ -294,7 +294,7 @@ def test_bbox(self): # Validate proj_item.validate() - def test_centroid(self): + def test_centroid(self) -> None: proj_item = pystac.Item.from_file(self.example_uri) # Get @@ -335,7 +335,7 @@ def test_centroid(self): ProjectionExtension.ext(proj_item).centroid = {"lat": 2.0, "lng": 3.0} proj_item.validate() - def test_shape(self): + def test_shape(self) -> None: proj_item = pystac.Item.from_file(self.example_uri) # Get @@ -369,7 +369,7 @@ def test_shape(self): # Validate proj_item.validate() - def test_transform(self): + def test_transform(self) -> None: proj_item = pystac.Item.from_file(self.example_uri) # Get diff --git a/tests/extensions/test_raster.py b/tests/extensions/test_raster.py new file mode 100644 index 000000000..d596ba30d --- /dev/null +++ b/tests/extensions/test_raster.py @@ -0,0 +1,199 @@ +import json +import unittest + +import pystac +from pystac import Item +from pystac.utils import get_opt +from pystac.extensions.raster import ( + Histogram, + RasterExtension, + RasterBand, + Sampling, + DataType, + Statistics, +) +from tests.utils import TestCases, test_to_from_dict + + +class RasterTest(unittest.TestCase): + PLANET_EXAMPLE_URI = TestCases.get_path( + "data-files/raster/raster-planet-example.json" + ) + SENTINEL2_EXAMPLE_URI = TestCases.get_path( + "data-files/raster/raster-sentinel2-example.json" + ) + GDALINFO_EXAMPLE_URI = TestCases.get_path("data-files/raster/gdalinfo.json") + + def setUp(self) -> None: + self.maxDiff = None + + def test_to_from_dict(self) -> None: + with open(self.PLANET_EXAMPLE_URI) as f: + item_dict = json.load(f) + test_to_from_dict(self, Item, item_dict) + + def test_validate_raster(self) -> None: + item = pystac.read_file(self.PLANET_EXAMPLE_URI) + item2 = pystac.read_file(self.SENTINEL2_EXAMPLE_URI) + item.validate() + item2.validate() + + def test_asset_bands(self) -> None: + item = pystac.Item.from_file(self.PLANET_EXAMPLE_URI) + + # Get + data_asset = item.assets["data"] + asset_bands = RasterExtension.ext(data_asset).bands + assert asset_bands is not None + self.assertEqual(len(asset_bands), 4) + self.assertEqual(asset_bands[0].nodata, 0) + self.assertEqual(asset_bands[0].sampling, Sampling.AREA) + self.assertEqual(asset_bands[0].unit, "W⋅sr−1⋅m−2⋅nm−1") + self.assertEqual(asset_bands[0].data_type, DataType.UINT16) + self.assertEqual(asset_bands[0].scale, 0.01) + self.assertEqual(asset_bands[0].offset, 0) + self.assertEqual(asset_bands[0].spatial_resolution, 3) + + band0_stats = asset_bands[0].statistics + assert band0_stats is not None + self.assertEqual(band0_stats.minimum, 1962) + self.assertEqual(band0_stats.maximum, 32925) + self.assertEqual(band0_stats.mean, 8498.9400644319) + self.assertEqual(band0_stats.stddev, 5056.1292002722) + self.assertEqual(band0_stats.valid_percent, 61.09) + + band0_hist = asset_bands[0].histogram + assert band0_hist is not None + self.assertEqual(band0_hist.count, 256) + self.assertEqual(band0_hist.min, 1901.288235294118) + self.assertEqual(band0_hist.max, 32985.71176470588) + self.assertEqual(len(band0_hist.buckets), band0_hist.count) + + index_asset = item.assets["metadata"] + asset_bands = RasterExtension.ext(index_asset).bands + self.assertIs(None, asset_bands) + + # Set + item2 = pystac.Item.from_file(self.SENTINEL2_EXAMPLE_URI) + b2_asset = item2.assets["B02"] + self.assertEqual( + get_opt(get_opt(RasterExtension.ext(b2_asset).bands)[0].statistics).maximum, + 19264, + ) + b1_asset = item2.assets["B01"] + RasterExtension.ext(b2_asset).bands = RasterExtension.ext(b1_asset).bands + + new_b2_asset_bands = RasterExtension.ext(item2.assets["B02"]).bands + + self.assertEqual( + get_opt(get_opt(new_b2_asset_bands)[0].statistics).maximum, 20567 + ) + + item2.validate() + + # Check adding a new asset + new_stats = [ + Statistics.create( + minimum=0, maximum=10000, mean=5000, stddev=10, valid_percent=88 + ), + Statistics.create( + minimum=-1, maximum=1, mean=0, stddev=1, valid_percent=100 + ), + Statistics.create( + minimum=1, maximum=255, mean=200, stddev=3, valid_percent=100 + ), + ] + # new_histograms = [] + with open(self.GDALINFO_EXAMPLE_URI) as gdaljson_file: + gdaljson_data = json.load(gdaljson_file) + new_histograms = list( + map( + lambda band: Histogram.from_dict(band["histogram"]), + gdaljson_data["bands"], + ) + ) + new_bands = [ + RasterBand.create( + nodata=1, + unit="test1", + statistics=new_stats[0], + histogram=new_histograms[0], + ), + RasterBand.create( + nodata=2, + unit="test2", + statistics=new_stats[1], + histogram=new_histograms[1], + ), + RasterBand.create( + nodata=3, + unit="test3", + statistics=new_stats[2], + histogram=new_histograms[2], + ), + ] + asset = pystac.Asset(href="some/path.tif", media_type=pystac.MediaType.GEOTIFF) + RasterExtension.ext(asset).bands = new_bands + item.add_asset("test", asset) + + self.assertEqual(len(item.assets["test"].properties["raster:bands"]), 3) + self.assertEqual( + item.assets["test"].properties["raster:bands"][1]["statistics"]["minimum"], + -1, + ) + self.assertEqual( + item.assets["test"].properties["raster:bands"][1]["histogram"]["min"], + 3848.354901960784, + ) + + for s in new_stats: + s.minimum = None + s.maximum = None + s.mean = None + s.stddev = None + s.valid_percent = None + self.assertEqual(len(s.properties), 0) + + for b in new_bands: + b.bits_per_sample = None + b.data_type = None + b.histogram = None + b.nodata = None + b.sampling = None + b.scale = None + b.spatial_resolution = None + b.statistics = None + b.unit = None + b.offset = None + self.assertEqual(len(b.properties), 0) + + new_stats[2].apply( + minimum=0, maximum=10000, mean=5000, stddev=10, valid_percent=88 + ) + new_stats[1].apply(minimum=-1, maximum=1, mean=0, stddev=1, valid_percent=100) + new_stats[0].apply( + minimum=1, maximum=255, mean=200, stddev=3, valid_percent=100 + ) + new_bands[2].apply( + nodata=1, + unit="test1", + statistics=new_stats[2], + histogram=new_histograms[0], + ) + new_bands[1].apply( + nodata=2, + unit="test2", + statistics=new_stats[1], + histogram=new_histograms[1], + ) + new_bands[0].apply( + nodata=3, + unit="test3", + statistics=new_stats[0], + histogram=new_histograms[2], + ) + RasterExtension.ext(item.assets["test"]).apply(new_bands) + self.assertEqual( + item.assets["test"].properties["raster:bands"][0]["statistics"]["minimum"], + 1, + ) diff --git a/tests/extensions/test_sar.py b/tests/extensions/test_sar.py index ae3920b6b..2696eba39 100644 --- a/tests/extensions/test_sar.py +++ b/tests/extensions/test_sar.py @@ -21,14 +21,14 @@ def make_item() -> pystac.Item: class SarItemExtTest(unittest.TestCase): - def setUp(self): + def setUp(self) -> None: super().setUp() self.item = make_item() - def test_stac_extensions(self): + def test_stac_extensions(self) -> None: self.assertTrue(SarExtension.has_extension(self.item)) - def test_required(self): + def test_required(self) -> None: mode: str = "Nonesense mode" frequency_band: sar.FrequencyBand = sar.FrequencyBand.P polarizations: List[sar.Polarization] = [ @@ -54,7 +54,7 @@ def test_required(self): self.item.validate() - def test_all(self): + def test_all(self) -> None: mode: str = "WV" frequency_band: sar.FrequencyBand = sar.FrequencyBand.KA polarizations: List[sar.Polarization] = [ @@ -127,7 +127,7 @@ def test_all(self): self.item.validate() - def test_polarization_must_be_list(self): + def test_polarization_must_be_list(self) -> None: mode: str = "Nonesense mode" frequency_band: sar.FrequencyBand = sar.FrequencyBand.P # Skip type hint as we are passing in an incorrect polarization. diff --git a/tests/extensions/test_sat.py b/tests/extensions/test_sat.py index 6831f2a7a..50fdbda5a 100644 --- a/tests/extensions/test_sat.py +++ b/tests/extensions/test_sat.py @@ -22,19 +22,19 @@ def make_item() -> pystac.Item: class SatTest(unittest.TestCase): - def setUp(self): + def setUp(self) -> None: super().setUp() self.item = make_item() - def test_stac_extensions(self): + def test_stac_extensions(self) -> None: self.assertTrue(SatExtension.has_extension(self.item)) - def test_no_args_fails(self): + def test_no_args_fails(self) -> None: SatExtension.ext(self.item).apply() with self.assertRaises(pystac.STACValidationError): self.item.validate() - def test_orbit_state(self): + def test_orbit_state(self) -> None: orbit_state = sat.OrbitState.ASCENDING SatExtension.ext(self.item).apply(orbit_state) self.assertEqual(orbit_state, SatExtension.ext(self.item).orbit_state) @@ -42,7 +42,7 @@ def test_orbit_state(self): self.assertFalse(SatExtension.ext(self.item).relative_orbit) self.item.validate() - def test_relative_orbit(self): + def test_relative_orbit(self) -> None: relative_orbit = 1234 SatExtension.ext(self.item).apply(None, relative_orbit) self.assertEqual(relative_orbit, SatExtension.ext(self.item).relative_orbit) @@ -50,13 +50,13 @@ def test_relative_orbit(self): self.assertFalse(SatExtension.ext(self.item).orbit_state) self.item.validate() - def test_relative_orbit_no_negative(self): + def test_relative_orbit_no_negative(self) -> None: negative_relative_orbit = -2 SatExtension.ext(self.item).apply(None, negative_relative_orbit) with self.assertRaises(pystac.STACValidationError): self.item.validate() - def test_both(self): + def test_both(self) -> None: orbit_state = sat.OrbitState.DESCENDING relative_orbit = 4321 SatExtension.ext(self.item).apply(orbit_state, relative_orbit) @@ -64,7 +64,7 @@ def test_both(self): self.assertEqual(relative_orbit, SatExtension.ext(self.item).relative_orbit) self.item.validate() - def test_modify(self): + def test_modify(self) -> None: SatExtension.ext(self.item).apply(sat.OrbitState.DESCENDING, 999) orbit_state = sat.OrbitState.GEOSTATIONARY @@ -75,7 +75,7 @@ def test_modify(self): self.assertEqual(relative_orbit, SatExtension.ext(self.item).relative_orbit) self.item.validate() - def test_from_dict(self): + def test_from_dict(self) -> None: orbit_state = sat.OrbitState.GEOSTATIONARY relative_orbit = 1001 d: Dict[str, Any] = { @@ -96,7 +96,7 @@ def test_from_dict(self): self.assertEqual(orbit_state, SatExtension.ext(item).orbit_state) self.assertEqual(relative_orbit, SatExtension.ext(item).relative_orbit) - def test_to_from_dict(self): + def test_to_from_dict(self) -> None: orbit_state = sat.OrbitState.GEOSTATIONARY relative_orbit = 1002 SatExtension.ext(self.item).apply(orbit_state, relative_orbit) @@ -108,14 +108,14 @@ def test_to_from_dict(self): self.assertEqual(orbit_state, SatExtension.ext(item).orbit_state) self.assertEqual(relative_orbit, SatExtension.ext(item).relative_orbit) - def test_clear_orbit_state(self): + def test_clear_orbit_state(self) -> None: SatExtension.ext(self.item).apply(sat.OrbitState.DESCENDING, 999) SatExtension.ext(self.item).orbit_state = None self.assertIsNone(SatExtension.ext(self.item).orbit_state) self.item.validate() - def test_clear_relative_orbit(self): + def test_clear_relative_orbit(self) -> None: SatExtension.ext(self.item).apply(sat.OrbitState.DESCENDING, 999) SatExtension.ext(self.item).relative_orbit = None diff --git a/tests/extensions/test_scientific.py b/tests/extensions/test_scientific.py index e0dc57cc9..eb1052351 100644 --- a/tests/extensions/test_scientific.py +++ b/tests/extensions/test_scientific.py @@ -2,6 +2,7 @@ import datetime import unittest +from typing import List, Optional import pystac from pystac.extensions import scientific @@ -40,14 +41,14 @@ def make_item() -> pystac.Item: class ItemScientificExtensionTest(unittest.TestCase): - def setUp(self): + def setUp(self) -> None: super().setUp() self.item = make_item() - def test_stac_extensions(self): + def test_stac_extensions(self) -> None: self.assertTrue(ScientificExtension.has_extension(self.item)) - def test_doi(self): + def test_doi(self) -> None: ScientificExtension.ext(self.item).apply(DOI) self.assertEqual(DOI, ScientificExtension.ext(self.item).doi) self.assertIn(scientific.DOI, self.item.properties) @@ -69,14 +70,14 @@ def test_doi(self): self.assertEqual(PUB1_DOI_URL, link.get_href()) self.item.validate() - def test_citation(self): + def test_citation(self) -> None: ScientificExtension.ext(self.item).apply(citation=CITATION) self.assertEqual(CITATION, ScientificExtension.ext(self.item).citation) self.assertIn(scientific.CITATION, self.item.properties) self.assertFalse(self.item.get_links(scientific.CITE_AS)) self.item.validate() - def test_publications_one(self): + def test_publications_one(self) -> None: publications = PUBLICATIONS[:1] ScientificExtension.ext(self.item).apply(publications=publications) self.assertEqual([1], [int("1")]) @@ -89,7 +90,7 @@ def test_publications_one(self): self.assertCountEqual(expected, doi_urls) self.item.validate() - def test_publications(self): + def test_publications(self) -> None: ScientificExtension.ext(self.item).apply(publications=PUBLICATIONS) self.assertEqual(PUBLICATIONS, ScientificExtension.ext(self.item).publications) self.assertIn(scientific.PUBLICATIONS, self.item.properties) @@ -100,7 +101,7 @@ def test_publications(self): self.assertCountEqual(expected, doi_urls) self.item.validate() - def test_remove_publication_one(self): + def test_remove_publication_one(self) -> None: publications = PUBLICATIONS[:1] ScientificExtension.ext(self.item).apply(DOI, publications=publications) ScientificExtension.ext(self.item).remove_publication(publications[0]) @@ -110,7 +111,7 @@ def test_remove_publication_one(self): self.assertEqual(DOI_URL, links[0].target) self.item.validate() - def test_remove_all_publications_one(self): + def test_remove_all_publications_one(self) -> None: publications = PUBLICATIONS[:1] ScientificExtension.ext(self.item).apply(DOI, publications=publications) ScientificExtension.ext(self.item).remove_publication() @@ -120,7 +121,7 @@ def test_remove_all_publications_one(self): self.assertEqual(DOI_URL, links[0].target) self.item.validate() - def test_remove_publication_forward(self): + def test_remove_publication_forward(self) -> None: ScientificExtension.ext(self.item).apply(DOI, publications=PUBLICATIONS) ScientificExtension.ext(self.item).remove_publication(PUBLICATIONS[0]) @@ -140,7 +141,7 @@ def test_remove_publication_forward(self): self.assertEqual(DOI_URL, links[0].target) self.item.validate() - def test_remove_publication_reverse(self): + def test_remove_publication_reverse(self) -> None: ScientificExtension.ext(self.item).apply(DOI, publications=PUBLICATIONS) ScientificExtension.ext(self.item).remove_publication(PUBLICATIONS[1]) @@ -158,7 +159,7 @@ def test_remove_publication_reverse(self): self.assertEqual(DOI_URL, links[0].target) self.item.validate() - def test_remove_all_publications_with_some(self): + def test_remove_all_publications_with_some(self) -> None: ScientificExtension.ext(self.item).apply(DOI, publications=PUBLICATIONS) ScientificExtension.ext(self.item).remove_publication() self.assertFalse(ScientificExtension.ext(self.item).publications) @@ -167,7 +168,7 @@ def test_remove_all_publications_with_some(self): self.assertEqual(DOI_URL, links[0].target) self.item.validate() - def test_remove_all_publications_with_none(self): + def test_remove_all_publications_with_none(self) -> None: ScientificExtension.ext(self.item).apply(DOI) ScientificExtension.ext(self.item).remove_publication() self.assertFalse(ScientificExtension.ext(self.item).publications) @@ -183,7 +184,8 @@ def make_collection() -> pystac.Collection: end = start + datetime.timedelta(5, 4, 3, 2, 1) bboxes = [[-180.0, -90.0, 180.0, 90.0]] spatial_extent = pystac.SpatialExtent(bboxes) - temporal_extent = pystac.TemporalExtent([[start, end]]) + intervals: List[List[Optional[datetime.datetime]]] = [[start, end]] + temporal_extent = pystac.TemporalExtent(intervals) extent = pystac.Extent(spatial_extent, temporal_extent) collection = pystac.Collection(asset_id, "desc", extent) collection.set_self_href(URL_TEMPLATE % 2019) @@ -193,14 +195,14 @@ def make_collection() -> pystac.Collection: class CollectionScientificExtensionTest(unittest.TestCase): - def setUp(self): + def setUp(self) -> None: super().setUp() self.collection = make_collection() - def test_stac_extensions(self): + def test_stac_extensions(self) -> None: self.assertTrue(ScientificExtension.has_extension(self.collection)) - def test_doi(self): + def test_doi(self) -> None: ScientificExtension.ext(self.collection).apply(DOI) self.assertEqual(DOI, ScientificExtension.ext(self.collection).doi) self.assertIn(scientific.DOI, self.collection.extra_fields) @@ -222,14 +224,14 @@ def test_doi(self): self.assertEqual(PUB1_DOI_URL, link.get_href()) self.collection.validate() - def test_citation(self): + def test_citation(self) -> None: ScientificExtension.ext(self.collection).apply(citation=CITATION) self.assertEqual(CITATION, ScientificExtension.ext(self.collection).citation) self.assertIn(scientific.CITATION, self.collection.extra_fields) self.assertFalse(self.collection.get_links(scientific.CITE_AS)) self.collection.validate() - def test_publications_one(self): + def test_publications_one(self) -> None: publications = PUBLICATIONS[:1] ScientificExtension.ext(self.collection).apply(publications=publications) self.assertEqual( @@ -244,7 +246,7 @@ def test_publications_one(self): self.collection.validate() - def test_publications(self): + def test_publications(self) -> None: ScientificExtension.ext(self.collection).apply(publications=PUBLICATIONS) self.assertEqual( PUBLICATIONS, ScientificExtension.ext(self.collection).publications @@ -258,7 +260,7 @@ def test_publications(self): self.collection.validate() - def test_remove_publication_one(self): + def test_remove_publication_one(self) -> None: publications = PUBLICATIONS[:1] ScientificExtension.ext(self.collection).apply(DOI, publications=publications) ScientificExtension.ext(self.collection).remove_publication(publications[0]) @@ -268,7 +270,7 @@ def test_remove_publication_one(self): self.assertEqual(DOI_URL, links[0].target) self.collection.validate() - def test_remove_all_publications_one(self): + def test_remove_all_publications_one(self) -> None: publications = PUBLICATIONS[:1] ScientificExtension.ext(self.collection).apply(DOI, publications=publications) ScientificExtension.ext(self.collection).remove_publication() @@ -278,7 +280,7 @@ def test_remove_all_publications_one(self): self.assertEqual(DOI_URL, links[0].target) self.collection.validate() - def test_remove_publication_forward(self): + def test_remove_publication_forward(self) -> None: ScientificExtension.ext(self.collection).apply(DOI, publications=PUBLICATIONS) ScientificExtension.ext(self.collection).remove_publication(PUBLICATIONS[0]) @@ -298,7 +300,7 @@ def test_remove_publication_forward(self): self.assertEqual(DOI_URL, links[0].target) self.collection.validate() - def test_remove_publication_reverse(self): + def test_remove_publication_reverse(self) -> None: ScientificExtension.ext(self.collection).apply(DOI, publications=PUBLICATIONS) ScientificExtension.ext(self.collection).remove_publication(PUBLICATIONS[1]) @@ -316,7 +318,7 @@ def test_remove_publication_reverse(self): self.assertEqual(DOI_URL, links[0].target) self.collection.validate() - def test_remove_all_publications_with_some(self): + def test_remove_all_publications_with_some(self) -> None: ScientificExtension.ext(self.collection).apply(DOI, publications=PUBLICATIONS) ScientificExtension.ext(self.collection).remove_publication() self.assertFalse(ScientificExtension.ext(self.collection).publications) @@ -325,7 +327,7 @@ def test_remove_all_publications_with_some(self): self.assertEqual(DOI_URL, links[0].target) self.collection.validate() - def test_remove_all_publications_with_none(self): + def test_remove_all_publications_with_none(self) -> None: ScientificExtension.ext(self.collection).apply(DOI) ScientificExtension.ext(self.collection).remove_publication() self.assertFalse(ScientificExtension.ext(self.collection).publications) diff --git a/tests/extensions/test_timestamps.py b/tests/extensions/test_timestamps.py index dad6dff3f..b01d86322 100644 --- a/tests/extensions/test_timestamps.py +++ b/tests/extensions/test_timestamps.py @@ -9,7 +9,7 @@ class TimestampsTest(unittest.TestCase): - def setUp(self): + def setUp(self) -> None: self.maxDiff = None self.example_uri = TestCases.get_path( "data-files/timestamps/example-landsat8.json" @@ -19,10 +19,10 @@ def setUp(self): self.sample_datetime_str = "2020-01-01T00:00:00Z" self.sample_datetime = str_to_datetime(self.sample_datetime_str) - def test_to_from_dict(self): + def test_to_from_dict(self) -> None: test_to_from_dict(self, pystac.Item, self.item_dict) - def test_apply(self): + def test_apply(self) -> None: item = next(iter(TestCases.test_case_2().get_all_items())) self.assertFalse(TimestampsExtension.has_extension(item)) @@ -58,11 +58,11 @@ def test_apply(self): for p in ("expires", "unpublished"): self.assertNotIn(p, item.properties) - def test_validate_timestamps(self): + def test_validate_timestamps(self) -> None: item = pystac.read_file(self.example_uri) item.validate() - def test_expires(self): + def test_expires(self) -> None: timestamps_item = pystac.Item.from_file(self.example_uri) # Get @@ -104,7 +104,7 @@ def test_expires(self): # Validate timestamps_item.validate() - def test_published(self): + def test_published(self) -> None: timestamps_item = pystac.Item.from_file(self.example_uri) # Get @@ -146,7 +146,7 @@ def test_published(self): # Validate timestamps_item.validate() - def test_unpublished(self): + def test_unpublished(self) -> None: timestamps_item = pystac.Item.from_file(self.example_uri) # Get diff --git a/tests/extensions/test_version.py b/tests/extensions/test_version.py index f036260e3..b5d8d7cc9 100644 --- a/tests/extensions/test_version.py +++ b/tests/extensions/test_version.py @@ -2,10 +2,11 @@ import datetime import unittest +from typing import List, Optional import pystac from pystac.extensions import version -from pystac.extensions.version import VersionExtension +from pystac.extensions.version import VersionExtension, VersionRelType from tests.utils import TestCases URL_TEMPLATE: str = "http://example.com/catalog/%s.json" @@ -29,39 +30,44 @@ def make_item(year: int) -> pystac.Item: class ItemVersionExtensionTest(unittest.TestCase): version: str = "1.2.3" - def setUp(self): + def setUp(self) -> None: super().setUp() self.item = make_item(2011) - def test_stac_extensions(self): + def test_rel_types(self) -> None: + self.assertEqual(str(VersionRelType.LATEST), "latest-version") + self.assertEqual(str(VersionRelType.PREDECESSOR), "predecessor-version") + self.assertEqual(str(VersionRelType.SUCCESSOR), "successor-version") + + def test_stac_extensions(self) -> None: self.assertTrue(VersionExtension.has_extension(self.item)) - def test_add_version(self): + def test_add_version(self) -> None: VersionExtension.ext(self.item).apply(self.version) self.assertEqual(self.version, VersionExtension.ext(self.item).version) self.assertNotIn(version.DEPRECATED, self.item.properties) self.assertFalse(VersionExtension.ext(self.item).deprecated) self.item.validate() - def test_version_in_properties(self): + def test_version_in_properties(self) -> None: VersionExtension.ext(self.item).apply(self.version, deprecated=True) self.assertIn(version.VERSION, self.item.properties) self.assertIn(version.DEPRECATED, self.item.properties) self.item.validate() - def test_add_not_deprecated_version(self): + def test_add_not_deprecated_version(self) -> None: VersionExtension.ext(self.item).apply(self.version, deprecated=False) self.assertIn(version.DEPRECATED, self.item.properties) self.assertFalse(VersionExtension.ext(self.item).deprecated) self.item.validate() - def test_add_deprecated_version(self): + def test_add_deprecated_version(self) -> None: VersionExtension.ext(self.item).apply(self.version, deprecated=True) self.assertIn(version.DEPRECATED, self.item.properties) self.assertTrue(VersionExtension.ext(self.item).deprecated) self.item.validate() - def test_latest(self): + def test_latest(self) -> None: year = 2013 latest = make_item(year) VersionExtension.ext(self.item).apply(self.version, latest=latest) @@ -69,11 +75,11 @@ def test_latest(self): self.assertIs(latest, latest_result) expected_href = URL_TEMPLATE % year - link = self.item.get_links(version.LATEST)[0] + link = self.item.get_links(VersionRelType.LATEST)[0] self.assertEqual(expected_href, link.get_href()) self.item.validate() - def test_predecessor(self): + def test_predecessor(self) -> None: year = 2010 predecessor = make_item(year) VersionExtension.ext(self.item).apply(self.version, predecessor=predecessor) @@ -81,11 +87,11 @@ def test_predecessor(self): self.assertIs(predecessor, predecessor_result) expected_href = URL_TEMPLATE % year - link = self.item.get_links(version.PREDECESSOR)[0] + link = self.item.get_links(VersionRelType.PREDECESSOR)[0] self.assertEqual(expected_href, link.get_href()) self.item.validate() - def test_successor(self): + def test_successor(self) -> None: year = 2012 successor = make_item(year) VersionExtension.ext(self.item).apply(self.version, successor=successor) @@ -93,15 +99,15 @@ def test_successor(self): self.assertIs(successor, successor_result) expected_href = URL_TEMPLATE % year - link = self.item.get_links(version.SUCCESSOR)[0] + link = self.item.get_links(VersionRelType.SUCCESSOR)[0] self.assertEqual(expected_href, link.get_href()) self.item.validate() - def test_fail_validate(self): + def test_fail_validate(self) -> None: with self.assertRaises(pystac.STACValidationError): self.item.validate() - def test_all_links(self): + def test_all_links(self) -> None: deprecated = True latest = make_item(2013) predecessor = make_item(2010) @@ -111,7 +117,7 @@ def test_all_links(self): ) self.item.validate() - def test_full_copy(self): + def test_full_copy(self) -> None: cat = TestCases.test_case_1() # Fetch two items from the catalog @@ -134,19 +140,28 @@ def test_full_copy(self): # Retrieve the copied version of the items item1_copy = cat_copy.get_item("area-1-1-imagery", recursive=True) + assert item1_copy is not None item2_copy = cat_copy.get_item("area-2-2-imagery", recursive=True) + assert item2_copy is not None # Check to see if the version links point to the instances of the # item objects as they should. - predecessor = item1_copy.get_single_link(version.PREDECESSOR).target - successor = item2_copy.get_single_link(version.SUCCESSOR).target - latest = item2_copy.get_single_link(version.LATEST).target - - self.assertIs(predecessor, item2_copy) - self.assertIs(successor, item1_copy) - self.assertIs(latest, item1_copy) - def test_setting_none_clears_link(self): + predecessor = item1_copy.get_single_link(VersionRelType.PREDECESSOR) + assert predecessor is not None + predecessor_target = predecessor.target + successor = item2_copy.get_single_link(VersionRelType.SUCCESSOR) + assert successor is not None + successor_target = successor.target + latest = item2_copy.get_single_link(VersionRelType.LATEST) + assert latest is not None + latest_target = latest.target + + self.assertIs(predecessor_target, item2_copy) + self.assertIs(successor_target, item1_copy) + self.assertIs(latest_target, item1_copy) + + def test_setting_none_clears_link(self) -> None: deprecated = False latest = make_item(2013) predecessor = make_item(2010) @@ -156,21 +171,21 @@ def test_setting_none_clears_link(self): ) VersionExtension.ext(self.item).latest = None - links = self.item.get_links(version.LATEST) + links = self.item.get_links(VersionRelType.LATEST) self.assertEqual(0, len(links)) self.assertIsNone(VersionExtension.ext(self.item).latest) VersionExtension.ext(self.item).predecessor = None - links = self.item.get_links(version.PREDECESSOR) + links = self.item.get_links(VersionRelType.PREDECESSOR) self.assertEqual(0, len(links)) self.assertIsNone(VersionExtension.ext(self.item).predecessor) VersionExtension.ext(self.item).successor = None - links = self.item.get_links(version.SUCCESSOR) + links = self.item.get_links(VersionRelType.SUCCESSOR) self.assertEqual(0, len(links)) self.assertIsNone(VersionExtension.ext(self.item).successor) - def test_multiple_link_setting(self): + def test_multiple_link_setting(self) -> None: deprecated = False latest1 = make_item(2013) predecessor1 = make_item(2010) @@ -183,7 +198,7 @@ def test_multiple_link_setting(self): latest2 = make_item(year) expected_href = URL_TEMPLATE % year VersionExtension.ext(self.item).latest = latest2 - links = self.item.get_links(version.LATEST) + links = self.item.get_links(VersionRelType.LATEST) self.assertEqual(1, len(links)) self.assertEqual(expected_href, links[0].get_href()) @@ -191,7 +206,7 @@ def test_multiple_link_setting(self): predecessor2 = make_item(year) expected_href = URL_TEMPLATE % year VersionExtension.ext(self.item).predecessor = predecessor2 - links = self.item.get_links(version.PREDECESSOR) + links = self.item.get_links(VersionRelType.PREDECESSOR) self.assertEqual(1, len(links)) self.assertEqual(expected_href, links[0].get_href()) @@ -199,7 +214,7 @@ def test_multiple_link_setting(self): successor2 = make_item(year) expected_href = URL_TEMPLATE % year VersionExtension.ext(self.item).successor = successor2 - links = self.item.get_links(version.SUCCESSOR) + links = self.item.get_links(VersionRelType.SUCCESSOR) self.assertEqual(1, len(links)) self.assertEqual(expected_href, links[0].get_href()) @@ -210,7 +225,8 @@ def make_collection(year: int) -> pystac.Collection: end = datetime.datetime(year, 1, 3, 4, 5) bboxes = [[-180.0, -90.0, 180.0, 90.0]] spatial_extent = pystac.SpatialExtent(bboxes) - temporal_extent = pystac.TemporalExtent([[start, end]]) + intervals: List[List[Optional[datetime.datetime]]] = [[start, end]] + temporal_extent = pystac.TemporalExtent(intervals) extent = pystac.Extent(spatial_extent, temporal_extent) collection = pystac.Collection(asset_id, "desc", extent) @@ -224,39 +240,39 @@ def make_collection(year: int) -> pystac.Collection: class CollectionVersionExtensionTest(unittest.TestCase): version: str = "1.2.3" - def setUp(self): + def setUp(self) -> None: super().setUp() self.collection = make_collection(2011) - def test_stac_extensions(self): + def test_stac_extensions(self) -> None: self.assertTrue(VersionExtension.has_extension(self.collection)) - def test_add_version(self): + def test_add_version(self) -> None: VersionExtension.ext(self.collection).apply(self.version) self.assertEqual(self.version, VersionExtension.ext(self.collection).version) self.assertNotIn(version.DEPRECATED, self.collection.extra_fields) self.assertFalse(VersionExtension.ext(self.collection).deprecated) self.collection.validate() - def test_version_deprecated(self): + def test_version_deprecated(self) -> None: VersionExtension.ext(self.collection).apply(self.version, deprecated=True) self.assertIn(version.VERSION, self.collection.extra_fields) self.assertIn(version.DEPRECATED, self.collection.extra_fields) self.collection.validate() - def test_add_not_deprecated_version(self): + def test_add_not_deprecated_version(self) -> None: VersionExtension.ext(self.collection).apply(self.version, deprecated=False) self.assertIn(version.DEPRECATED, self.collection.extra_fields) self.assertFalse(VersionExtension.ext(self.collection).deprecated) self.collection.validate() - def test_add_deprecated_version(self): + def test_add_deprecated_version(self) -> None: VersionExtension.ext(self.collection).apply(self.version, deprecated=True) self.assertIn(version.DEPRECATED, self.collection.extra_fields) self.assertTrue(VersionExtension.ext(self.collection).deprecated) self.collection.validate() - def test_latest(self): + def test_latest(self) -> None: year = 2013 latest = make_collection(year) VersionExtension.ext(self.collection).apply(self.version, latest=latest) @@ -264,11 +280,11 @@ def test_latest(self): self.assertIs(latest, latest_result) expected_href = URL_TEMPLATE % year - link = self.collection.get_links(version.LATEST)[0] + link = self.collection.get_links(VersionRelType.LATEST)[0] self.assertEqual(expected_href, link.get_href()) self.collection.validate() - def test_predecessor(self): + def test_predecessor(self) -> None: year = 2010 predecessor = make_collection(year) VersionExtension.ext(self.collection).apply( @@ -278,11 +294,11 @@ def test_predecessor(self): self.assertIs(predecessor, predecessor_result) expected_href = URL_TEMPLATE % year - link = self.collection.get_links(version.PREDECESSOR)[0] + link = self.collection.get_links(VersionRelType.PREDECESSOR)[0] self.assertEqual(expected_href, link.get_href()) self.collection.validate() - def test_successor(self): + def test_successor(self) -> None: year = 2012 successor = make_collection(year) VersionExtension.ext(self.collection).apply(self.version, successor=successor) @@ -290,15 +306,15 @@ def test_successor(self): self.assertIs(successor, successor_result) expected_href = URL_TEMPLATE % year - link = self.collection.get_links(version.SUCCESSOR)[0] + link = self.collection.get_links(VersionRelType.SUCCESSOR)[0] self.assertEqual(expected_href, link.get_href()) self.collection.validate() - def test_fail_validate(self): + def test_fail_validate(self) -> None: with self.assertRaises(pystac.STACValidationError): self.collection.validate() - def test_validate_all(self): + def test_validate_all(self) -> None: deprecated = True latest = make_collection(2013) predecessor = make_collection(2010) @@ -308,7 +324,7 @@ def test_validate_all(self): ) self.collection.validate() - def test_full_copy(self): + def test_full_copy(self) -> None: cat = TestCases.test_case_1() # Fetch two collections from the catalog @@ -330,19 +346,27 @@ def test_full_copy(self): # Retrieve the copied version of the items col1_copy = cat_copy.get_child("area-1-1", recursive=True) + assert col1_copy is not None col2_copy = cat_copy.get_child("area-2-2", recursive=True) + assert col2_copy is not None # Check to see if the version links point to the instances of the # col objects as they should. - predecessor = col1_copy.get_single_link(version.PREDECESSOR).target - successor = col2_copy.get_single_link(version.SUCCESSOR).target - latest = col2_copy.get_single_link(version.LATEST).target - - self.assertIs(predecessor, col2_copy) - self.assertIs(successor, col1_copy) - self.assertIs(latest, col1_copy) - - def test_setting_none_clears_link(self): + predecessor = col1_copy.get_single_link(VersionRelType.PREDECESSOR) + assert predecessor is not None + predecessor_target = predecessor.target + successor = col2_copy.get_single_link(VersionRelType.SUCCESSOR) + assert successor is not None + successor_target = successor.target + latest = col2_copy.get_single_link(VersionRelType.LATEST) + assert latest is not None + latest_target = latest.target + + self.assertIs(predecessor_target, col2_copy) + self.assertIs(successor_target, col1_copy) + self.assertIs(latest_target, col1_copy) + + def test_setting_none_clears_link(self) -> None: deprecated = False latest = make_collection(2013) predecessor = make_collection(2010) @@ -352,21 +376,21 @@ def test_setting_none_clears_link(self): ) VersionExtension.ext(self.collection).latest = None - links = self.collection.get_links(version.LATEST) + links = self.collection.get_links(VersionRelType.LATEST) self.assertEqual(0, len(links)) self.assertIsNone(VersionExtension.ext(self.collection).latest) VersionExtension.ext(self.collection).predecessor = None - links = self.collection.get_links(version.PREDECESSOR) + links = self.collection.get_links(VersionRelType.PREDECESSOR) self.assertEqual(0, len(links)) self.assertIsNone(VersionExtension.ext(self.collection).predecessor) VersionExtension.ext(self.collection).successor = None - links = self.collection.get_links(version.SUCCESSOR) + links = self.collection.get_links(VersionRelType.SUCCESSOR) self.assertEqual(0, len(links)) self.assertIsNone(VersionExtension.ext(self.collection).successor) - def test_multiple_link_setting(self): + def test_multiple_link_setting(self) -> None: deprecated = False latest1 = make_collection(2013) predecessor1 = make_collection(2010) @@ -379,7 +403,7 @@ def test_multiple_link_setting(self): latest2 = make_collection(year) expected_href = URL_TEMPLATE % year VersionExtension.ext(self.collection).latest = latest2 - links = self.collection.get_links(version.LATEST) + links = self.collection.get_links(VersionRelType.LATEST) self.assertEqual(1, len(links)) self.assertEqual(expected_href, links[0].get_href()) @@ -387,7 +411,7 @@ def test_multiple_link_setting(self): predecessor2 = make_collection(year) expected_href = URL_TEMPLATE % year VersionExtension.ext(self.collection).predecessor = predecessor2 - links = self.collection.get_links(version.PREDECESSOR) + links = self.collection.get_links(VersionRelType.PREDECESSOR) self.assertEqual(1, len(links)) self.assertEqual(expected_href, links[0].get_href()) @@ -395,7 +419,7 @@ def test_multiple_link_setting(self): successor2 = make_collection(year) expected_href = URL_TEMPLATE % year VersionExtension.ext(self.collection).successor = successor2 - links = self.collection.get_links(version.SUCCESSOR) + links = self.collection.get_links(VersionRelType.SUCCESSOR) self.assertEqual(1, len(links)) self.assertEqual(expected_href, links[0].get_href()) diff --git a/tests/extensions/test_view.py b/tests/extensions/test_view.py index ee96a7107..a4956bb60 100644 --- a/tests/extensions/test_view.py +++ b/tests/extensions/test_view.py @@ -7,16 +7,16 @@ class ViewTest(unittest.TestCase): - def setUp(self): + def setUp(self) -> None: self.maxDiff = None self.example_uri = TestCases.get_path("data-files/view/example-landsat8.json") - def test_to_from_dict(self): + def test_to_from_dict(self) -> None: with open(self.example_uri) as f: d = json.load(f) test_to_from_dict(self, pystac.Item, d) - def test_apply(self): + def test_apply(self) -> None: item = next(iter(TestCases.test_case_2().get_all_items())) self.assertFalse(ViewExtension.has_extension(item)) @@ -35,17 +35,18 @@ def test_apply(self): self.assertEqual(ViewExtension.ext(item).sun_azimuth, 4.0) self.assertEqual(ViewExtension.ext(item).sun_elevation, 5.0) - def test_validate_view(self): + def test_validate_view(self) -> None: item = pystac.Item.from_file(self.example_uri) self.assertTrue(ViewExtension.has_extension(item)) item.validate() - def test_off_nadir(self): + def test_off_nadir(self) -> None: view_item = pystac.Item.from_file(self.example_uri) # Get self.assertIn("view:off_nadir", view_item.properties) view_off_nadir = ViewExtension.ext(view_item).off_nadir + assert view_off_nadir is not None self.assertEqual(view_off_nadir, view_item.properties["view:off_nadir"]) # Set @@ -73,12 +74,13 @@ def test_off_nadir(self): # Validate view_item.validate() - def test_incidence_angle(self): + def test_incidence_angle(self) -> None: view_item = pystac.Item.from_file(self.example_uri) # Get self.assertIn("view:incidence_angle", view_item.properties) view_incidence_angle = ViewExtension.ext(view_item).incidence_angle + assert view_incidence_angle is not None self.assertEqual( view_incidence_angle, view_item.properties["view:incidence_angle"] ) @@ -110,12 +112,13 @@ def test_incidence_angle(self): # Validate view_item.validate() - def test_azimuth(self): + def test_azimuth(self) -> None: view_item = pystac.Item.from_file(self.example_uri) # Get self.assertIn("view:azimuth", view_item.properties) view_azimuth = ViewExtension.ext(view_item).azimuth + assert view_azimuth is not None self.assertEqual(view_azimuth, view_item.properties["view:azimuth"]) # Set @@ -143,12 +146,13 @@ def test_azimuth(self): # Validate view_item.validate() - def test_sun_azimuth(self): + def test_sun_azimuth(self) -> None: view_item = pystac.Item.from_file(self.example_uri) # Get self.assertIn("view:sun_azimuth", view_item.properties) view_sun_azimuth = ViewExtension.ext(view_item).sun_azimuth + assert view_sun_azimuth is not None self.assertEqual(view_sun_azimuth, view_item.properties["view:sun_azimuth"]) # Set @@ -178,12 +182,13 @@ def test_sun_azimuth(self): # Validate view_item.validate() - def test_sun_elevation(self): + def test_sun_elevation(self) -> None: view_item = pystac.Item.from_file(self.example_uri) # Get self.assertIn("view:sun_elevation", view_item.properties) view_sun_elevation = ViewExtension.ext(view_item).sun_elevation + assert view_sun_elevation is not None self.assertEqual(view_sun_elevation, view_item.properties["view:sun_elevation"]) # Set diff --git a/tests/serialization/test_identify.py b/tests/serialization/test_identify.py index 97c1e6436..829f16cc7 100644 --- a/tests/serialization/test_identify.py +++ b/tests/serialization/test_identify.py @@ -14,10 +14,10 @@ class IdentifyTest(unittest.TestCase): - def setUp(self): + def setUp(self) -> None: self.examples = TestCases.get_examples_info() - def test_identify(self): + def test_identify(self) -> None: collection_cache = CollectionCache() for example in self.examples: with self.subTest(example.path): @@ -49,7 +49,7 @@ def test_identify(self): class VersionTest(unittest.TestCase): - def test_version_ordering(self): + def test_version_ordering(self) -> None: self.assertEqual(STACVersionID("0.9.0"), STACVersionID("0.9.0")) self.assertFalse(STACVersionID("0.9.0") < STACVersionID("0.9.0")) self.assertFalse(STACVersionID("0.9.0") != STACVersionID("0.9.0")) @@ -59,12 +59,12 @@ def test_version_ordering(self): self.assertFalse(STACVersionID("0.9.0") > "0.9.0") # type:ignore self.assertTrue(STACVersionID("0.9.0") <= "0.9.0") # type:ignore self.assertTrue( - STACVersionID("1.0.0-beta.1") + STACVersionID("1.0.0-beta.1") # type:ignore <= STACVersionID("1.0.0-beta.2") # type:ignore ) self.assertFalse(STACVersionID("1.0.0") < STACVersionID("1.0.0-beta.2")) - def test_version_range_ordering(self): + def test_version_range_ordering(self) -> None: version_range = STACVersionRange("0.9.0", "1.0.0-beta.2") self.assertIsInstance(str(version_range), str) self.assertTrue(version_range.contains("1.0.0-beta.1")) diff --git a/tests/serialization/test_migrate.py b/tests/serialization/test_migrate.py index c4c9ed6f9..98ba4b8fa 100644 --- a/tests/serialization/test_migrate.py +++ b/tests/serialization/test_migrate.py @@ -16,10 +16,10 @@ class MigrateTest(unittest.TestCase): - def setUp(self): + def setUp(self) -> None: self.examples = [e for e in TestCases.get_examples_info()] - def test_migrate(self): + def test_migrate(self) -> None: collection_cache = CollectionCache() for example in self.examples: with self.subTest(example.path): @@ -55,7 +55,7 @@ def test_migrate(self): pystac.read_dict(migrated_d, href=path), pystac.STACObject ) - def test_migrates_removed_extension(self): + def test_migrates_removed_extension(self) -> None: item = pystac.Item.from_file( TestCases.get_path( "data-files/examples/0.7.0/extensions/sar/" "examples/sentinel1.json" @@ -67,7 +67,7 @@ def test_migrates_removed_extension(self): str_to_datetime("2018-11-03T23:58:55.121559Z"), ) - def test_migrates_added_extension(self): + def test_migrates_added_extension(self) -> None: item = pystac.Item.from_file( TestCases.get_path( "data-files/examples/0.8.1/item-spec/" "examples/planet-sample.json" @@ -79,7 +79,7 @@ def test_migrates_added_extension(self): self.assertEqual(view_ext.sun_elevation, 58.8) self.assertEqual(view_ext.off_nadir, 1) - def test_migrates_renamed_extension(self): + def test_migrates_renamed_extension(self) -> None: collection = pystac.Collection.from_file( TestCases.get_path( "data-files/examples/0.9.0/extensions/asset/" diff --git a/tests/test_cache.py b/tests/test_cache.py index 1fcc75965..70e0bc886 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -20,7 +20,7 @@ def create_catalog(suffix: Any, include_href: bool = True) -> pystac.Catalog: class ResolvedObjectCacheTest(unittest.TestCase): - def tests_get_or_cache_returns_previously_cached_href(self): + def tests_get_or_cache_returns_previously_cached_href(self) -> None: cache = ResolvedObjectCache() cat = create_catalog(1) cache_result_1 = cache.get_or_cache(cat) @@ -30,7 +30,7 @@ def tests_get_or_cache_returns_previously_cached_href(self): cache_result_2 = cache.get_or_cache(identical_cat) self.assertIs(cache_result_2, cat) - def test_get_or_cache_returns_previously_cached_id(self): + def test_get_or_cache_returns_previously_cached_id(self) -> None: cache = ResolvedObjectCache() cat = create_catalog(1, include_href=False) cache_result_1 = cache.get_or_cache(cat) @@ -42,7 +42,7 @@ def test_get_or_cache_returns_previously_cached_id(self): class ResolvedObjectCollectionCacheTest(unittest.TestCase): - def test_merge(self): + def test_merge(self) -> None: cat1 = create_catalog(1, include_href=False) cat2 = create_catalog(2) cat3 = create_catalog(3, include_href=False) @@ -79,7 +79,7 @@ def test_merge(self): ) self.assertIs(merged.get_by_href(get_opt(cat2.get_self_href())), cat2) - def test_cache(self): + def test_cache(self) -> None: cache = ResolvedObjectCache().as_collection_cache() collection = TestCases.test_case_8() collection_json = collection.to_dict() diff --git a/tests/test_catalog.py b/tests/test_catalog.py index 2222f82b6..503bf7adf 100644 --- a/tests/test_catalog.py +++ b/tests/test_catalog.py @@ -22,7 +22,7 @@ class CatalogTypeTest(unittest.TestCase): - def test_determine_type_for_absolute_published(self): + def test_determine_type_for_absolute_published(self) -> None: cat = TestCases.test_case_1() with TemporaryDirectory(dir=os.getcwd()) as tmp_dir: cat.normalize_and_save(tmp_dir, catalog_type=CatalogType.ABSOLUTE_PUBLISHED) @@ -33,7 +33,7 @@ def test_determine_type_for_absolute_published(self): catalog_type = CatalogType.determine_type(cat_json) self.assertEqual(catalog_type, CatalogType.ABSOLUTE_PUBLISHED) - def test_determine_type_for_relative_published(self): + def test_determine_type_for_relative_published(self) -> None: cat = TestCases.test_case_2() with TemporaryDirectory(dir=os.getcwd()) as tmp_dir: cat.normalize_and_save(tmp_dir, catalog_type=CatalogType.RELATIVE_PUBLISHED) @@ -44,14 +44,14 @@ def test_determine_type_for_relative_published(self): catalog_type = CatalogType.determine_type(cat_json) self.assertEqual(catalog_type, CatalogType.RELATIVE_PUBLISHED) - def test_determine_type_for_self_contained(self): + def test_determine_type_for_self_contained(self) -> None: cat_json = pystac.StacIO.default().read_json( TestCases.get_path("data-files/catalogs/test-case-1/catalog.json") ) catalog_type = CatalogType.determine_type(cat_json) self.assertEqual(catalog_type, CatalogType.SELF_CONTAINED) - def test_determine_type_for_unknown(self): + def test_determine_type_for_unknown(self) -> None: catalog = Catalog(id="test", description="test desc") subcat = Catalog(id="subcat", description="subcat desc") catalog.add_child(subcat) @@ -80,7 +80,7 @@ def test_create_and_read(self) -> None: self.assertEqual(len(list(items)), 8) - def test_read_remote(self): + def test_read_remote(self) -> None: # TODO: Move this URL to the main stac-spec repo once the example JSON is fixed. catalog_url = ( "https://raw.githubusercontent.com/lossyrob/stac-spec/" @@ -90,10 +90,11 @@ def test_read_remote(self): cat = Catalog.from_file(catalog_url) zanzibar = cat.get_child("zanzibar-collection") + assert zanzibar is not None self.assertEqual(len(list(zanzibar.get_items())), 2) - def test_clear_items_removes_from_cache(self): + def test_clear_items_removes_from_cache(self) -> None: catalog = Catalog(id="test", description="test") subcat = Catalog(id="subcat", description="test") catalog.add_child(subcat) @@ -138,7 +139,7 @@ def test_clear_items_removes_from_cache(self): self.assertEqual(len(items), 1) self.assertEqual(items[0].properties["key"], "three") - def test_clear_children_removes_from_cache(self): + def test_clear_children_removes_from_cache(self) -> None: catalog = Catalog(id="test", description="test") subcat = Catalog(id="subcat", description="test") catalog.add_child(subcat) @@ -163,7 +164,7 @@ def test_clear_children_removes_from_cache(self): self.assertEqual(len(children), 1) self.assertEqual(children[0].description, "test3") - def test_clear_children_sets_parent_and_root_to_None(self): + def test_clear_children_sets_parent_and_root_to_None(self) -> None: catalog = Catalog(id="test", description="test") subcat1 = Catalog(id="subcat", description="test") subcat2 = Catalog(id="subcat2", description="test2") @@ -184,35 +185,35 @@ def test_clear_children_sets_parent_and_root_to_None(self): self.assertIsNone(subcat1.get_root()) self.assertIsNone(subcat2.get_root()) - def test_add_child_throws_if_item(self): + def test_add_child_throws_if_item(self) -> None: cat = TestCases.test_case_1() item = next(iter(cat.get_all_items())) with self.assertRaises(pystac.STACError): cat.add_child(item) # type:ignore - def test_add_item_throws_if_child(self): + def test_add_item_throws_if_child(self) -> None: cat = TestCases.test_case_1() child = next(iter(cat.get_children())) with self.assertRaises(pystac.STACError): cat.add_item(child) # type:ignore - def test_get_child_returns_none_if_not_found(self): + def test_get_child_returns_none_if_not_found(self) -> None: cat = TestCases.test_case_1() child = cat.get_child("thisshouldnotbeachildid", recursive=True) self.assertIsNone(child) - def test_get_item_returns_none_if_not_found(self): + def test_get_item_returns_none_if_not_found(self) -> None: cat = TestCases.test_case_1() item = cat.get_item("thisshouldnotbeanitemid", recursive=True) self.assertIsNone(item) - def test_sets_catalog_type(self): + def test_sets_catalog_type(self) -> None: cat = TestCases.test_case_1() self.assertEqual(cat.catalog_type, CatalogType.SELF_CONTAINED) - def test_walk_iterates_correctly(self): - def test_catalog(cat: Catalog): + def test_walk_iterates_correctly(self) -> None: + def test_catalog(cat: Catalog) -> None: expected_catalog_iterations = 1 actual_catalog_iterations = 0 with self.subTest(title="Testing catalog {}".format(cat.id)): @@ -236,7 +237,7 @@ def test_catalog(cat: Catalog): for cat in TestCases.all_test_catalogs(): test_catalog(cat) - def test_clone_generates_correct_links(self): + def test_clone_generates_correct_links(self) -> None: catalogs = TestCases.all_test_catalogs() for catalog in catalogs: @@ -279,7 +280,7 @@ def test_clone_generates_correct_links(self): ), ) - def test_save_uses_previous_catalog_type(self): + def test_save_uses_previous_catalog_type(self) -> None: catalog = TestCases.test_case_1() assert catalog.catalog_type == CatalogType.SELF_CONTAINED with TemporaryDirectory(dir=os.getcwd()) as tmp_dir: @@ -290,17 +291,19 @@ def test_save_uses_previous_catalog_type(self): cat2 = pystac.Catalog.from_file(href) self.assertEqual(cat2.catalog_type, CatalogType.SELF_CONTAINED) - def test_clone_uses_previous_catalog_type(self): + def test_clone_uses_previous_catalog_type(self) -> None: catalog = TestCases.test_case_1() assert catalog.catalog_type == CatalogType.SELF_CONTAINED clone = catalog.clone() self.assertEqual(clone.catalog_type, CatalogType.SELF_CONTAINED) - def test_normalize_hrefs_sets_all_hrefs(self): + def test_normalize_hrefs_sets_all_hrefs(self) -> None: catalog = TestCases.test_case_1() catalog.normalize_hrefs("http://example.com") for root, _, items in catalog.walk(): - self.assertTrue(root.get_self_href().startswith("http://example.com")) + self_href = root.get_self_href() + assert self_href is not None + self.assertTrue(self_href.startswith("http://example.com")) for link in root.links: if link.is_resolved(): target_href = cast(pystac.STACObject, link.target).self_href @@ -315,13 +318,15 @@ def test_normalize_hrefs_sets_all_hrefs(self): for item in items: self.assertIn("http://example.com", item.self_href) - def test_normalize_hrefs_makes_absolute_href(self): + def test_normalize_hrefs_makes_absolute_href(self) -> None: catalog = TestCases.test_case_1() catalog.normalize_hrefs("./relativepath") abspath = os.path.abspath("./relativepath") - self.assertTrue(catalog.get_self_href().startswith(abspath)) + self_href = catalog.get_self_href() + assert self_href is not None + self.assertTrue(self_href.startswith(abspath)) - def test_normalize_href_works_with_label_source_links(self): + def test_normalize_href_works_with_label_source_links(self) -> None: catalog = TestCases.test_case_1() catalog.normalize_hrefs("http://example.com") item = catalog.get_item("area-1-1-labels", recursive=True) @@ -333,7 +338,7 @@ def test_normalize_href_works_with_label_source_links(self): "area-1-1-imagery/area-1-1-imagery.json", ) - def test_generate_subcatalogs_works_with_custom_properties(self): + def test_generate_subcatalogs_works_with_custom_properties(self) -> None: catalog = TestCases.test_case_8() defaults = {"pl:item_type": "PlanetScope"} catalog.generate_subcatalogs( @@ -341,11 +346,12 @@ def test_generate_subcatalogs_works_with_custom_properties(self): ) month_cat = catalog.get_child("8", recursive=True) + assert month_cat is not None type_cats = set([cat.id for cat in month_cat.get_children()]) self.assertEqual(type_cats, set(["PSScene4Band", "SkySatScene", "PlanetScope"])) - def test_generate_subcatalogs_does_not_change_item_count(self): + def test_generate_subcatalogs_does_not_change_item_count(self) -> None: catalog = TestCases.test_case_7() item_counts = { @@ -366,7 +372,7 @@ def test_generate_subcatalogs_does_not_change_item_count(self): actual, expected, msg=" for child '{}'".format(child.id) ) - def test_generate_subcatalogs_can_be_applied_multiple_times(self): + def test_generate_subcatalogs_can_be_applied_multiple_times(self) -> None: catalog = TestCases.test_case_8() _ = catalog.generate_subcatalogs("${year}/${month}") @@ -385,7 +391,7 @@ def test_generate_subcatalogs_can_be_applied_multiple_times(self): msg=" for item '{}'".format(item.id), ) - def test_generate_subcatalogs_works_after_adding_more_items(self): + def test_generate_subcatalogs_works_after_adding_more_items(self) -> None: catalog = Catalog(id="test", description="Test") properties = dict(property1="A", property2=1) catalog.add_item( @@ -410,11 +416,17 @@ def test_generate_subcatalogs_works_after_adding_more_items(self): catalog.generate_subcatalogs("${property1}/${property2}") catalog.normalize_hrefs("/tmp") - item1_parent = catalog.get_item("item1", recursive=True).get_parent() - item2_parent = catalog.get_item("item2", recursive=True).get_parent() + item1 = catalog.get_item("item1", recursive=True) + assert item1 is not None + item1_parent = item1.get_parent() + assert item1_parent is not None + item2 = catalog.get_item("item2", recursive=True) + assert item2 is not None + item2_parent = item2.get_parent() + assert item2_parent is not None self.assertEqual(item1_parent.get_self_href(), item2_parent.get_self_href()) - def test_generate_subcatalogs_works_for_branched_subcatalogs(self): + def test_generate_subcatalogs_works_for_branched_subcatalogs(self) -> None: catalog = Catalog(id="test", description="Test") item_properties = [ dict(property1="A", property2=1, property3="i"), # add 3 subcats @@ -439,7 +451,7 @@ def test_generate_subcatalogs_works_for_branched_subcatalogs(self): expected_subcats = {"A", "B", "1", "2", "i", "j"} self.assertSetEqual(actual_subcats, expected_subcats) - def test_generate_subcatalogs_works_for_subcatalogs_with_same_ids(self): + def test_generate_subcatalogs_works_for_subcatalogs_with_same_ids(self) -> None: catalog = Catalog(id="test", description="Test") item_properties = [ dict(property1=1, property2=1), # add 2 subcats @@ -465,12 +477,14 @@ def test_generate_subcatalogs_works_for_subcatalogs_with_same_ids(self): catalog.normalize_hrefs("/") for item in catalog.get_all_items(): - parent_href = item.get_parent().self_href + item_parent = item.get_parent() + assert item_parent is not None + parent_href = item_parent.self_href path_to_parent, _ = os.path.split(parent_href) subcats = [el for el in path_to_parent.split(os.sep) if el] self.assertEqual(len(subcats), 2, msg=" for item '{}'".format(item.id)) - def test_map_items(self): + def test_map_items(self) -> None: def item_mapper(item: pystac.Item) -> pystac.Item: item.properties["ITEM_MAPPER"] = "YEP" return item @@ -491,7 +505,7 @@ def item_mapper(item: pystac.Item) -> pystac.Item: for item in catalog.get_all_items(): self.assertFalse("ITEM_MAPPER" in item.properties) - def test_map_items_multiple(self): + def test_map_items_multiple(self) -> None: def item_mapper(item: pystac.Item) -> List[pystac.Item]: item2 = item.clone() item2.id = item2.id + "_2" @@ -533,7 +547,7 @@ def item_mapper(item: pystac.Item) -> List[pystac.Item]: or ("ITEM_MAPPER_2" in item.properties) ) - def test_map_items_multiple_2(self): + def test_map_items_multiple_2(self) -> None: catalog = Catalog(id="test-1", description="Test1") item1 = Item( id="item1", @@ -595,7 +609,7 @@ def create_label_item(item: pystac.Item) -> List[pystac.Item]: items = new_catalog.get_all_items() self.assertTrue(len(list(items)) == 4) - def test_map_assets_single(self): + def test_map_assets_single(self) -> None: changed_asset = "d43bead8-e3f8-4c51-95d6-e24e750a402b" def asset_mapper(key: str, asset: pystac.Asset) -> pystac.Asset: @@ -624,7 +638,7 @@ def asset_mapper(key: str, asset: pystac.Asset) -> pystac.Asset: self.assertNotEqual(asset.title, "NEW TITLE") self.assertTrue(found) - def test_map_assets_tup(self): + def test_map_assets_tup(self) -> None: changed_assets: List[str] = [] def asset_mapper( @@ -661,7 +675,7 @@ def asset_mapper( self.assertTrue(found) self.assertTrue(not_found) - def test_map_assets_multi(self): + def test_map_assets_multi(self) -> None: changed_assets = [] def asset_mapper( @@ -706,17 +720,19 @@ def asset_mapper( self.assertTrue(found2) self.assertTrue(not_found) - def test_make_all_asset_hrefs_absolute(self): + def test_make_all_asset_hrefs_absolute(self) -> None: cat = TestCases.test_case_2() cat.make_all_asset_hrefs_absolute() item = cat.get_item("cf73ec1a-d790-4b59-b077-e101738571ed", recursive=True) + assert item is not None href = item.assets["cf73ec1a-d790-4b59-b077-e101738571ed"].href self.assertTrue(is_absolute_href(href)) - def test_make_all_asset_hrefs_relative(self): + def test_make_all_asset_hrefs_relative(self) -> None: cat = TestCases.test_case_2() item = cat.get_item("cf73ec1a-d790-4b59-b077-e101738571ed", recursive=True) + assert item is not None asset = item.assets["cf73ec1a-d790-4b59-b077-e101738571ed"] original_href = asset.href cat.make_all_asset_hrefs_absolute() @@ -728,8 +744,8 @@ def test_make_all_asset_hrefs_relative(self): self.assertFalse(is_absolute_href(asset.href)) self.assertEqual(asset.href, original_href) - def test_make_all_links_relative_or_absolute(self): - def check_all_relative(cat: Catalog): + def test_make_all_links_relative_or_absolute(self) -> None: + def check_all_relative(cat: Catalog) -> None: for root, catalogs, items in cat.walk(): for link in root.links: if link.rel in HIERARCHICAL_LINKS: @@ -739,7 +755,7 @@ def check_all_relative(cat: Catalog): if link.rel in HIERARCHICAL_LINKS: self.assertFalse(is_absolute_href(link.href)) - def check_all_absolute(cat: Catalog): + def check_all_absolute(cat: Catalog) -> None: for root, catalogs, items in cat.walk(): for link in root.links: self.assertTrue(is_absolute_href(link.href)) @@ -758,7 +774,7 @@ def check_all_absolute(cat: Catalog): c2.catalog_type = CatalogType.ABSOLUTE_PUBLISHED check_all_absolute(c2) - def test_full_copy_and_normalize_works_with_created_stac(self): + def test_full_copy_and_normalize_works_with_created_stac(self) -> None: cat = TestCases.test_case_3() cat_copy = cat.full_copy() cat_copy.normalize_hrefs("http://example.com") @@ -771,7 +787,7 @@ def test_full_copy_and_normalize_works_with_created_stac(self): if link.rel != "self": self.assertIsNot(link.get_href(), None) - def test_extra_fields(self): + def test_extra_fields(self) -> None: catalog = TestCases.test_case_1() catalog.extra_fields["type"] = "FeatureCollection" @@ -788,7 +804,7 @@ def test_extra_fields(self): self.assertTrue("type" in read_cat.extra_fields) self.assertEqual(read_cat.extra_fields["type"], "FeatureCollection") - def test_validate_all(self): + def test_validate_all(self) -> None: for cat in TestCases.all_test_catalogs(): with self.subTest(cat.id): # If hrefs are not set, it will fail validation. @@ -799,6 +815,7 @@ def test_validate_all(self): # Make one invalid, write it off, read it in, ensure it throws cat = TestCases.test_case_1() item = cat.get_item("area-1-1-labels", recursive=True) + assert item is not None item.geometry = {"type": "INVALID", "coordinates": "NONE"} with TemporaryDirectory(dir=os.getcwd()) as tmp_dir: cat.normalize_hrefs(tmp_dir) @@ -809,13 +826,14 @@ def test_validate_all(self): with self.assertRaises(pystac.STACValidationError): cat2.validate_all() - def test_set_hrefs_manually(self): + def test_set_hrefs_manually(self) -> None: catalog = TestCases.test_case_1() # Modify the datetimes year = 2004 month = 2 for item in catalog.get_all_items(): + assert item.datetime is not None item.datetime = item.datetime.replace(year=year, month=month) year += 1 month += 1 @@ -835,6 +853,7 @@ def test_set_hrefs_manually(self): # Set each item's HREF based on it's datetime for item in items: + assert item.datetime is not None item_href = "{}/{}-{}/{}.json".format( root_dir, item.datetime.year, item.datetime.month, item.id ) @@ -870,7 +889,7 @@ def test_set_hrefs_manually(self): msg="{} does not end with {}".format(self_href, end), ) - def test_collections_cache_correctly(self): + def test_collections_cache_correctly(self) -> None: catalogs = TestCases.all_test_catalogs() mock_io = MockStacIO() for cat in catalogs: @@ -899,7 +918,7 @@ def test_collections_cache_correctly(self): ), ) - def test_reading_iterating_and_writing_works_as_expected(self): + def test_reading_iterating_and_writing_works_as_expected(self) -> None: """Test case to cover issue #88""" stac_uri = "tests/data-files/catalogs/test-case-6/catalog.json" cat = Catalog.from_file(stac_uri) @@ -921,11 +940,11 @@ def test_reading_iterating_and_writing_works_as_expected(self): # Iterate again over the items. This would fail in #88 pass - def test_get_children_cbers(self): + def test_get_children_cbers(self) -> None: cat = TestCases.test_case_6() self.assertEqual(len(list(cat.get_children())), 4) - def test_resolve_planet(self): + def test_resolve_planet(self) -> None: """Test against a bug that caused infinite recursion during link resolution""" cat = TestCases.test_case_8() for root, _, items in cat.walk(): @@ -933,7 +952,7 @@ def test_resolve_planet(self): item.resolve_links() root.resolve_links() - def test_handles_children_with_same_id(self): + def test_handles_children_with_same_id(self) -> None: # This catalog has the root and child collection share an ID. cat = pystac.Catalog.from_file( TestCases.get_path("data-files/invalid/shared-id/catalog.json") @@ -942,7 +961,7 @@ def test_handles_children_with_same_id(self): self.assertEqual(len(items), 1) - def test_catalog_with_href_caches_by_href(self): + def test_catalog_with_href_caches_by_href(self) -> None: cat = TestCases.test_case_1() cache = cat._resolved_objects @@ -952,7 +971,7 @@ def test_catalog_with_href_caches_by_href(self): class FullCopyTest(unittest.TestCase): - def check_link(self, link: pystac.Link, tag: str): + def check_link(self, link: pystac.Link, tag: str) -> None: if link.is_resolved(): target_href: str = cast(pystac.STACObject, link.target).self_href else: @@ -962,11 +981,11 @@ def check_link(self, link: pystac.Link, tag: str): '[{}] {} does not contain "{}"'.format(link.rel, target_href, tag), ) - def check_item(self, item: Item, tag: str): + def check_item(self, item: Item, tag: str) -> None: for link in item.links: self.check_link(link, tag) - def check_catalog(self, c: Catalog, tag: str): + def check_catalog(self, c: Catalog, tag: str) -> None: self.assertEqual(len(c.get_links("root")), 1) for link in c.links: @@ -1076,6 +1095,7 @@ def test_full_copy_4(self) -> None: # Check that the relative asset link was saved correctly in the copy. item = cat2.get_item("cf73ec1a-d790-4b59-b077-e101738571ed", recursive=True) + assert item is not None href = item.assets[ "cf73ec1a-d790-4b59-b077-e101738571ed" diff --git a/tests/test_collection.py b/tests/test_collection.py index b87a86b98..44fce57cc 100644 --- a/tests/test_collection.py +++ b/tests/test_collection.py @@ -16,7 +16,7 @@ class CollectionTest(unittest.TestCase): - def test_spatial_extent_from_coordinates(self): + def test_spatial_extent_from_coordinates(self) -> None: extent = SpatialExtent.from_coordinates(ARBITRARY_GEOM["coordinates"]) self.assertEqual(len(extent.bboxes), 1) @@ -25,13 +25,13 @@ def test_spatial_extent_from_coordinates(self): for x in bbox: self.assertTrue(type(x) is float) - def test_read_eo_items_are_heritable(self): + def test_read_eo_items_are_heritable(self) -> None: cat = TestCases.test_case_5() item = next(iter(cat.get_all_items())) self.assertTrue(EOExtension.has_extension(item)) - def test_save_uses_previous_catalog_type(self): + def test_save_uses_previous_catalog_type(self) -> None: collection = TestCases.test_case_8() assert collection.STAC_OBJECT_TYPE == pystac.STACObjectType.COLLECTION self.assertEqual(collection.catalog_type, CatalogType.SELF_CONTAINED) @@ -43,15 +43,18 @@ def test_save_uses_previous_catalog_type(self): collection2 = pystac.Collection.from_file(href) self.assertEqual(collection2.catalog_type, CatalogType.SELF_CONTAINED) - def test_clone_uses_previous_catalog_type(self): + def test_clone_uses_previous_catalog_type(self) -> None: catalog = TestCases.test_case_8() assert catalog.catalog_type == CatalogType.SELF_CONTAINED clone = catalog.clone() self.assertEqual(clone.catalog_type, CatalogType.SELF_CONTAINED) - def test_multiple_extents(self): + def test_multiple_extents(self) -> None: cat1 = TestCases.test_case_1() - col1 = cat1.get_child("country-1").get_child("area-1-1") + country = cat1.get_child("country-1") + assert country is not None + col1 = country.get_child("area-1-1") + assert col1 is not None col1.validate() self.assertIsInstance(col1, Collection) validate_dict(col1.to_dict(), pystac.STACObjectType.COLLECTION) @@ -74,9 +77,10 @@ def test_multiple_extents(self): cloned_ext = ext.clone() self.assertDictEqual(cloned_ext.to_dict(), multi_ext_dict["extent"]) - def test_extra_fields(self): + def test_extra_fields(self) -> None: catalog = TestCases.test_case_2() collection = catalog.get_child("1a8c1632-fa91-4a62-b33e-3a87c2ebdf16") + assert collection is not None collection.extra_fields["test"] = "extra" @@ -92,7 +96,7 @@ def test_extra_fields(self): self.assertTrue("test" in read_col.extra_fields) self.assertEqual(read_col.extra_fields["test"], "extra") - def test_update_extents(self): + def test_update_extents(self) -> None: catalog = TestCases.test_case_2() base_collection = catalog.get_child("1a8c1632-fa91-4a62-b33e-3a87c2ebdf16") @@ -149,7 +153,7 @@ def test_update_extents(self): collection.extent.temporal.intervals, ) - def test_supplying_href_in_init_does_not_fail(self): + def test_supplying_href_in_init_does_not_fail(self) -> None: test_href = "http://example.com/collection.json" spatial_extent = SpatialExtent(bboxes=[ARBITRARY_BBOX]) temporal_extent = TemporalExtent(intervals=[[TEST_DATETIME, None]]) @@ -161,7 +165,7 @@ def test_supplying_href_in_init_does_not_fail(self): self.assertEqual(collection.get_self_href(), test_href) - def test_collection_with_href_caches_by_href(self): + def test_collection_with_href_caches_by_href(self) -> None: collection = pystac.Collection.from_file( TestCases.get_path("data-files/examples/hand-0.8.1/collection.json") ) @@ -173,7 +177,7 @@ def test_collection_with_href_caches_by_href(self): class ExtentTest(unittest.TestCase): - def test_spatial_allows_single_bbox(self): + def test_spatial_allows_single_bbox(self) -> None: temporal_extent = TemporalExtent(intervals=[[TEST_DATETIME, None]]) # Pass in a single BBOX @@ -190,7 +194,7 @@ def test_spatial_allows_single_bbox(self): collection.validate() - def test_from_items(self): + def test_from_items(self) -> None: item1 = Item( id="test-item-1", geometry=ARBITRARY_GEOM, diff --git a/tests/test_item.py b/tests/test_item.py index 0a1221289..3f42190d2 100644 --- a/tests/test_item.py +++ b/tests/test_item.py @@ -15,13 +15,13 @@ class ItemTest(unittest.TestCase): - def get_example_item_dict(self): + def get_example_item_dict(self) -> Dict[str, Any]: m = TestCases.get_path("data-files/item/sample-item.json") with open(m) as f: item_dict = json.load(f) return item_dict - def test_to_from_dict(self): + def test_to_from_dict(self) -> None: self.maxDiff = None item_dict = self.get_example_item_dict() @@ -37,7 +37,7 @@ def test_to_from_dict(self): ) self.assertEqual(len(item.assets["thumbnail"].properties), 0) - def test_set_self_href_does_not_break_asset_hrefs(self): + def test_set_self_href_does_not_break_asset_hrefs(self) -> None: cat = TestCases.test_case_2() for item in cat.get_all_items(): for asset in item.assets.values(): @@ -47,7 +47,7 @@ def test_set_self_href_does_not_break_asset_hrefs(self): for asset in item.assets.values(): self.assertTrue(is_absolute_href(asset.href)) - def test_set_self_href_none_ignores_relative_asset_hrefs(self): + def test_set_self_href_none_ignores_relative_asset_hrefs(self) -> None: cat = TestCases.test_case_2() for item in cat.get_all_items(): for asset in item.assets.values(): @@ -57,7 +57,7 @@ def test_set_self_href_none_ignores_relative_asset_hrefs(self): for asset in item.assets.values(): self.assertFalse(is_absolute_href(asset.href)) - def test_asset_absolute_href(self): + def test_asset_absolute_href(self) -> None: item_dict = self.get_example_item_dict() item = Item.from_dict(item_dict) rel_asset = Asset("./data.geojson") @@ -66,7 +66,7 @@ def test_asset_absolute_href(self): actual_href = rel_asset.get_absolute_href() self.assertEqual(expected_href, actual_href) - def test_extra_fields(self): + def test_extra_fields(self) -> None: item = pystac.Item.from_file( TestCases.get_path("data-files/item/sample-item.json") ) @@ -85,7 +85,7 @@ def test_extra_fields(self): self.assertTrue("test" in read_item.extra_fields) self.assertEqual(read_item.extra_fields["test"], "extra") - def test_clearing_collection(self): + def test_clearing_collection(self) -> None: collection = TestCases.test_case_4().get_child("acc") assert isinstance(collection, pystac.Collection) item = next(iter(collection.get_all_items())) @@ -97,7 +97,7 @@ def test_clearing_collection(self): self.assertEqual(item.collection_id, collection.id) self.assertIs(item.get_collection(), collection) - def test_datetime_ISO8601_format(self): + def test_datetime_ISO8601_format(self) -> None: item_dict = self.get_example_item_dict() item = Item.from_dict(item_dict) @@ -106,7 +106,7 @@ def test_datetime_ISO8601_format(self): self.assertEqual("2016-05-03T13:22:30.040000Z", formatted_time) - def test_null_datetime(self): + def test_null_datetime(self) -> None: item = pystac.Item.from_file( TestCases.get_path("data-files/item/sample-item.json") ) @@ -133,7 +133,7 @@ def test_null_datetime(self): null_dt_item.validate() - def test_get_set_asset_datetime(self): + def test_get_set_asset_datetime(self) -> None: item = pystac.Item.from_file( TestCases.get_path("data-files/item/sample-item-asset-properties.json") ) @@ -158,7 +158,7 @@ def test_get_set_asset_datetime(self): str_to_datetime("2018-05-03T13:22:30.040Z"), ) - def test_read_eo_item_owns_asset(self): + def test_read_eo_item_owns_asset(self) -> None: item = next( x for x in TestCases.test_case_1().get_all_items() if isinstance(x, Item) ) @@ -166,7 +166,7 @@ def test_read_eo_item_owns_asset(self): for asset_key in item.assets: self.assertEqual(item.assets[asset_key].owner, item) - def test_null_geometry(self): + def test_null_geometry(self) -> None: m = TestCases.get_path( "data-files/examples/1.0.0-beta.2/item-spec/examples/null-geom-item.json" ) @@ -184,7 +184,7 @@ def test_null_geometry(self): with self.assertRaises(KeyError): item_dict["bbox"] - def test_0_9_item_with_no_extensions_does_not_read_collection_data(self): + def test_0_9_item_with_no_extensions_does_not_read_collection_data(self) -> None: item_json = pystac.StacIO.default().read_json( TestCases.get_path("data-files/examples/hand-0.9.0/010100/010100.json") ) @@ -196,7 +196,7 @@ def test_0_9_item_with_no_extensions_does_not_read_collection_data(self): ) self.assertFalse(did_merge) - def test_clone_sets_asset_owner(self): + def test_clone_sets_asset_owner(self) -> None: cat = TestCases.test_case_2() item = next(iter(cat.get_all_items())) original_asset = list(item.assets.values())[0] @@ -206,7 +206,7 @@ def test_clone_sets_asset_owner(self): clone_asset = list(clone.assets.values())[0] self.assertIs(clone_asset.owner, clone) - def test_make_asset_href_relative_is_noop_on_relative_hrefs(self): + def test_make_asset_href_relative_is_noop_on_relative_hrefs(self) -> None: cat = TestCases.test_case_2() item = next(iter(cat.get_all_items())) asset = list(item.assets.values())[0] @@ -218,7 +218,7 @@ def test_make_asset_href_relative_is_noop_on_relative_hrefs(self): class CommonMetadataTest(unittest.TestCase): - def setUp(self): + def setUp(self) -> None: self.URI_1 = TestCases.get_path( "data-files/examples/1.0.0-beta.2/item-spec/examples/datetimerange.json" ) @@ -241,7 +241,7 @@ def setUp(self): ], } - def test_datetimes(self): + def test_datetimes(self) -> None: # save dict of original item to check that `common_metadata` # method doesn't mutate self.item_1 before = self.ITEM_1.clone().to_dict() @@ -254,7 +254,7 @@ def test_datetimes(self): self.assertDictEqual(before, self.ITEM_1.to_dict()) self.assertIsNone(common_metadata.providers) - def test_common_metadata_start_datetime(self): + def test_common_metadata_start_datetime(self) -> None: x = self.ITEM_1.clone() start_datetime_str = "2018-01-01T13:21:30Z" start_datetime_dt = str_to_datetime(start_datetime_str) @@ -269,7 +269,7 @@ def test_common_metadata_start_datetime(self): self.assertEqual(x.common_metadata.start_datetime, example_datetime_dt) self.assertEqual(x.properties["start_datetime"], example_datetime_str) - def test_common_metadata_end_datetime(self): + def test_common_metadata_end_datetime(self) -> None: x = self.ITEM_1.clone() end_datetime_str = "2018-01-01T13:31:30Z" end_datetime_dt = str_to_datetime(end_datetime_str) @@ -284,7 +284,7 @@ def test_common_metadata_end_datetime(self): self.assertEqual(x.common_metadata.end_datetime, example_datetime_dt) self.assertEqual(x.properties["end_datetime"], example_datetime_str) - def test_common_metadata_created(self): + def test_common_metadata_created(self) -> None: x = self.ITEM_2.clone() created_str = "2016-05-04T00:00:01Z" created_dt = str_to_datetime(created_str) @@ -299,7 +299,7 @@ def test_common_metadata_created(self): self.assertEqual(x.common_metadata.created, example_datetime_dt) self.assertEqual(x.properties["created"], example_datetime_str) - def test_common_metadata_updated(self): + def test_common_metadata_updated(self) -> None: x = self.ITEM_2.clone() updated_str = "2017-01-01T00:30:55Z" updated_dt = str_to_datetime(updated_str) @@ -314,7 +314,7 @@ def test_common_metadata_updated(self): self.assertEqual(x.common_metadata.updated, example_datetime_dt) self.assertEqual(x.properties["updated"], example_datetime_str) - def test_common_metadata_providers(self): + def test_common_metadata_providers(self) -> None: x = self.ITEM_2.clone() providers_dict_list: List[Dict[str, Any]] = [ @@ -370,7 +370,7 @@ def test_common_metadata_providers(self): self.assertIsInstance(pd2, dict) self.assertDictEqual(pd1, pd2) - def test_common_metadata_basics(self): + def test_common_metadata_basics(self) -> None: x = self.ITEM_2.clone() # Title @@ -434,7 +434,7 @@ def test_common_metadata_basics(self): self.assertEqual(x.common_metadata.gsd, example_gsd) self.assertEqual(x.properties["gsd"], example_gsd) - def test_asset_start_datetime(self): + def test_asset_start_datetime(self) -> None: item = pystac.Item.from_file( TestCases.get_path("data-files/item/sample-item-asset-properties.json") ) @@ -457,7 +457,7 @@ def test_asset_start_datetime(self): self.assertEqual(new_a1_value, set_value) self.assertEqual(cm.start_datetime, item_value) - def test_asset_end_datetime(self): + def test_asset_end_datetime(self) -> None: item = pystac.Item.from_file( TestCases.get_path("data-files/item/sample-item-asset-properties.json") ) @@ -480,7 +480,7 @@ def test_asset_end_datetime(self): self.assertEqual(new_a1_value, set_value) self.assertEqual(cm.end_datetime, item_value) - def test_asset_license(self): + def test_asset_license(self) -> None: item = pystac.Item.from_file( TestCases.get_path("data-files/item/sample-item-asset-properties.json") ) @@ -503,7 +503,7 @@ def test_asset_license(self): self.assertEqual(new_a1_value, set_value) self.assertEqual(cm.license, item_value) - def test_asset_providers(self): + def test_asset_providers(self) -> None: item = pystac.Item.from_file( TestCases.get_path("data-files/item/sample-item-asset-properties.json") ) @@ -538,7 +538,7 @@ def test_asset_providers(self): self.assertEqual(new_a1_value[0].to_dict(), set_value[0].to_dict()) self.assertEqual(get_opt(cm.providers)[0].to_dict(), item_value[0].to_dict()) - def test_asset_platform(self): + def test_asset_platform(self) -> None: item = pystac.Item.from_file( TestCases.get_path("data-files/item/sample-item-asset-properties.json") ) @@ -561,7 +561,7 @@ def test_asset_platform(self): self.assertEqual(new_a1_value, set_value) self.assertEqual(cm.platform, item_value) - def test_asset_instruments(self): + def test_asset_instruments(self) -> None: item = pystac.Item.from_file( TestCases.get_path("data-files/item/sample-item-asset-properties.json") ) @@ -584,7 +584,7 @@ def test_asset_instruments(self): self.assertEqual(new_a1_value, set_value) self.assertEqual(cm.instruments, item_value) - def test_asset_constellation(self): + def test_asset_constellation(self) -> None: item = pystac.Item.from_file( TestCases.get_path("data-files/item/sample-item-asset-properties.json") ) @@ -607,7 +607,7 @@ def test_asset_constellation(self): self.assertEqual(new_a1_value, set_value) self.assertEqual(cm.constellation, item_value) - def test_asset_mission(self): + def test_asset_mission(self) -> None: item = pystac.Item.from_file( TestCases.get_path("data-files/item/sample-item-asset-properties.json") ) @@ -630,7 +630,7 @@ def test_asset_mission(self): self.assertEqual(new_a1_value, set_value) self.assertEqual(cm.mission, item_value) - def test_asset_gsd(self): + def test_asset_gsd(self) -> None: item = pystac.Item.from_file( TestCases.get_path("data-files/item/sample-item-asset-properties.json") ) @@ -653,7 +653,7 @@ def test_asset_gsd(self): self.assertEqual(new_a1_value, set_value) self.assertEqual(cm.gsd, item_value) - def test_asset_created(self): + def test_asset_created(self) -> None: item = pystac.Item.from_file( TestCases.get_path("data-files/item/sample-item-asset-properties.json") ) @@ -676,7 +676,7 @@ def test_asset_created(self): self.assertEqual(new_a1_value, set_value) self.assertEqual(cm.created, item_value) - def test_asset_updated(self): + def test_asset_updated(self) -> None: item = pystac.Item.from_file( TestCases.get_path("data-files/item/sample-item-asset-properties.json") ) diff --git a/tests/test_layout.py b/tests/test_layout.py index bb0840ea5..f88365364 100644 --- a/tests/test_layout.py +++ b/tests/test_layout.py @@ -16,7 +16,7 @@ class LayoutTemplateTest(unittest.TestCase): - def test_templates_item_datetime(self): + def test_templates_item_datetime(self) -> None: year = 2020 month = 11 day = 3 @@ -45,7 +45,7 @@ def test_templates_item_datetime(self): path = template.substitute(item) self.assertEqual(path, "2020/11/3/2020-11-03/item.json") - def test_templates_item_start_datetime(self): + def test_templates_item_start_datetime(self) -> None: year = 2020 month = 11 day = 3 @@ -77,10 +77,11 @@ def test_templates_item_start_datetime(self): path = template.substitute(item) self.assertEqual(path, "2020/11/3/2020-11-03/item.json") - def test_templates_item_collection(self): + def test_templates_item_collection(self) -> None: template = LayoutTemplate("${collection}/item.json") collection = TestCases.test_case_4().get_child("acc") + assert collection is not None item = next(iter(collection.get_all_items())) assert item.collection_id is not None @@ -92,10 +93,11 @@ def test_templates_item_collection(self): path = template.substitute(item) self.assertEqual(path, "{}/item.json".format(item.collection_id)) - def test_throws_for_no_collection(self): + def test_throws_for_no_collection(self) -> None: template = LayoutTemplate("${collection}/item.json") collection = TestCases.test_case_4().get_child("acc") + assert collection is not None item = next(iter(collection.get_all_items())) item.set_collection(None) assert item.collection_id is None @@ -103,7 +105,7 @@ def test_throws_for_no_collection(self): with self.assertRaises(TemplateError): template.get_template_values(item) - def test_nested_properties(self): + def test_nested_properties(self) -> None: dt = datetime(2020, 11, 3, 18, 30) template = LayoutTemplate("${test.prop}/${ext:extra.test.prop}/item.json") @@ -128,7 +130,7 @@ def test_nested_properties(self): self.assertEqual(path, "4326/3857/item.json") - def test_substitute_with_colon_properties(self): + def test_substitute_with_colon_properties(self) -> None: dt = datetime(2020, 11, 3, 18, 30) template = LayoutTemplate("${ext:prop}/item.json") @@ -145,7 +147,7 @@ def test_substitute_with_colon_properties(self): self.assertEqual(path, "1/item.json") - def test_defaults(self): + def test_defaults(self) -> None: template = LayoutTemplate( "${doesnotexist}/collection.json", defaults={"doesnotexist": "yes"} ) @@ -157,7 +159,7 @@ def test_defaults(self): self.assertEqual(path, "yes/collection.json") - def test_docstring_examples(self): + def test_docstring_examples(self) -> None: item = pystac.Item.from_file( TestCases.get_path( "data-files/examples/1.0.0-beta.2/item-spec/" @@ -214,13 +216,13 @@ def fn(item: pystac.Item, parent_dir: str) -> str: return fn - def test_produces_layout_for_catalog(self): + def test_produces_layout_for_catalog(self) -> None: strategy = CustomLayoutStrategy(catalog_func=self.get_custom_catalog_func()) cat = pystac.Catalog(id="test", description="test desc") href = strategy.get_href(cat, parent_dir="http://example.com", is_root=True) self.assertEqual(href, "http://example.com/cat/True/test.json") - def test_produces_fallback_layout_for_catalog(self): + def test_produces_fallback_layout_for_catalog(self) -> None: fallback = BestPracticesLayoutStrategy() strategy = CustomLayoutStrategy( collection_func=self.get_custom_collection_func(), @@ -232,7 +234,7 @@ def test_produces_fallback_layout_for_catalog(self): expected = fallback.get_href(cat, parent_dir="http://example.com") self.assertEqual(href, expected) - def test_produces_layout_for_collection(self): + def test_produces_layout_for_collection(self) -> None: strategy = CustomLayoutStrategy( collection_func=self.get_custom_collection_func() ) @@ -242,7 +244,7 @@ def test_produces_layout_for_collection(self): href, "http://example.com/col/False/{}.json".format(collection.id) ) - def test_produces_fallback_layout_for_collection(self): + def test_produces_fallback_layout_for_collection(self) -> None: fallback = BestPracticesLayoutStrategy() strategy = CustomLayoutStrategy( catalog_func=self.get_custom_catalog_func(), @@ -254,14 +256,14 @@ def test_produces_fallback_layout_for_collection(self): expected = fallback.get_href(collection, parent_dir="http://example.com") self.assertEqual(href, expected) - def test_produces_layout_for_item(self): + def test_produces_layout_for_item(self) -> None: strategy = CustomLayoutStrategy(item_func=self.get_custom_item_func()) collection = TestCases.test_case_8() item = next(iter(collection.get_all_items())) href = strategy.get_href(item, parent_dir="http://example.com") self.assertEqual(href, "http://example.com/item/{}.json".format(item.id)) - def test_produces_fallback_layout_for_item(self): + def test_produces_fallback_layout_for_item(self) -> None: fallback = BestPracticesLayoutStrategy() strategy = CustomLayoutStrategy( catalog_func=self.get_custom_catalog_func(), @@ -285,20 +287,20 @@ def _get_collection(self) -> Collection: assert isinstance(result, Collection) return result - def test_produces_layout_for_catalog(self): + def test_produces_layout_for_catalog(self) -> None: strategy = TemplateLayoutStrategy(catalog_template=self.TEST_CATALOG_TEMPLATE) cat = pystac.Catalog(id="test", description="test-desc") href = strategy.get_href(cat, parent_dir="http://example.com") self.assertEqual(href, "http://example.com/cat/test/test-desc/catalog.json") - def test_produces_layout_for_catalog_with_filename(self): + def test_produces_layout_for_catalog_with_filename(self) -> None: template = "cat/${id}/${description}/${id}.json" strategy = TemplateLayoutStrategy(catalog_template=template) cat = pystac.Catalog(id="test", description="test-desc") href = strategy.get_href(cat, parent_dir="http://example.com") self.assertEqual(href, "http://example.com/cat/test/test-desc/test.json") - def test_produces_fallback_layout_for_catalog(self): + def test_produces_fallback_layout_for_catalog(self) -> None: fallback = BestPracticesLayoutStrategy() strategy = TemplateLayoutStrategy( collection_template=self.TEST_COLLECTION_TEMPLATE, @@ -310,7 +312,7 @@ def test_produces_fallback_layout_for_catalog(self): expected = fallback.get_href(cat, parent_dir="http://example.com") self.assertEqual(href, expected) - def test_produces_layout_for_collection(self): + def test_produces_layout_for_collection(self) -> None: strategy = TemplateLayoutStrategy( collection_template=self.TEST_COLLECTION_TEMPLATE ) @@ -323,7 +325,7 @@ def test_produces_layout_for_collection(self): ), ) - def test_produces_layout_for_collection_with_filename(self): + def test_produces_layout_for_collection_with_filename(self) -> None: template = "col/${id}/${license}/col.json" strategy = TemplateLayoutStrategy(collection_template=template) collection = self._get_collection() @@ -335,7 +337,7 @@ def test_produces_layout_for_collection_with_filename(self): ), ) - def test_produces_fallback_layout_for_collection(self): + def test_produces_fallback_layout_for_collection(self) -> None: fallback = BestPracticesLayoutStrategy() strategy = TemplateLayoutStrategy( catalog_template=self.TEST_CATALOG_TEMPLATE, @@ -347,7 +349,7 @@ def test_produces_fallback_layout_for_collection(self): expected = fallback.get_href(collection, parent_dir="http://example.com") self.assertEqual(href, expected) - def test_produces_layout_for_item(self): + def test_produces_layout_for_item(self) -> None: strategy = TemplateLayoutStrategy(item_template=self.TEST_ITEM_TEMPLATE) collection = self._get_collection() item = next(iter(collection.get_all_items())) @@ -357,7 +359,7 @@ def test_produces_layout_for_item(self): "http://example.com/item/{}/{}.json".format(item.collection_id, item.id), ) - def test_produces_layout_for_item_without_filename(self): + def test_produces_layout_for_item_without_filename(self) -> None: template = "item/${collection}" strategy = TemplateLayoutStrategy(item_template=template) collection = self._get_collection() @@ -368,7 +370,7 @@ def test_produces_layout_for_item_without_filename(self): "http://example.com/item/{}/{}.json".format(item.collection_id, item.id), ) - def test_produces_fallback_layout_for_item(self): + def test_produces_fallback_layout_for_item(self) -> None: fallback = BestPracticesLayoutStrategy() strategy = TemplateLayoutStrategy( catalog_template=self.TEST_CATALOG_TEMPLATE, @@ -383,36 +385,36 @@ def test_produces_fallback_layout_for_item(self): class BestPracticesLayoutStrategyTest(unittest.TestCase): - def setUp(self): + def setUp(self) -> None: self.strategy = BestPracticesLayoutStrategy() - def test_produces_layout_for_root_catalog(self): + def test_produces_layout_for_root_catalog(self) -> None: cat = pystac.Catalog(id="test", description="test desc") href = self.strategy.get_href( cat, parent_dir="http://example.com", is_root=True ) self.assertEqual(href, "http://example.com/catalog.json") - def test_produces_layout_for_child_catalog(self): + def test_produces_layout_for_child_catalog(self) -> None: cat = pystac.Catalog(id="test", description="test desc") href = self.strategy.get_href(cat, parent_dir="http://example.com") self.assertEqual(href, "http://example.com/test/catalog.json") - def test_produces_layout_for_root_collection(self): + def test_produces_layout_for_root_collection(self) -> None: collection = TestCases.test_case_8() href = self.strategy.get_href( collection, parent_dir="http://example.com", is_root=True ) self.assertEqual(href, "http://example.com/collection.json") - def test_produces_layout_for_child_collection(self): + def test_produces_layout_for_child_collection(self) -> None: collection = TestCases.test_case_8() href = self.strategy.get_href(collection, parent_dir="http://example.com") self.assertEqual( href, "http://example.com/{}/collection.json".format(collection.id) ) - def test_produces_layout_for_item(self): + def test_produces_layout_for_item(self) -> None: collection = TestCases.test_case_8() item = next(iter(collection.get_all_items())) href = self.strategy.get_href(item, parent_dir="http://example.com") diff --git a/tests/test_link.py b/tests/test_link.py index d916f1f07..b9a50b13c 100644 --- a/tests/test_link.py +++ b/tests/test_link.py @@ -1,5 +1,6 @@ import datetime import unittest +from typing import Any, Dict, List import pystac from tests.utils.test_cases import ARBITRARY_EXTENT @@ -10,7 +11,7 @@ class LinkTest(unittest.TestCase): item: pystac.Item - def setUp(self): + def setUp(self) -> None: self.item = pystac.Item( id="test-item", geometry=None, @@ -19,7 +20,7 @@ def setUp(self): properties={}, ) - def test_minimal(self): + def test_minimal(self) -> None: rel = "my rel" target = "https://example.com/a/b" link = pystac.Link(rel, target) @@ -53,7 +54,7 @@ def test_minimal(self): link.set_owner(self.item) self.assertEqual(self.item, link.owner) - def test_relative(self): + def test_relative(self) -> None: rel = "my rel" target = "../elsewhere" mime_type = "example/stac_thing" @@ -67,27 +68,41 @@ def test_relative(self): } self.assertEqual(expected_dict, link.to_dict()) - def test_link_does_not_fail_if_href_is_none(self): + def test_link_does_not_fail_if_href_is_none(self) -> None: """Test to ensure get_href does not fail when the href is None.""" catalog = pystac.Catalog(id="test", description="test desc") catalog.add_item(self.item) catalog.set_self_href("/some/href") link = catalog.get_single_link("item") + assert link is not None self.assertIsNone(link.get_href()) - def test_resolve_stac_object_no_root_and_target_is_item(self): + def test_resolve_stac_object_no_root_and_target_is_item(self) -> None: link = pystac.Link("my rel", target=self.item) link.resolve_stac_object() class StaticLinkTest(unittest.TestCase): - def test_from_dict_round_trip(self): - test_cases = [ + def setUp(self) -> None: + self.item = pystac.Item( + id="test-item", + geometry=None, + bbox=None, + datetime=TEST_DATETIME, + properties={}, + ) + + self.collection = pystac.Collection( + "collection id", "desc", extent=ARBITRARY_EXTENT + ) + + def test_from_dict_round_trip(self) -> None: + test_cases: List[Dict[str, Any]] = [ {"rel": "", "href": ""}, # Not valid, but works. {"rel": "r", "href": "t"}, {"rel": "r", "href": "/t"}, - {"rel": "r", "href": "t", "type": "a/b", "title": "t", "c": "d", 1: 2}, + {"rel": "r", "href": "t", "type": "a/b", "title": "t", "c": "d", "1": 2}, # Special case. {"rel": "self", "href": "t"}, ] @@ -95,19 +110,28 @@ def test_from_dict_round_trip(self): d2 = pystac.Link.from_dict(d).to_dict() self.assertEqual(d, d2) - def test_from_dict_failures(self): - for d in [{}, {"href": "t"}, {"rel": "r"}]: + def test_from_dict_failures(self) -> None: + dicts: List[Dict[str, Any]] = [{}, {"href": "t"}, {"rel": "r"}] + for d in dicts: with self.assertRaises(KeyError): pystac.Link.from_dict(d) - def test_collection(self): - c = pystac.Collection("collection id", "desc", extent=ARBITRARY_EXTENT) - link = pystac.Link.collection(c) + def test_collection(self) -> None: + link = pystac.Link.collection(self.collection) expected = {"rel": "collection", "href": None, "type": "application/json"} self.assertEqual(expected, link.to_dict()) - def test_child(self): - c = pystac.Collection("collection id", "desc", extent=ARBITRARY_EXTENT) - link = pystac.Link.child(c) + def test_child(self) -> None: + link = pystac.Link.child(self.collection) expected = {"rel": "child", "href": None, "type": "application/json"} self.assertEqual(expected, link.to_dict()) + + def test_canonical_item(self) -> None: + link = pystac.Link.canonical(self.item) + expected = {"rel": "canonical", "href": None, "type": "application/json"} + self.assertEqual(expected, link.to_dict()) + + def test_canonical_collection(self) -> None: + link = pystac.Link.canonical(self.collection) + expected = {"rel": "canonical", "href": None, "type": "application/json"} + self.assertEqual(expected, link.to_dict()) diff --git a/tests/test_stac_io.py b/tests/test_stac_io.py index 97b18649c..58ac51e23 100644 --- a/tests/test_stac_io.py +++ b/tests/test_stac_io.py @@ -7,7 +7,7 @@ class StacIOTest(unittest.TestCase): - def test_stac_io_issues_warnings(self): + def test_stac_io_issues_warnings(self) -> None: with warnings.catch_warnings(record=True) as w: # Cause all warnings to always be triggered. warnings.simplefilter("always") @@ -20,7 +20,7 @@ def test_stac_io_issues_warnings(self): self.assertEqual(len(w), 1) self.assertTrue(issubclass(w[-1].category, DeprecationWarning)) - def test_read_text(self): + def test_read_text(self) -> None: _ = pystac.read_file( TestCases.get_path("data-files/collections/multi-extent.json") ) diff --git a/tests/test_utils.py b/tests/test_utils.py index 37e197c2d..e80345780 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -60,7 +60,7 @@ def test_make_relative_href_url(self) -> None: actual = make_relative_href(source_href, start_href) self.assertEqual(actual, expected) - def test_make_relative_href_windows(self): + def test_make_relative_href_windows(self) -> None: utils._pathlib = ntpath try: # Test cases of (source_href, start_href, expected) @@ -118,7 +118,7 @@ def test_make_relative_href_windows(self): @unittest.skipIf( sys.platform in ("win32", "cygwin"), reason="Paths are specific to posix" ) - def test_make_absolute_href(self): + def test_make_absolute_href(self) -> None: # Test cases of (source_href, start_href, expected) test_cases = [ ("item.json", "/a/b/c/catalog.json", "/a/b/c/item.json"), @@ -154,14 +154,14 @@ def test_make_absolute_href(self): @unittest.skipIf( sys.platform in ("win32", "cygwin"), reason="Paths are specific to posix" ) - def test_make_absolute_href_on_vsitar(self): + def test_make_absolute_href_on_vsitar(self) -> None: rel_path = "some/item.json" cat_path = "/vsitar//tmp/catalog.tar/catalog.json" expected = "/vsitar//tmp/catalog.tar/some/item.json" self.assertEqual(expected, make_absolute_href(rel_path, cat_path)) - def test_make_absolute_href_windows(self): + def test_make_absolute_href_windows(self) -> None: utils._pathlib = ntpath try: # Test cases of (source_href, start_href, expected) @@ -202,7 +202,7 @@ def test_make_absolute_href_windows(self): finally: utils._pathlib = os.path - def test_is_absolute_href(self): + def test_is_absolute_href(self) -> None: # Test cases of (href, expected) test_cases = [ ("item.json", False), @@ -216,7 +216,7 @@ def test_is_absolute_href(self): actual = is_absolute_href(href) self.assertEqual(actual, expected) - def test_is_absolute_href_windows(self): + def test_is_absolute_href_windows(self) -> None: utils._pathlib = ntpath try: @@ -235,7 +235,7 @@ def test_is_absolute_href_windows(self): finally: utils._pathlib = os.path - def test_datetime_to_str(self): + def test_datetime_to_str(self) -> None: cases = ( ( "timezone naive, assume utc", @@ -259,7 +259,7 @@ def test_datetime_to_str(self): got = utils.datetime_to_str(dt) self.assertEqual(expected, got) - def test_geojson_bbox(self): + def test_geojson_bbox(self) -> None: # Use sample Geojson from https://en.wikipedia.org/wiki/GeoJSON with open("tests/data-files/geojson/sample.geojson") as sample_geojson: all_features = json.load(sample_geojson) diff --git a/tests/test_version.py b/tests/test_version.py index cc48ff1b7..3c20ee852 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -6,18 +6,18 @@ class VersionTest(unittest.TestCase): - def setUp(self): + def setUp(self) -> None: self._prev_env_version = os.environ.get("PYSTAC_STAC_VERSION_OVERRIDE") self._prev_version = pystac.get_stac_version() - def tearDown(self): + def tearDown(self) -> None: if self._prev_env_version is None: os.environ.pop("PYSTAC_STAC_VERSION_OVERRIDE", None) else: os.environ["PYSTAC_STAC_VERSION_OVERRIDE"] = self._prev_env_version pystac.set_stac_version(None) - def test_override_stac_version_with_environ(self): + def test_override_stac_version_with_environ(self) -> None: override_version = "1.0.0-gamma.2" os.environ["PYSTAC_STAC_VERSION_OVERRIDE"] = override_version @@ -25,7 +25,7 @@ def test_override_stac_version_with_environ(self): d = cat.to_dict() self.assertEqual(d["stac_version"], override_version) - def test_override_stac_version_with_call(self): + def test_override_stac_version_with_call(self) -> None: override_version = "1.0.0-delta.2" pystac.set_stac_version(override_version) cat = TestCases.test_case_1() diff --git a/tests/test_writing.py b/tests/test_writing.py index ccf6be320..049557824 100644 --- a/tests/test_writing.py +++ b/tests/test_writing.py @@ -1,6 +1,7 @@ import os import unittest from tempfile import TemporaryDirectory +from typing import Any, List import pystac from pystac import Collection, CatalogType, HIERARCHICAL_LINKS @@ -15,7 +16,7 @@ class STACWritingTest(unittest.TestCase): and ensure that links are correctly set to relative or absolute. """ - def validate_catalog(self, catalog: pystac.Catalog): + def validate_catalog(self, catalog: pystac.Catalog) -> int: catalog.validate() validated_count = 1 @@ -28,12 +29,14 @@ def validate_catalog(self, catalog: pystac.Catalog): return validated_count - def validate_file(self, path: str, object_type: str): + def validate_file(self, path: str, object_type: str) -> List[Any]: d = pystac.StacIO.default().read_json(path) return validate_dict(d, pystac.STACObjectType(object_type)) - def validate_link_types(self, root_href: str, catalog_type: pystac.CatalogType): - def validate_asset_href_type(item: pystac.Item, item_href: str): + def validate_link_types( + self, root_href: str, catalog_type: pystac.CatalogType + ) -> None: + def validate_asset_href_type(item: pystac.Item, item_href: str) -> None: for asset in item.assets.values(): if not is_absolute_href(asset.href): is_valid = not is_absolute_href(asset.href) @@ -47,13 +50,13 @@ def validate_asset_href_type(item: pystac.Item, item_href: str): def validate_item_link_type( href: str, link_type: str, should_include_self: bool - ): + ) -> None: item_dict = pystac.StacIO.default().read_json(href) item = pystac.Item.from_file(href) - rel_links = ( - HIERARCHICAL_LINKS - + pystac.EXTENSION_HOOKS.get_extended_object_links(item) - ) + rel_links = [ + *HIERARCHICAL_LINKS, + *pystac.EXTENSION_HOOKS.get_extended_object_links(item), + ] for link in item.get_links(): if not link.rel == "self": if link_type == "RELATIVE" and link.rel in rel_links: @@ -68,7 +71,7 @@ def validate_item_link_type( def validate_catalog_link_type( href: str, link_type: str, should_include_self: bool - ): + ) -> None: cat_dict = pystac.StacIO.default().read_json(href) cat = pystac.Catalog.from_file(href) @@ -122,7 +125,7 @@ def do_test( for item in items: self.validate_file(item.self_href, pystac.STACObjectType.ITEM) - def test_testcases(self): + def test_testcases(self) -> None: for catalog in TestCases.all_test_catalogs(): catalog = catalog.full_copy() ctypes = [ diff --git a/tests/utils/stac_io_mock.py b/tests/utils/stac_io_mock.py index 57904df5c..1ff0d437d 100644 --- a/tests/utils/stac_io_mock.py +++ b/tests/utils/stac_io_mock.py @@ -9,7 +9,7 @@ class MockStacIO(pystac.StacIO): clients to replace STAC_IO functionality, all within a context scope. """ - def __init__(self): + def __init__(self) -> None: self.mock = Mock() def read_text( diff --git a/tests/utils/test_cases.py b/tests/utils/test_cases.py index 4faae0f6c..4fc820265 100644 --- a/tests/utils/test_cases.py +++ b/tests/utils/test_cases.py @@ -203,7 +203,7 @@ def test_case_3() -> Catalog: return root_cat @staticmethod - def test_case_4(): + def test_case_4() -> Catalog: """Test case that is based on a local copy of the Tier 1 dataset from DrivenData's OpenCities AI Challenge. See: https://www.drivendata.org/competitions/60/building-segmentation-disaster-resilience @@ -213,21 +213,21 @@ def test_case_4(): ) @staticmethod - def test_case_5(): + def test_case_5() -> Catalog: """Based on a subset of https://cbers.stac.cloud/""" return Catalog.from_file( TestCases.get_path("data-files/catalogs/test-case-5/catalog.json") ) @staticmethod - def test_case_6(): + def test_case_6() -> Catalog: """Based on a subset of CBERS, contains a root and 4 empty children""" return Catalog.from_file( TestCases.get_path("data-files/catalogs/cbers-partial/catalog.json") ) @staticmethod - def test_case_7(): + def test_case_7() -> Catalog: """Test case 4 as STAC version 0.8.1""" return Catalog.from_file( TestCases.get_path("data-files/catalogs/label_catalog-v0.8.1/catalog.json") diff --git a/tests/validation/test_schema_uri_map.py b/tests/validation/test_schema_uri_map.py index dd6954a7a..9075710dc 100644 --- a/tests/validation/test_schema_uri_map.py +++ b/tests/validation/test_schema_uri_map.py @@ -5,7 +5,7 @@ class SchemaUriMapTest(unittest.TestCase): - def test_gets_schema_uri_for_old_version(self): + def test_gets_schema_uri_for_old_version(self) -> None: d = DefaultSchemaUriMap() uri = d.get_object_schema_uri(pystac.STACObjectType.ITEM, "0.8.0") diff --git a/tests/validation/test_validate.py b/tests/validation/test_validate.py index d5492cbf0..d45a3d329 100644 --- a/tests/validation/test_validate.py +++ b/tests/validation/test_validate.py @@ -17,7 +17,7 @@ class ValidateTest(unittest.TestCase): - def test_validate_current_version(self): + def test_validate_current_version(self) -> None: catalog = pystac.read_file( TestCases.get_path("data-files/catalogs/test-case-1/" "catalog.json") ) @@ -35,7 +35,7 @@ def test_validate_current_version(self): item = pystac.read_file(TestCases.get_path("data-files/item/sample-item.json")) item.validate() - def test_validate_examples(self): + def test_validate_examples(self) -> None: for example in TestCases.get_examples_info(): with self.subTest(example.path): stac_version = example.stac_version @@ -72,10 +72,11 @@ def test_validate_examples(self): ) raise e - def test_validate_error_contains_href(self): + def test_validate_error_contains_href(self) -> None: # Test that the exception message contains the HREF of the object if available. cat = TestCases.test_case_1() item = cat.get_item("area-1-1-labels", recursive=True) + assert item is not None assert item.get_self_href() is not None item.geometry = {"type": "INVALID"} @@ -87,7 +88,7 @@ def test_validate_error_contains_href(self): self.assertTrue(get_opt(item.get_self_href()) in str(e)) raise e - def test_validate_all(self): + def test_validate_all(self) -> None: for test_case in TestCases.all_test_catalogs(): catalog_href = test_case.get_self_href() if catalog_href is not None: @@ -128,7 +129,7 @@ def test_validate_all(self): with self.assertRaises(pystac.STACValidationError): pystac.validation.validate_all(stac_dict, new_cat_href) - def test_validates_geojson_with_tuple_coordinates(self): + def test_validates_geojson_with_tuple_coordinates(self) -> None: """This unit tests guards against a bug where if a geometry dict has tuples instead of lists for the coordinate sequence, which can be produced by shapely, then the geometry still passses