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
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ license = { file = "LICENSE" }
requires-python = ">= 3.9"
dependencies = [
"attr",
"eodag[all-providers] == 4.0.0a1",
"eodag[all-providers] @ git+https://github.com/CS-SI/eodag.git@develop",
"fastapi",
"geojson",
"geojson-pydantic",
Expand Down Expand Up @@ -65,7 +65,7 @@ explicit_package_bases = true
exclude = ["tests", ".venv"]

[[tool.mypy.overrides]]
module = ["pygeofilter", "pygeofilter.*", "stac_fastapi", "stac_fastapi.*"]
module = ["geojson", "pygeofilter", "pygeofilter.*", "stac_fastapi", "stac_fastapi.*"]
ignore_missing_imports = true

[tool.pytest.ini_options]
Expand Down
6 changes: 3 additions & 3 deletions stac_fastapi/eodag/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
SortExtension,
)
from stac_fastapi.extensions.core.free_text import FreeTextConformanceClasses
from stac_fastapi.extensions.core.pagination.token_pagination import TokenPaginationExtension
from stac_fastapi.extensions.core.query import QueryConformanceClasses
from stac_fastapi.extensions.core.sort import SortConformanceClasses

Expand All @@ -60,7 +61,6 @@
from stac_fastapi.eodag.extensions.ecmwf import EcmwfExtension
from stac_fastapi.eodag.extensions.filter import FiltersClient
from stac_fastapi.eodag.extensions.offset_pagination import OffsetPaginationExtension
from stac_fastapi.eodag.extensions.pagination import PaginationExtension
from stac_fastapi.eodag.extensions.stac import (
ElectroOpticalExtension,
FederationExtension,
Expand Down Expand Up @@ -107,7 +107,7 @@
"query": QueryExtension(),
"sort": SortExtension(),
"filter": FilterExtension(client=FiltersClient(stac_metadata_model=stac_metadata_model)),
"pagination": PaginationExtension(),
"token": TokenPaginationExtension(),
}

# collection_search extensions
Expand All @@ -120,7 +120,7 @@

# item_collection extensions
itm_col_extensions_map = {
"pagination": PaginationExtension(),
"token": TokenPaginationExtension(),
"sort": SortExtension(conformance_classes=[SortConformanceClasses.ITEMS]),
}

Expand Down
69 changes: 50 additions & 19 deletions stac_fastapi/eodag/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,13 @@
from stac_pydantic.links import Relations
from stac_pydantic.shared import MimeTypes

from eodag import SearchResult
from eodag.api.core import DEFAULT_ITEMS_PER_PAGE
from eodag import EOProduct, SearchResult
from eodag.plugins.search.build_search_result import ECMWFSearch
from eodag.utils import deepcopy, get_geometry_from_various
from eodag.utils.exceptions import NoMatchingCollection as EodagNoMatchingCollection
from stac_fastapi.eodag.client import CustomCoreClient
from stac_fastapi.eodag.config import get_settings
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.links import (
Expand Down Expand Up @@ -183,15 +183,18 @@ def _search_base(self, search_request: BaseSearchPostRequest, request: Request)
else:
raise HTTPException(status_code=400, detail="A collection is required")

# get products by ids
if ids := eodag_args.pop("ids", []):
# get products by ids
search_result = SearchResult([])
for item_id in ids:
eodag_args["id"] = item_id
search_result.extend(request.app.state.dag.search(validate=validate, **eodag_args))
search_result.number_matched = len(search_result)
elif eodag_args.get("token") and eodag_args.get("provider"):
# search with pagination
search_result = eodag_search_next_page(request.app.state.dag, eodag_args)
else:
# search without ids
# search without ids or pagination
search_result = request.app.state.dag.search(validate=validate, **eodag_args)

if search_result.errors and not len(search_result):
Expand All @@ -208,28 +211,22 @@ def _search_base(self, search_request: BaseSearchPostRequest, request: Request)
)
features.append(feature)

collection = ItemCollection(
feature_collection = ItemCollection(
type="FeatureCollection",
features=features,
numberMatched=search_result.number_matched,
numberReturned=len(features),
)

# pagination
next_page = None
if search_request.page:
number_returned = len(search_result)
items_per_page = search_request.limit or DEFAULT_ITEMS_PER_PAGE
if not search_result.number_matched or (
(search_request.page - 1) * items_per_page + number_returned < search_result.number_matched
):
next_page = search_request.page + 1

collection["links"] = PagingLinks(
if "provider" not in request.state.eodag_args and len(search_result) > 0:
request.state.eodag_args["provider"] = search_result[-1].provider
feature_collection["links"] = PagingLinks(
request=request,
next=next_page,
next=search_result.next_page_token,
federation_backend=request.state.eodag_args.get("provider"),
).get_links(request_json=request_json, extensions=extension_names)
return collection
return feature_collection

async def all_collections(
self,
Expand Down Expand Up @@ -447,6 +444,7 @@ def get_search(
intersects: Optional[str] = None,
filter_expr: Optional[str] = None,
filter_lang: Optional[str] = "cql2-text",
token: Optional[str] = None,
**kwargs: Any,
) -> ItemCollection:
"""
Expand Down Expand Up @@ -474,7 +472,7 @@ def get_search(
"bbox": bbox,
"limit": limit,
"query": orjson.loads(unquote_plus(query)) if query else query,
"page": page,
"token": token,
"sortby": get_sortby_to_post(sortby),
"intersects": orjson.loads(unquote_plus(intersects)) if intersects else intersects,
}
Expand Down Expand Up @@ -569,7 +567,7 @@ def prepare_search_base_args(search_request: BaseSearchPostRequest, model: type[
"""
base_args = (
{
"page": search_request.page,
"token": search_request.token,
"items_per_page": search_request.limit,
"raise_errors": False,
"count": get_settings().count,
Expand Down Expand Up @@ -735,3 +733,36 @@ def add_error(error_message: str) -> None:
raise ValidationError.from_exception_data(title="stac-fastapi-eodag", line_errors=errors)

return cql_args


def eodag_search_next_page(dag, eodag_args):
"""Perform an eodag search with pagination.

:param dag: The EODAG instance.
:param eodag_args: The EODAG search arguments.
:returns: The search result for the next page.
"""
eodag_args = eodag_args.copy()
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."
)
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)
search_result = SearchResult(
[EOProduct(provider, {"id": "_"})] * int(eodag_args.get("items_per_page", DEFAULT_ITEMS_PER_PAGE)),
next_page_token=next_page_token,
next_page_token_key=next_page_token_key,
search_params=eodag_args,
raise_errors=eodag_args.pop("raise_errors", None),
)
search_result._dag = dag
try:
search_result = next(search_result.next_page())
except StopIteration:
logger.info("StopIteration encountered during next page search.")
search_result = SearchResult([])
return search_result
23 changes: 20 additions & 3 deletions stac_fastapi/eodag/models/links.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,14 @@
from urllib.parse import ParseResult, parse_qs, unquote, urlencode, urljoin, urlparse

import attr
import geojson
from stac_fastapi.types.requests import get_base_url
from stac_pydantic.links import Relations
from stac_pydantic.shared import MimeTypes
from starlette.requests import Request

from eodag.utils import update_nested_dict

# These can be inferred from the item/collection so they aren't included in the database
# Instead they are dynamically generated when querying the database using the classes defined below
INFERRED_LINK_RELS = ["self", "item", "collection"]
Expand Down Expand Up @@ -139,14 +142,25 @@ def get_links(
class PagingLinks(BaseLinks):
"""Create links for paging."""

next: Optional[int] = attr.ib(kw_only=True, default=None)
next: Optional[str] = attr.ib(kw_only=True, default=None)
federation_backend: Optional[str] = attr.ib(kw_only=True, default=None)

def link_next(self) -> Optional[dict[str, Any]]:
"""Create link for next page."""
if self.next is not None:
method = self.request.method
federation_backend_dict = (
{"query": {"federation:backends": {"eq": self.federation_backend}}} if self.federation_backend else {}
)
if method == "GET":
href = merge_params(self.url, {"page": [str(self.next)]})
params_update_dict: dict[str, list[str]] = {"token": [str(self.next)]}
if "query" in self.request.query_params:
params_update_dict["query"] = [
geojson.dumps(
geojson.loads(self.request.query_params["query"]) | federation_backend_dict["query"]
)
]
href = merge_params(self.url, params_update_dict)
return {
"rel": Relations.next.value,
"type": MimeTypes.geojson.value,
Expand All @@ -155,12 +169,15 @@ def link_next(self) -> Optional[dict[str, Any]]:
"title": "Next page",
}
if method == "POST":
post_body = update_nested_dict(
self.request.state.postbody, {"token": self.next} | federation_backend_dict
)
return {
"rel": Relations.next,
"type": MimeTypes.geojson,
"method": method,
"href": f"{self.request.url}",
"body": {**self.request.state.postbody, "page": self.next},
"body": post_body,
"title": "Next page",
}

Expand Down
Loading
Loading