Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/api_reference/eoproduct.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@ Conversion
----------

.. automethod:: EOProduct.as_dict
.. automethod:: EOProduct.as_pystac_object
.. automethod:: EOProduct.from_dict
.. automethod:: EOProduct.from_file
.. automethod:: EOProduct.from_pystac

Interface
---------
Expand Down
6 changes: 4 additions & 2 deletions docs/api_reference/searchresult.rst
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ Conversion
.. autosummary::

SearchResult.from_dict
SearchResult.from_pystac
SearchResult.as_dict
SearchResult.as_pystac_object
SearchResult.as_shapely_geometry_object
SearchResult.as_wkt_object

Expand All @@ -60,5 +62,5 @@ Interface

.. autoclass:: SearchResult
:members: crunch, filter_date, filter_latest_intersect, filter_latest_by_name, filter_overlap, filter_property,
filter_online, from_dict, as_dict, as_shapely_geometry_object, as_wkt_object, next_page,
__geo_interface__
filter_online, from_dict, from_pystac, as_dict, as_pystac_object, as_shapely_geometry_object,
as_wkt_object, next_page, __geo_interface__
11 changes: 6 additions & 5 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,15 +271,16 @@
# configuration for intersphinx
intersphinx_mapping = {
"python": ("https://docs.python.org/3/", None),
"click": ("https://click.palletsprojects.com/en/stable/", None),
"fsspec": ("https://filesystem-spec.readthedocs.io/en/stable/", None),
"pydantic": ("https://docs.pydantic.dev/latest", None),
"pystac": ("https://pystac.readthedocs.io/en/stable/", None),
"python-requests": ("https://requests.readthedocs.io/en/stable/", None),
"rasterio": ("https://rasterio.readthedocs.io/en/stable/", None),
"rioxarray": ("https://corteva.github.io/rioxarray/stable/", None),
"shapely": ("https://shapely.readthedocs.io/en/stable/", None),
"click": ("https://click.palletsprojects.com/en/stable/", None),
"urllib3": ("https://urllib3.readthedocs.io/en/stable/", None),
"xarray": ("https://docs.xarray.dev/en/stable/", None),
"rasterio": ("https://rasterio.readthedocs.io/en/stable/", None),
"rioxarray": ("https://corteva.github.io/rioxarray/stable/", None),
"fsspec": ("https://filesystem-spec.readthedocs.io/en/stable/", None),
"pydantic": ("https://docs.pydantic.dev/latest", None),
}

suppress_warnings = ["misc.copy_overwrite"]
Expand Down
4,657 changes: 4,495 additions & 162 deletions docs/notebooks/api_user_guide/5_serialize_deserialize.ipynb

Large diffs are not rendered by default.

31 changes: 31 additions & 0 deletions eodag/api/product/_product.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import geojson
import orjson
import requests
from pystac import Item
from requests import RequestException
from requests.auth import AuthBase
from shapely import geometry
Expand Down Expand Up @@ -302,6 +303,15 @@ def as_dict(self, skip_invalid: bool = True) -> dict[str, Any]:
}
return geojson_repr

def as_pystac_object(self, skip_invalid: bool = True) -> Item:
"""Builds a representation of EOProduct as a pystac Item to enable its manipulation with pystac methods

:param skip_invalid: Whether to skip properties whose values are not valid according to the STAC specification.
:returns: The representation of a :class:`~eodag.api.product._product.EOProduct` as a :class:`pystac.Item`
"""
prod_dict = self.as_dict(skip_invalid=skip_invalid)
return Item.from_dict(prod_dict)

@classmethod
def from_dict(
cls,
Expand Down Expand Up @@ -362,6 +372,27 @@ def from_file(

return cls.from_dict(feature, dag=dag, raise_errors=raise_errors)

@classmethod
def from_pystac(
cls,
item: Item,
dag: Optional[EODataAccessGateway] = None,
raise_errors: bool = False,
) -> EOProduct:
"""Builds an :class:`~eodag.api.product._product.EOProduct` object from a pystac Item.

:param item: The :class:`pystac.Item` containing the metadata of the product
:param dag: (optional) The EODataAccessGateway instance to use for registering the product downloader. If not
provided, the downloader and authenticator will not be registered.
:param raise_errors: (optional) Whether to raise exceptions in case of errors during the deserialize process.
If False, several import methods will be tried: from serialized, from eodag-server, from
known provider, from unknown provider.
:returns: An instance of :class:`~eodag.api.product._product.EOProduct`
:raises: :class:`~eodag.utils.exceptions.ValidationError`
"""
feature = item.to_dict()
return cls.from_dict(feature, dag=dag, raise_errors=raise_errors)

@classmethod
@_deprecated(
reason="Please use 'EOProduct.from_dict' instead",
Expand Down
27 changes: 27 additions & 0 deletions eodag/api/search_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from typing import TYPE_CHECKING, Annotated, Any, Iterable, Iterator, Optional, Union

import geojson
from pystac import ItemCollection
from shapely.geometry import GeometryCollection
from shapely.geometry import mapping as shapely_mapping
from shapely.geometry import shape
Expand Down Expand Up @@ -235,6 +236,22 @@ def from_file(

return cls.from_dict(feature, dag=dag)

@classmethod
def from_pystac(
cls,
item_collection: ItemCollection,
dag: Optional[EODataAccessGateway] = None,
) -> SearchResult:
"""Builds an :class:`~eodag.api.search_result.SearchResult` object from a pystac ItemCollection

:param item_collection: The :class:`pystac.ItemCollection` containing the metadata of the products.
:param dag: (optional) The EODataAccessGateway instance to use for registering the products.
:returns: An eodag representation of a search result
"""
features_collection = item_collection.to_dict()

return cls.from_dict(features_collection, dag=dag)

@staticmethod
@_deprecated(
reason="Please use 'SearchResult.from_dict' instead",
Expand Down Expand Up @@ -315,6 +332,16 @@ def as_wkt_object(self, skip_invalid: bool = True) -> str:
"""
return self.as_shapely_geometry_object(skip_invalid=skip_invalid).wkt

def as_pystac_object(self, skip_invalid: bool = True) -> ItemCollection:
"""Pystac ItemCollection representation of SearchResult

:param skip_invalid: Whether to skip properties whose values are not valid according to the STAC specification.
:returns: The representation of a :class:`~eodag.api.search_result.SearchResult` as a
:class:`pystac.ItemCollection`
"""
results_dict = self.as_dict(skip_invalid=skip_invalid)
return ItemCollection.from_dict(results_dict)

@property
def __geo_interface__(self) -> dict[str, Any]:
"""Implements the geo-interface protocol.
Expand Down
19 changes: 19 additions & 0 deletions tests/units/test_eoproduct.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import geojson
import responses
from lxml import html
from pystac import Item
from shapely import geometry

from eodag.config import PluginConfig
Expand Down Expand Up @@ -784,3 +785,21 @@ def test_eoproduct_serialize(self):
self.assertFalse(
any("mgrs" in ext for ext in prod_dict.get("stac_extensions", []))
)

def test_eoproduct_as_pystac_object(self):
"""eoproduct.as_pystac_object must return a pystac.Item"""
product = self._dummy_product(
properties={"id": "dummy_id", "datetime": "2021-01-01T00:00:00Z"}
)
pystac_item = product.as_pystac_object()
self.assertIsInstance(pystac_item, Item)
pystac_item.validate()

def test_eoproduct_from_pystac(self):
"""eoproduct.from_pystac must return an EOProduct instance from a pystac.Item"""
product = self._dummy_product(
properties={"id": "dummy_id", "datetime": "2021-01-01T00:00:00Z"}
)
pystac_item = Item.from_dict(product.as_dict())
product_from_pystac = EOProduct.from_pystac(pystac_item)
self.assertIsInstance(product_from_pystac, EOProduct)
12 changes: 12 additions & 0 deletions tests/units/test_search_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

import geojson
from lxml import html
from pystac import ItemCollection
from shapely.geometry.collection import GeometryCollection

from tests.context import EOProduct, SearchResult
Expand Down Expand Up @@ -73,6 +74,10 @@ def test_search_result_as_wkt_object(self):
self.assertIsInstance(wkt_object, str)
self.assertTrue(wkt_object.startswith("GEOMETRYCOLLECTION"))

def test_search_result_as_pystac_object(self):
pystac_object = self.search_result.as_pystac_object()
self.assertIsInstance(pystac_object, ItemCollection)

def test_search_result_is_list_like(self):
"""SearchResult must provide a list interface"""
self.assertIsInstance(self.search_result, UserList)
Expand All @@ -84,6 +89,13 @@ def test_search_result_from_feature_collection(self):
)
self.assertEqual(len(same_search_result), len(self.search_result))

def test_search_result_from_pystac(self):
"""SearchResult instances must be build-able from feature collection geojson"""
same_search_result = SearchResult.from_pystac(
ItemCollection.from_dict(self.search_result.as_dict())
)
self.assertEqual(len(same_search_result), len(self.search_result))

def test_search_result_repr_html(self):
"""SearchResult html repr must be correctly formatted"""
sr_repr = html.fromstring(self.search_result._repr_html_())
Expand Down
Loading