2121
2222import asyncio
2323import logging
24+ import re
2425from typing import TYPE_CHECKING , Any , cast
2526from urllib .parse import unquote_plus
2627
2728import attr
2829import orjson
2930from fastapi import HTTPException
30- from fastapi .responses import StreamingResponse
3131from pydantic import ValidationError
3232from pydantic_core import InitErrorDetails , PydanticCustomError
3333from pygeofilter .backends .cql2_json import to_cql2
5252from stac_fastapi .eodag .constants import DEFAULT_ITEMS_PER_PAGE
5353from stac_fastapi .eodag .cql_evaluate import EodagEvaluator
5454from stac_fastapi .eodag .errors import NoMatchingCollection , ResponseSearchError
55+ from stac_fastapi .eodag .models .item import create_stac_item
5556from stac_fastapi .eodag .models .links import (
5657 CollectionLinks ,
5758 CollectionSearchPagingLinks ,
5859 ItemCollectionLinks ,
5960 PagingLinks ,
6061)
61- from stac_fastapi .eodag .models .stac_metadata import (
62- CommonStacMetadata ,
63- create_stac_item ,
64- get_sortby_to_post ,
65- )
62+ from stac_fastapi .eodag .models .stac_metadata import CommonStacMetadata
6663from stac_fastapi .eodag .utils import (
6764 check_poly_is_point ,
6865 dt_range_to_eodag ,
@@ -375,10 +372,11 @@ async def item_collection(
375372 bbox : Optional [list [NumType ]] = None ,
376373 datetime : Optional [str ] = None ,
377374 limit : Optional [int ] = None ,
378- page : Optional [ str ] = None ,
375+ # extensions
379376 sortby : Optional [list [str ]] = None ,
380377 filter_expr : Optional [str ] = None ,
381378 filter_lang : Optional [str ] = "cql2-text" ,
379+ token : Optional [str ] = None ,
382380 ** kwargs : Any ,
383381 ) -> ItemCollection :
384382 """
@@ -391,10 +389,10 @@ async def item_collection(
391389 :param bbox: Bounding box to filter the items.
392390 :param datetime: Date and time range to filter the items.
393391 :param limit: Maximum number of items to return.
394- :param page: Page token for pagination.
395392 :param sortby: List of fields to sort the results by.
396393 :param filter_expr: CQL filter to apply to the search.
397394 :param filter_lang: Language of the filter (default is "cql2-text").
395+ :param token: Page token for pagination.
398396 :param kwargs: Additional arguments.
399397 :returns: An ItemCollection.
400398 :raises NotFoundError: If the collection does not exist.
@@ -407,20 +405,10 @@ async def item_collection(
407405 "bbox" : bbox ,
408406 "datetime" : datetime ,
409407 "limit" : limit ,
410- "page " : page ,
408+ "token " : token ,
411409 }
412410
413- if sortby :
414- sortby_converted = get_sortby_to_post (sortby )
415- base_args ["sortby" ] = cast (Any , sortby_converted )
416-
417- if filter_expr :
418- add_filter_to_args (base_args , filter_lang , filter_expr )
419-
420- clean = {}
421- for k , v in base_args .items ():
422- if v is not None and v != []:
423- clean [k ] = v
411+ clean = self ._clean_search_args (base_args , sortby = sortby , filter_expr = filter_expr , filter_lang = filter_lang )
424412
425413 search_request = self .post_request_model .model_validate (clean )
426414 item_collection = self ._search_base (search_request , request )
@@ -448,12 +436,12 @@ def get_search(
448436 collections : Optional [list [str ]] = None ,
449437 ids : Optional [list [str ]] = None ,
450438 bbox : Optional [list [NumType ]] = None ,
439+ intersects : Optional [str ] = None ,
451440 datetime : Optional [str ] = None ,
452441 limit : Optional [int ] = None ,
442+ # Extensions
453443 query : Optional [str ] = None ,
454- page : Optional [str ] = None ,
455444 sortby : Optional [list [str ]] = None ,
456- intersects : Optional [str ] = None ,
457445 filter_expr : Optional [str ] = None ,
458446 filter_lang : Optional [str ] = "cql2-text" ,
459447 token : Optional [str ] = None ,
@@ -466,14 +454,14 @@ def get_search(
466454 :param collections: List of collection IDs to include in the search.
467455 :param ids: List of item IDs to include in the search.
468456 :param bbox: Bounding box to filter the search.
457+ :param intersects: GeoJSON geometry to filter the search.
469458 :param datetime: Date and time range to filter the search.
470459 :param limit: Maximum number of items to return.
471460 :param query: Query string to filter the search.
472- :param page: Page token for pagination.
473461 :param sortby: List of fields to sort the results by.
474- :param intersects: GeoJSON geometry to filter the search.
475462 :param filter_expr: CQL filter to apply to the search.
476- :param filter_lang: Language of the filter (default is "cql2-text").
463+ :param filter_lang: Language of the filter.
464+ :param token: Page token for pagination.
477465 :param kwargs: Additional arguments.
478466 :returns: Found items.
479467 :raises HTTPException: If the provided parameters are invalid.
@@ -483,23 +471,18 @@ def get_search(
483471 "ids" : ids ,
484472 "bbox" : bbox ,
485473 "limit" : limit ,
486- "query" : orjson .loads (unquote_plus (query )) if query else query ,
487474 "token" : token ,
488- "sortby" : get_sortby_to_post (sortby ),
489- "intersects" : orjson .loads (unquote_plus (intersects )) if intersects else intersects ,
490475 }
491476
492- if datetime :
493- base_args ["datetime" ] = format_datetime_range (datetime )
494-
495- if filter_expr :
496- add_filter_to_args (base_args , filter_lang , filter_expr )
497-
498- # Remove None values from dict
499- clean = {}
500- for k , v in base_args .items ():
501- if v is not None and v != []:
502- clean [k ] = v
477+ clean = self ._clean_search_args (
478+ base_args ,
479+ intersects = intersects ,
480+ datetime = datetime ,
481+ sortby = sortby ,
482+ query = query ,
483+ filter_expr = filter_expr ,
484+ filter_lang = filter_lang ,
485+ )
503486
504487 try :
505488 search_request = self .post_request_model (** clean )
@@ -529,40 +512,55 @@ async def get_item(self, item_id: str, collection_id: str, request: Request, **k
529512
530513 return Item (** item_collection ["features" ][0 ])
531514
532- async def download_item (self , item_id : str , collection_id : str , request : Request , ** kwargs ) -> StreamingResponse :
533- """
534- Download item by ID.
515+ def _clean_search_args (
516+ self ,
517+ base_args : dict [str , Any ],
518+ intersects : Optional [str ] = None ,
519+ datetime : Optional [str ] = None ,
520+ sortby : Optional [list [str ]] = None ,
521+ query : Optional [str ] = None ,
522+ filter_expr : Optional [str ] = None ,
523+ filter_lang : Optional [str ] = None ,
524+ ** kwargs : Any ,
525+ ) -> dict [str , Any ]:
526+ """Clean up search arguments to match format expected by pgstac"""
527+ if filter_expr :
528+ if filter_lang == "cql2-text" :
529+ filter_expr = to_cql2 (parse_cql2_text (filter_expr ))
530+ filter_lang = "cql2-json"
535531
536- :param item_id: ID of the item.
537- :param collection_id: ID of the collection.
538- :param request: The request object.
539- :param kwargs: Additional arguments.
540- :returns: Streaming response for the item download.
541- """
542- product : EOProduct
543- product , _ = request .app .state .dag .search ({"collection" : collection_id , "id" : item_id })[0 ]
544-
545- # when could this really happen ?
546- if not product .downloader :
547- download_plugin = request .app .state .dag ._plugins_manager .get_download_plugin (product )
548- auth_plugin = request .app .state .dag ._plugins_manager .get_auth_plugin (download_plugin .provider )
549- product .register_downloader (download_plugin , auth_plugin )
550-
551- # required for auth. Can be removed when EODAG implements the auth interface
552- auth = (
553- product .downloader_auth .authenticate () if product .downloader_auth is not None else product .downloader_auth
554- )
532+ base_args ["filter" ] = str2json ("filter_expr" , filter_expr )
533+ base_args ["filter_lang" ] = "cql2-json"
555534
556- if product .downloader is None :
557- raise HTTPException (status_code = 500 , detail = "No downloader found for this product" )
558- # can we make something more clean here ?
559- download_stream_dict = product .downloader ._stream_download_dict (product , auth = auth )
535+ if datetime :
536+ base_args ["datetime" ] = format_datetime_range (datetime )
560537
561- return StreamingResponse (
562- content = download_stream_dict .content ,
563- headers = download_stream_dict .headers ,
564- media_type = download_stream_dict .media_type ,
565- )
538+ if query :
539+ base_args ["query" ] = orjson .loads (unquote_plus (query ))
540+
541+ if intersects :
542+ base_args ["intersects" ] = orjson .loads (unquote_plus (intersects ))
543+
544+ if sortby :
545+ sort_param = []
546+ for sort in sortby :
547+ sortparts = re .match (r"^([+-]?)(.*)$" , sort )
548+ if sortparts :
549+ sort_param .append (
550+ {
551+ "field" : sortparts .group (2 ).strip (),
552+ "direction" : "desc" if sortparts .group (1 ) == "-" else "asc" ,
553+ }
554+ )
555+ base_args ["sortby" ] = sort_param
556+
557+ # Remove None values from dict
558+ clean = {}
559+ for k , v in base_args .items ():
560+ if v is not None and v != []:
561+ clean [k ] = v
562+
563+ return clean
566564
567565
568566def prepare_search_base_args (search_request : BaseSearchPostRequest , model : type [CommonStacMetadata ]) -> dict [str , Any ]:
@@ -753,9 +751,7 @@ def eodag_search_next_page(dag, eodag_args):
753751 next_page_token = eodag_args .pop ("token" , None )
754752 provider = eodag_args .get ("provider" )
755753 if not next_page_token or not provider :
756- raise HTTPException (
757- status_code = 500 , detail = "Missing required token and federation backend for next page search."
758- )
754+ raise ValueError ("Missing required token and federation backend for next page search." )
759755 search_plugin = next (dag ._plugins_manager .get_search_plugins (provider = provider ))
760756 next_page_token_key = getattr (search_plugin .config , "pagination" , {}).get ("next_page_token_key" , "page" )
761757 eodag_args .pop ("count" , None )
@@ -773,18 +769,3 @@ def eodag_search_next_page(dag, eodag_args):
773769 logger .info ("StopIteration encountered during next page search." )
774770 search_result = SearchResult ([])
775771 return search_result
776-
777-
778- def add_filter_to_args (base_args : dict [str , Any ], filter_lang : Optional [str ], filter_expr : Optional [str ]):
779- """Parse the filter from the query and add to arguments
780-
781- :param base_args:
782- :param filter_expr: CQL filter to apply to the search.
783- :param filter_lang: Language of the filter (default is "cql2-text").
784- """
785- if filter_lang == "cql2-text" :
786- filter_expr = to_cql2 (parse_cql2_text (filter_expr ))
787- filter_lang = "cql2-json"
788-
789- base_args ["filter" ] = str2json ("filter_expr" , filter_expr )
790- base_args ["filter_lang" ] = "cql2-json"
0 commit comments