Skip to content

Commit 428cad0

Browse files
committed
feat: pagination using token
1 parent 9c52069 commit 428cad0

File tree

3 files changed

+69
-16
lines changed

3 files changed

+69
-16
lines changed

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ license = { file = "LICENSE" }
88
requires-python = ">= 3.9"
99
dependencies = [
1010
"attr",
11-
"eodag[all-providers] == 4.0.0a1",
11+
"eodag[all-providers] @ git+https://github.com/CS-SI/eodag.git@develop",
1212
"fastapi",
1313
"geojson",
1414
"geojson-pydantic",
@@ -65,7 +65,7 @@ explicit_package_bases = true
6565
exclude = ["tests", ".venv"]
6666

6767
[[tool.mypy.overrides]]
68-
module = ["pygeofilter", "pygeofilter.*", "stac_fastapi", "stac_fastapi.*"]
68+
module = ["geojson", "pygeofilter", "pygeofilter.*", "stac_fastapi", "stac_fastapi.*"]
6969
ignore_missing_imports = true
7070

7171
[tool.pytest.ini_options]

stac_fastapi/eodag/core.py

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,13 @@
4141
from stac_pydantic.links import Relations
4242
from stac_pydantic.shared import MimeTypes
4343

44-
from eodag import SearchResult
44+
from eodag import EOProduct, SearchResult
4545
from eodag.plugins.search.build_search_result import ECMWFSearch
4646
from eodag.utils import deepcopy, get_geometry_from_various
4747
from eodag.utils.exceptions import NoMatchingCollection as EodagNoMatchingCollection
4848
from stac_fastapi.eodag.client import CustomCoreClient
4949
from stac_fastapi.eodag.config import get_settings
50+
from stac_fastapi.eodag.constants import DEFAULT_ITEMS_PER_PAGE
5051
from stac_fastapi.eodag.cql_evaluate import EodagEvaluator
5152
from stac_fastapi.eodag.errors import NoMatchingCollection, ResponseSearchError
5253
from stac_fastapi.eodag.models.links import (
@@ -182,15 +183,18 @@ def _search_base(self, search_request: BaseSearchPostRequest, request: Request)
182183
else:
183184
raise HTTPException(status_code=400, detail="A collection is required")
184185

185-
# get products by ids
186186
if ids := eodag_args.pop("ids", []):
187+
# get products by ids
187188
search_result = SearchResult([])
188189
for item_id in ids:
189190
eodag_args["id"] = item_id
190191
search_result.extend(request.app.state.dag.search(validate=validate, **eodag_args))
191192
search_result.number_matched = len(search_result)
193+
elif eodag_args.get("token") and eodag_args.get("provider"):
194+
# search with pagination
195+
search_result = eodag_search_next_page(request.app.state.dag, eodag_args)
192196
else:
193-
# search without ids
197+
# search without ids or pagination
194198
search_result = request.app.state.dag.search(validate=validate, **eodag_args)
195199

196200
if search_result.errors and not len(search_result):
@@ -207,23 +211,22 @@ def _search_base(self, search_request: BaseSearchPostRequest, request: Request)
207211
)
208212
features.append(feature)
209213

210-
collection = ItemCollection(
214+
feature_collection = ItemCollection(
211215
type="FeatureCollection",
212216
features=features,
213217
numberMatched=search_result.number_matched,
214218
numberReturned=len(features),
215219
)
216220

217221
# pagination
218-
next_page = None
219-
if hasattr(search_result, "next_page_token_key"):
220-
next_page = search_result.next_page_token_key.split(":", 1)[1]
221-
222-
collection["links"] = PagingLinks(
222+
if "provider" not in request.state.eodag_args and len(search_result) > 0:
223+
request.state.eodag_args["provider"] = search_result[-1].provider
224+
feature_collection["links"] = PagingLinks(
223225
request=request,
224-
next=next_page,
226+
next=search_result.next_page_token,
227+
federation_backend=request.state.eodag_args.get("provider"),
225228
).get_links(request_json=request_json, extensions=extension_names)
226-
return collection
229+
return feature_collection
227230

228231
async def all_collections(
229232
self,
@@ -730,3 +733,36 @@ def add_error(error_message: str) -> None:
730733
raise ValidationError.from_exception_data(title="stac-fastapi-eodag", line_errors=errors)
731734

732735
return cql_args
736+
737+
738+
def eodag_search_next_page(dag, eodag_args):
739+
"""Perform an eodag search with pagination.
740+
741+
:param dag: The EODAG instance.
742+
:param eodag_args: The EODAG search arguments.
743+
:returns: The search result for the next page.
744+
"""
745+
eodag_args = eodag_args.copy()
746+
next_page_token = eodag_args.pop("token", None)
747+
provider = eodag_args.get("provider")
748+
if not next_page_token or not provider:
749+
raise HTTPException(
750+
status_code=500, detail="Missing required token and federation backend for next page search."
751+
)
752+
search_plugin = next(dag._plugins_manager.get_search_plugins(provider=provider))
753+
next_page_token_key = getattr(search_plugin.config, "pagination", {}).get("next_page_token_key", "page")
754+
eodag_args.pop("count", None)
755+
search_result = SearchResult(
756+
[EOProduct(provider, {"id": "_"})] * int(eodag_args.get("items_per_page", DEFAULT_ITEMS_PER_PAGE)),
757+
next_page_token=next_page_token,
758+
next_page_token_key=next_page_token_key,
759+
search_params=eodag_args,
760+
raise_errors=eodag_args.pop("raise_errors", None),
761+
)
762+
search_result._dag = dag
763+
try:
764+
search_result = next(search_result.next_page())
765+
except StopIteration:
766+
logger.info("StopIteration encountered during next page search.")
767+
search_result = SearchResult([])
768+
return search_result

stac_fastapi/eodag/models/links.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,14 @@
2121
from urllib.parse import ParseResult, parse_qs, unquote, urlencode, urljoin, urlparse
2222

2323
import attr
24+
import geojson
2425
from stac_fastapi.types.requests import get_base_url
2526
from stac_pydantic.links import Relations
2627
from stac_pydantic.shared import MimeTypes
2728
from starlette.requests import Request
2829

30+
from eodag.utils import update_nested_dict
31+
2932
# These can be inferred from the item/collection so they aren't included in the database
3033
# Instead they are dynamically generated when querying the database using the classes defined below
3134
INFERRED_LINK_RELS = ["self", "item", "collection"]
@@ -139,14 +142,25 @@ def get_links(
139142
class PagingLinks(BaseLinks):
140143
"""Create links for paging."""
141144

142-
next: Optional[int] = attr.ib(kw_only=True, default=None)
145+
next: Optional[str] = attr.ib(kw_only=True, default=None)
146+
federation_backend: Optional[str] = attr.ib(kw_only=True, default=None)
143147

144148
def link_next(self) -> Optional[dict[str, Any]]:
145149
"""Create link for next page."""
146150
if self.next is not None:
147151
method = self.request.method
152+
federation_backend_dict = (
153+
{"query": {"federation:backends": {"eq": self.federation_backend}}} if self.federation_backend else {}
154+
)
148155
if method == "GET":
149-
href = merge_params(self.url, {"token": [str(self.next)]})
156+
params_update_dict: dict[str, list[str]] = {"token": [str(self.next)]}
157+
if "query" in self.request.query_params:
158+
params_update_dict["query"] = [
159+
geojson.dumps(
160+
geojson.loads(self.request.query_params["query"]) | federation_backend_dict["query"]
161+
)
162+
]
163+
href = merge_params(self.url, params_update_dict)
150164
return {
151165
"rel": Relations.next.value,
152166
"type": MimeTypes.geojson.value,
@@ -155,12 +169,15 @@ def link_next(self) -> Optional[dict[str, Any]]:
155169
"title": "Next page",
156170
}
157171
if method == "POST":
172+
post_body = update_nested_dict(
173+
self.request.state.postbody, {"token": self.next} | federation_backend_dict
174+
)
158175
return {
159176
"rel": Relations.next,
160177
"type": MimeTypes.geojson,
161178
"method": method,
162179
"href": f"{self.request.url}",
163-
"body": {**self.request.state.postbody, "page": self.next},
180+
"body": post_body,
164181
"title": "Next page",
165182
}
166183

0 commit comments

Comments
 (0)