Skip to content
Open
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
58 changes: 0 additions & 58 deletions stac_fastapi/eodag/api.py

This file was deleted.

171 changes: 74 additions & 97 deletions stac_fastapi/eodag/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@

import asyncio
import logging
import re
from typing import TYPE_CHECKING, Any, cast
from urllib.parse import unquote_plus

import attr
import orjson
from fastapi import HTTPException
from fastapi.responses import StreamingResponse
from pydantic import ValidationError
from pydantic_core import InitErrorDetails, PydanticCustomError
from pygeofilter.backends.cql2_json import to_cql2
Expand All @@ -50,17 +50,14 @@
from stac_fastapi.eodag.constants import DEFAULT_ITEMS_PER_PAGE
from stac_fastapi.eodag.cql_evaluate import EodagEvaluator
from stac_fastapi.eodag.errors import NoMatchingCollection, ResponseSearchError
from stac_fastapi.eodag.models.item import create_stac_item
from stac_fastapi.eodag.models.links import (
CollectionLinks,
CollectionSearchPagingLinks,
ItemCollectionLinks,
PagingLinks,
)
from stac_fastapi.eodag.models.stac_metadata import (
CommonStacMetadata,
create_stac_item,
get_sortby_to_post,
)
from stac_fastapi.eodag.models.stac_metadata import CommonStacMetadata
from stac_fastapi.eodag.utils import (
check_poly_is_point,
dt_range_to_eodag,
Expand Down Expand Up @@ -371,10 +368,11 @@ async def item_collection(
bbox: Optional[list[NumType]] = None,
datetime: Optional[str] = None,
limit: Optional[int] = None,
page: Optional[str] = None,
# extensions
sortby: Optional[list[str]] = None,
filter_expr: Optional[str] = None,
filter_lang: Optional[str] = "cql2-text",
token: Optional[str] = None,
**kwargs: Any,
) -> ItemCollection:
"""
Expand All @@ -387,10 +385,10 @@ async def item_collection(
:param bbox: Bounding box to filter the items.
:param datetime: Date and time range to filter the items.
:param limit: Maximum number of items to return.
:param page: Page token for pagination.
:param sortby: List of fields to sort the results by.
:param filter_expr: CQL filter to apply to the search.
:param filter_lang: Language of the filter (default is "cql2-text").
:param token: Page token for pagination.
:param kwargs: Additional arguments.
:returns: An ItemCollection.
:raises NotFoundError: If the collection does not exist.
Expand All @@ -403,20 +401,10 @@ async def item_collection(
"bbox": bbox,
"datetime": datetime,
"limit": limit,
"page": page,
"token": token,
}

if sortby:
sortby_converted = get_sortby_to_post(sortby)
base_args["sortby"] = cast(Any, sortby_converted)

if filter_expr:
add_filter_to_args(base_args, filter_lang, filter_expr)

clean = {}
for k, v in base_args.items():
if v is not None and v != []:
clean[k] = v
clean = self._clean_search_args(base_args, sortby=sortby, filter_expr=filter_expr, filter_lang=filter_lang)

search_request = self.post_request_model.model_validate(clean)
item_collection = self._search_base(search_request, request)
Expand Down Expand Up @@ -444,12 +432,12 @@ def get_search(
collections: Optional[list[str]] = None,
ids: Optional[list[str]] = None,
bbox: Optional[list[NumType]] = None,
intersects: Optional[str] = None,
datetime: Optional[str] = None,
limit: Optional[int] = None,
# Extensions
query: Optional[str] = None,
page: Optional[str] = None,
sortby: Optional[list[str]] = None,
intersects: Optional[str] = None,
filter_expr: Optional[str] = None,
filter_lang: Optional[str] = "cql2-text",
token: Optional[str] = None,
Expand All @@ -462,14 +450,14 @@ def get_search(
:param collections: List of collection IDs to include in the search.
:param ids: List of item IDs to include in the search.
:param bbox: Bounding box to filter the search.
:param intersects: GeoJSON geometry to filter the search.
:param datetime: Date and time range to filter the search.
:param limit: Maximum number of items to return.
:param query: Query string to filter the search.
:param page: Page token for pagination.
:param sortby: List of fields to sort the results by.
:param intersects: GeoJSON geometry to filter the search.
:param filter_expr: CQL filter to apply to the search.
:param filter_lang: Language of the filter (default is "cql2-text").
:param filter_lang: Language of the filter.
:param token: Page token for pagination.
:param kwargs: Additional arguments.
:returns: Found items.
:raises HTTPException: If the provided parameters are invalid.
Expand All @@ -479,23 +467,18 @@ def get_search(
"ids": ids,
"bbox": bbox,
"limit": limit,
"query": orjson.loads(unquote_plus(query)) if query else query,
"token": token,
"sortby": get_sortby_to_post(sortby),
"intersects": orjson.loads(unquote_plus(intersects)) if intersects else intersects,
}

if datetime:
base_args["datetime"] = format_datetime_range(datetime)

if filter_expr:
add_filter_to_args(base_args, filter_lang, filter_expr)

# Remove None values from dict
clean = {}
for k, v in base_args.items():
if v is not None and v != []:
clean[k] = v
clean = self._clean_search_args(
base_args,
intersects=intersects,
datetime=datetime,
sortby=sortby,
query=query,
filter_expr=filter_expr,
filter_lang=filter_lang,
)

try:
search_request = self.post_request_model(**clean)
Expand Down Expand Up @@ -525,40 +508,53 @@ async def get_item(self, item_id: str, collection_id: str, request: Request, **k

return Item(**item_collection["features"][0])

async def download_item(self, item_id: str, collection_id: str, request: Request, **kwargs) -> StreamingResponse:
"""
Download item by ID.
def _clean_search_args(
self,
base_args: dict[str, Any],
intersects: Optional[str] = None,
datetime: Optional[str] = None,
sortby: Optional[str] = None,
query: Optional[str] = None,
filter_expr: Optional[str] = None,
filter_lang: Optional[str] = None,
**kwargs: Any,
) -> dict[str, Any]:
"""Clean up search arguments to match format expected by pgstac"""
if filter_expr:
if filter_lang == "cql2-text":
filter_expr = to_cql2(parse_cql2_text(filter_expr))
filter_lang = "cql2-json"

:param item_id: ID of the item.
:param collection_id: ID of the collection.
:param request: The request object.
:param kwargs: Additional arguments.
:returns: Streaming response for the item download.
"""
product: EOProduct
product, _ = request.app.state.dag.search({"collection": collection_id, "id": item_id})[0]

# when could this really happen ?
if not product.downloader:
download_plugin = request.app.state.dag._plugins_manager.get_download_plugin(product)
auth_plugin = request.app.state.dag._plugins_manager.get_auth_plugin(download_plugin.provider)
product.register_downloader(download_plugin, auth_plugin)

# required for auth. Can be removed when EODAG implements the auth interface
auth = (
product.downloader_auth.authenticate() if product.downloader_auth is not None else product.downloader_auth
)
base_args["filter"] = str2json("filter_expr", filter_expr)
base_args["filter_lang"] = "cql2-json"

if product.downloader is None:
raise HTTPException(status_code=500, detail="No downloader found for this product")
# can we make something more clean here ?
download_stream_dict = product.downloader._stream_download_dict(product, auth=auth)
if datetime:
base_args["datetime"] = format_datetime_range(datetime)

return StreamingResponse(
content=download_stream_dict.content,
headers=download_stream_dict.headers,
media_type=download_stream_dict.media_type,
)
if query:
base_args["query"] = orjson.loads(unquote_plus(query))

if intersects:
base_args["intersects"] = orjson.loads(unquote_plus(intersects))

if sortby:
sort_param = []
for sort in sortby:
sortparts = re.match(r"^([+-]?)(.*)$", sort)
if sortparts:
sort_param.append({
"field": sortparts.group(2).strip(),
"direction": "desc" if sortparts.group(1) == "-" else "asc",
})
base_args["sortby"] = sort_param

# Remove None values from dict
clean = {}
for k, v in base_args.items():
if v is not None and v != []:
clean[k] = v

return clean


def prepare_search_base_args(search_request: BaseSearchPostRequest, model: type[CommonStacMetadata]) -> dict[str, Any]:
Expand Down Expand Up @@ -598,15 +594,13 @@ def prepare_search_base_args(search_request: BaseSearchPostRequest, model: type[
param_tuples = []
for param in sortby:
dumped_param = param.model_dump(mode="json")
param_tuples.append(
(
sort_by_special_fields.get(
model.to_eodag(dumped_param["field"]),
model.to_eodag(dumped_param["field"]),
),
dumped_param["direction"],
)
)
param_tuples.append((
sort_by_special_fields.get(
model.to_eodag(dumped_param["field"]),
model.to_eodag(dumped_param["field"]),
),
dumped_param["direction"],
))
sort_by["sort_by"] = param_tuples

eodag_query = {}
Expand Down Expand Up @@ -749,9 +743,7 @@ def eodag_search_next_page(dag, eodag_args):
next_page_token = eodag_args.pop("token", None)
provider = eodag_args.get("provider")
if not next_page_token or not provider:
raise HTTPException(
status_code=500, detail="Missing required token and federation backend for next page search."
)
raise ValueError("Missing required token and federation backend for next page search.")
search_plugin = next(dag._plugins_manager.get_search_plugins(provider=provider))
next_page_token_key = getattr(search_plugin.config, "pagination", {}).get("next_page_token_key", "page")
eodag_args.pop("count", None)
Expand All @@ -769,18 +761,3 @@ def eodag_search_next_page(dag, eodag_args):
logger.info("StopIteration encountered during next page search.")
search_result = SearchResult([])
return search_result


def add_filter_to_args(base_args: dict[str, Any], filter_lang: Optional[str], filter_expr: Optional[str]):
"""Parse the filter from the query and add to arguments

:param base_args:
:param filter_expr: CQL filter to apply to the search.
:param filter_lang: Language of the filter (default is "cql2-text").
"""
if filter_lang == "cql2-text":
filter_expr = to_cql2(parse_cql2_text(filter_expr))
filter_lang = "cql2-json"

base_args["filter"] = str2json("filter_expr", filter_expr)
base_args["filter_lang"] = "cql2-json"
6 changes: 2 additions & 4 deletions stac_fastapi/eodag/extensions/collection_order.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,8 @@

from stac_fastapi.eodag.config import get_settings
from stac_fastapi.eodag.errors import ResponseSearchError
from stac_fastapi.eodag.models.stac_metadata import (
CommonStacMetadata,
create_stac_item,
)
from stac_fastapi.eodag.models.item import create_stac_item
from stac_fastapi.eodag.models.stac_metadata import CommonStacMetadata

logger = logging.getLogger(__name__)

Expand Down
Loading
Loading