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: 1 addition & 1 deletion stac_fastapi/eodag/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ async def _search_base(self, search_request: BaseSearchPostRequest, request: Req
extension_names = [type(ext).__name__ for ext in self.extensions]

for product in search_result:
feature = create_stac_item(product, self.extension_is_enabled, request, extension_names, request_json)
feature = create_stac_item(product, request, extension_names, request_json)
features.append(feature)

feature_collection = ItemCollection(
Expand Down
6 changes: 1 addition & 5 deletions stac_fastapi/eodag/extensions/collection_order.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,6 @@ class BaseCollectionOrderClient:
stac_metadata_model: type[BaseModel] = attr.ib(default=CommonStacMetadata)
extensions: list[ApiExtension] = attr.ib(default=[])

def extension_is_enabled(self, extension: str) -> bool:
"""Check if an api extension is enabled."""
return any(type(ext).__name__ == extension for ext in self.extensions)

def order_collection(
self,
collection_id: str,
Expand Down Expand Up @@ -135,7 +131,7 @@ def order_collection(
)
extension_names = [type(ext).__name__ for ext in self.extensions]

return create_stac_item(product, self.extension_is_enabled, request, extension_names)
return create_stac_item(product, request, extension_names)


@attr.s
Expand Down
162 changes: 64 additions & 98 deletions stac_fastapi/eodag/models/item.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,28 +17,26 @@
# limitations under the License.
"""stac item."""

from typing import Any, Callable, Optional
from typing import Any, Optional
from urllib.parse import quote, unquote_plus, urlparse

import orjson
from fastapi import Request
from stac_fastapi.types.errors import NotFoundError
from stac_fastapi.types.requests import get_base_url
from stac_fastapi.types.stac import Item
from stac_pydantic.api.version import STAC_API_VERSION
from stac_pydantic.shared import Asset

from eodag.api.product._product import EOProduct
from eodag.api.product.metadata_mapping import OFFLINE_STATUS, ONLINE_STATUS
from eodag.api.product.metadata_mapping import ONLINE_STATUS
from eodag.utils import deepcopy, guess_file_type
from stac_fastapi.eodag.config import Settings, get_settings
from stac_fastapi.eodag.errors import MisconfiguredError
from stac_fastapi.eodag.models.links import ItemLinks


def _get_retrieve_body_for_order(product: EOProduct) -> dict[str, Any]:
"""returns the body of the request used to order a product"""
parts = urlparse(product.properties["eodag:order_link"])
def _get_retrieve_body_for_order(order_link: str) -> dict[str, Any]:
"""Return the body of the request used to order a product."""
parts = urlparse(order_link)
keys = ["request", "inputs", "location"] # keys used by different providers
request_dict = orjson.loads(parts.query)
retrieve_body = None
Expand All @@ -57,7 +55,6 @@ def _get_retrieve_body_for_order(product: EOProduct) -> dict[str, Any]:

def create_stac_item(
product: EOProduct,
extension_is_enabled: Callable[[str], bool],
request: Request,
extension_names: Optional[list[str]],
request_json: Optional[Any] = None,
Expand All @@ -68,118 +65,87 @@ def create_stac_item(

settings: Settings = get_settings()

collection_obj = request.app.state.dag.collections_config.get(product.collection)
collection = collection_obj.id if collection_obj else product.collection

feature = Item(
type="Feature",
assets={},
id=product.properties["id"],
geometry=product.geometry.__geo_interface__,
bbox=product.geometry.bounds,
collection=collection,
stac_version=STAC_API_VERSION,
)
feature = product.as_dict()
properties = feature["properties"]
assets = feature["assets"]
provider = properties["federation:backends"][0]
collection = feature["collection"]
enabled_extensions = set(extension_names or [])

download_base_url = settings.download_base_url
if not download_base_url:
download_base_url = get_base_url(request)
download_base_url = settings.download_base_url or get_base_url(request)

quoted_id = quote(feature["id"])
asset_proxy_url = (
(download_base_url + f"data/{product.provider}/{collection}/{quoted_id}")
if extension_is_enabled("DataDownload")
(download_base_url + f"data/{provider}/{collection}/{quoted_id}")
if "DataDownload" in enabled_extensions
else None
)

settings = get_settings()
auto_order_whitelist = settings.auto_order_whitelist
if product.provider in auto_order_whitelist:
if provider in auto_order_whitelist:
# a product from a whitelisted federation backend is considered as online
product.properties["order:status"] = ONLINE_STATUS

# create assets only if product is not offline
if (
product.properties.get("order:status", ONLINE_STATUS) != OFFLINE_STATUS
or product.provider in auto_order_whitelist
):
for k, v in product.assets.items():
# TODO: download extension with origin link (make it optional ?)
asset_model = Asset.model_validate(v)
feature["assets"][k] = asset_model.model_dump(exclude_none=True)

if asset_proxy_url:
origin = deepcopy(feature["assets"][k])
quoted_key = quote(k)
feature["assets"][k]["href"] = asset_proxy_url + "/" + quoted_key

origin_href = origin.get("href")
if (
settings.keep_origin_url
and origin_href
and not origin_href.startswith(tuple(settings.origin_url_blacklist))
):
feature["assets"][k]["alternate"] = {"origin": origin}

# TODO: remove downloadLink asset after EODAG assets rework
if (download_link := product.properties.get("eodag:download_link")) and not any(
key.endswith(".parquet") for key in product.assets
):
origin_href = download_link
if asset_proxy_url:
download_link = asset_proxy_url + "/downloadLink"

mime_type = guess_file_type(origin_href) or "application/octet-stream"

feature["assets"]["downloadLink"] = {
"title": "Download link",
"href": download_link,
# TODO: download link is not always a ZIP archive
"type": mime_type,
properties["order:status"] = ONLINE_STATUS

keep_origin_url = settings.keep_origin_url
origin_url_blacklist = tuple(settings.origin_url_blacklist)

if asset_proxy_url:
for asset_name, asset in assets.items():
should_keep_origin = keep_origin_url and not asset["href"].startswith(origin_url_blacklist)
origin = deepcopy(asset) if should_keep_origin else None

asset["href"] = f"{asset_proxy_url}/{quote(asset_name)}"
asset.pop("storage:refs", None)

if origin:
asset["alternate"] = {"origin": origin}

# TODO: remove downloadLink asset after EODAG assets rework
has_parquet_asset = any(asset_name.endswith(".parquet") for asset_name in assets)
if (download_link := properties.get("eodag:download_link")) and not has_parquet_asset:
origin_href = download_link
proxied_href = f"{asset_proxy_url}/downloadLink" if asset_proxy_url else origin_href
mime_type = guess_file_type(origin_href) or "application/octet-stream"

download_asset = {"title": "Download link", "href": proxied_href, "type": mime_type, "roles": ["data"]}

if asset_proxy_url and keep_origin_url and not origin_href.startswith(origin_url_blacklist):
download_asset["alternate"] = {
"origin": {
"title": "Origin asset link",
"href": origin_href,
"type": mime_type,
},
}

if settings.keep_origin_url and not origin_href.startswith(tuple(settings.origin_url_blacklist)):
feature["assets"]["downloadLink"]["alternate"] = {
"origin": {
"title": "Origin asset link",
"href": origin_href,
# TODO: download link is not always a ZIP archive
"type": mime_type,
},
}

product_dict = product.as_dict()
assets["downloadLink"] = download_asset

# filter properties we do not want to expose
feature["properties"] = {k: v for k, v in product_dict["properties"].items() if not k.startswith("eodag:")}
feature["properties"] = {k: v for k, v in properties.items() if not k.startswith("eodag:")}
feature["properties"].pop("qs", None)

feature["stac_extensions"] = product_dict["stac_extensions"]

if extension_names and product.provider not in auto_order_whitelist:
if "CollectionOrderExtension" in extension_names and (
not product.properties.get("eodag:order_link", False)
or feature["properties"].get("order:status", "") != "orderable"
):
extension_names.remove("CollectionOrderExtension")
else:
extension_names = []
link_extensions = list(enabled_extensions)
if "CollectionOrderExtension" in enabled_extensions:
is_orderable = (
bool(properties.get("eodag:order_link")) and feature["properties"].get("order:status") == "orderable"
)
if provider in auto_order_whitelist or not is_orderable:
link_extensions.remove("CollectionOrderExtension")

# get request body for retrieve link (if product has to be ordered)
if "eodag:order_link" in product.properties:
retrieve_body = _get_retrieve_body_for_order(product)
else:
retrieve_body = {}
retrieve_body = (
_get_retrieve_body_for_order(order_link) if (order_link := properties.get("eodag:order_link")) else {}
)

if eodag_args := getattr(request.state, "eodag_args", None):
if provider := eodag_args.get("provider", None):
retrieve_body["federation:backends"] = [provider]
collection_obj = request.app.state.dag.collections_config.get(product.collection)
collection_title = (collection_obj.title if collection_obj else None) or collection

feature["links"] = ItemLinks(
collection_id=collection,
collection_title=collection_obj.title if collection_obj else collection,
collection_title=collection_title,
item_id=quoted_id,
retrieve_body=retrieve_body,
request=request,
).get_links(extensions=extension_names, extra_links=feature.get("links"), request_json=request_json)
).get_links(extensions=link_extensions, extra_links=feature.get("links"), request_json=request_json)

return feature
4 changes: 2 additions & 2 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading