Skip to content

Commit 480239a

Browse files
committed
feat: allow to download results from all pages
1 parent f2ac36b commit 480239a

9 files changed

Lines changed: 181 additions & 77 deletions

File tree

eodag/api/core.py

Lines changed: 85 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,9 @@ def __init__(self, user_conf_file_path=None, locations_conf_path=None):
170170
)
171171
self.set_locations_conf(locations_conf_path)
172172

173+
# record searchs done
174+
self.search_kwargs_list = []
175+
173176
def get_version(self):
174177
"""Get eodag package version"""
175178
return pkg_resources.get_distribution("eodag").version
@@ -976,69 +979,77 @@ def search_iter_page(
976979
if iteration > 1 and next_page_query_obj:
977980
pagination_config["next_page_query_obj"] = next_page_query_obj
978981
logger.info("Iterate search over multiple pages: page #%s", iteration)
979-
try:
980-
products, _ = self._do_search(
981-
search_plugin, count=False, raise_errors=True, **search_kwargs
982+
if search_kwargs in self.search_kwargs_list:
983+
logger.info(
984+
"Search on page #%s skipped since it was already requested",
985+
iteration,
982986
)
983-
except Exception:
984-
products = SearchResult([])
985-
finally:
986-
# we don't want that next(search_iter_page(...)) modifies the plugin
987-
# indefinitely. So we reset after each request, but before the generator
988-
# yields, the attr next_page_url (to None) and
989-
# config.pagination["next_page_url_tpl"] (to its original value).
990-
next_page_url = getattr(search_plugin, "next_page_url", None)
991-
next_page_query_obj = getattr(search_plugin, "next_page_query_obj", {})
992-
next_page_merge = getattr(search_plugin, "next_page_merge", None)
993-
994-
if next_page_url:
995-
search_plugin.next_page_url = None
996-
if prev_next_page_url_tpl:
997-
search_plugin.config.pagination[
998-
"next_page_url_tpl"
999-
] = prev_next_page_url_tpl
1000-
if next_page_query_obj:
1001-
if prev_next_page_query_obj:
1002-
search_plugin.config.pagination[
1003-
"next_page_query_obj"
1004-
] = prev_next_page_query_obj
1005-
# Update next_page_query_obj for next page req
1006-
if next_page_merge:
1007-
search_plugin.next_page_query_obj = dict(
1008-
getattr(search_plugin, "query_params", {}),
1009-
**next_page_query_obj,
1010-
)
1011-
else:
1012-
search_plugin.next_page_query_obj = next_page_query_obj
1013-
1014-
if len(products) > 0:
1015-
# The first products between two iterations are compared. If they
1016-
# are actually the same product, it means the iteration failed at
1017-
# progressing for some reason. This is implemented as a workaround
1018-
# to some search plugins/providers not handling pagination.
1019-
product = products[0]
1020-
if (
1021-
prev_product
1022-
and product.properties["id"] == prev_product.properties["id"]
1023-
and product.provider == prev_product.provider
1024-
):
1025-
logger.warning(
1026-
"Iterate over pages: stop iterating since the next page "
1027-
"appears to have the same products as in the previous one. "
1028-
"This provider may not implement pagination.",
987+
else:
988+
try:
989+
products, _ = self._do_search(
990+
search_plugin, count=False, raise_errors=True, **search_kwargs
991+
)
992+
except Exception:
993+
products = SearchResult([])
994+
finally:
995+
# we don't want that next(search_iter_page(...)) modifies the plugin
996+
# indefinitely. So we reset after each request, but before the generator
997+
# yields, the attr next_page_url (to None) and
998+
# config.pagination["next_page_url_tpl"] (to its original value).
999+
next_page_url = getattr(search_plugin, "next_page_url", None)
1000+
next_page_query_obj = getattr(
1001+
search_plugin, "next_page_query_obj", {}
10291002
)
1003+
next_page_merge = getattr(search_plugin, "next_page_merge", None)
1004+
1005+
if next_page_url:
1006+
search_plugin.next_page_url = None
1007+
if prev_next_page_url_tpl:
1008+
search_plugin.config.pagination[
1009+
"next_page_url_tpl"
1010+
] = prev_next_page_url_tpl
1011+
if next_page_query_obj:
1012+
if prev_next_page_query_obj:
1013+
search_plugin.config.pagination[
1014+
"next_page_query_obj"
1015+
] = prev_next_page_query_obj
1016+
# Update next_page_query_obj for next page req
1017+
if next_page_merge:
1018+
search_plugin.next_page_query_obj = dict(
1019+
getattr(search_plugin, "query_params", {}),
1020+
**next_page_query_obj,
1021+
)
1022+
else:
1023+
search_plugin.next_page_query_obj = next_page_query_obj
1024+
1025+
if len(products) > 0:
1026+
# The first products between two iterations are compared. If they
1027+
# are actually the same product, it means the iteration failed at
1028+
# progressing for some reason. This is implemented as a workaround
1029+
# to some search plugins/providers not handling pagination.
1030+
product = products[0]
1031+
if (
1032+
prev_product
1033+
and product.properties["id"] == prev_product.properties["id"]
1034+
and product.provider == prev_product.provider
1035+
):
1036+
logger.warning(
1037+
"Iterate over pages: stop iterating since the next page "
1038+
"appears to have the same products as in the previous one. "
1039+
"This provider may not implement pagination.",
1040+
)
1041+
last_page_with_products = iteration - 1
1042+
break
1043+
yield products
1044+
prev_product = product
1045+
# Prevent a last search if the current one returned less than the
1046+
# maximum number of items asked for.
1047+
if len(products) < items_per_page:
1048+
last_page_with_products = iteration
1049+
break
1050+
else:
10301051
last_page_with_products = iteration - 1
10311052
break
1032-
yield products
1033-
prev_product = product
1034-
# Prevent a last search if the current one returned less than the
1035-
# maximum number of items asked for.
1036-
if len(products) < items_per_page:
1037-
last_page_with_products = iteration
1038-
break
1039-
else:
1040-
last_page_with_products = iteration - 1
1041-
break
10421053
iteration += 1
10431054
search_kwargs["page"] = iteration
10441055
logger.debug(
@@ -1360,7 +1371,10 @@ def _prepare_search(
13601371
logger.debug(
13611372
"Using plugin class for search: %s", search_plugin.__class__.__name__
13621373
)
1363-
auth_plugin = self._plugins_manager.get_auth_plugin(search_plugin.provider)
1374+
# get auth plugin by preventing possible multiple "auth" key in the return value
1375+
auth_plugin = kwargs.pop(
1376+
"auth", self._plugins_manager.get_auth_plugin(search_plugin.provider)
1377+
)
13641378

13651379
# append auth to search plugin if needed
13661380
if getattr(search_plugin.config, "need_auth", False) and callable(
@@ -1529,7 +1543,10 @@ def _do_search(self, search_plugin, count=True, raise_errors=False, **kwargs):
15291543
"Error while searching on provider %s (ignored):",
15301544
search_plugin.provider,
15311545
)
1532-
return SearchResult(results), total_results
1546+
results.search_kwargs = kwargs
1547+
if kwargs not in self.search_kwargs_list:
1548+
self.search_kwargs_list.append(kwargs)
1549+
return results, total_results
15331550

15341551
def crunch(self, results, **kwargs):
15351552
"""Apply the filters given through the keyword arguments to the results
@@ -1604,24 +1621,26 @@ def download_all(
16041621
:param timeout: (optional) If download fails, maximum time in minutes
16051622
before stop retrying to download
16061623
:type timeout: int
1607-
:param kwargs: `outputs_prefix` (str), `extract` (bool), `delete_archive` (bool)
1608-
and `dl_url_params` (dict) can be provided as additional kwargs
1609-
and will override any other values defined in a configuration
1610-
file or with environment variables.
1624+
:param kwargs: `outputs_prefix` (str), `extract` (bool), `delete_archive` (bool),
1625+
`dl_url_params` (dict) and `exhaust` (bool) can be provided as
1626+
additional kwargs and will override any other values defined in a
1627+
configuration file or with environment variables.
16111628
:type kwargs: Union[str, bool, dict]
16121629
:returns: A collection of the absolute paths to the downloaded products
16131630
:rtype: list
16141631
"""
16151632
paths = []
16161633
if search_result:
1617-
logger.info("Downloading %s products", len(search_result))
1634+
if not kwargs.get("exhaust", False):
1635+
logger.info("Downloading %s products", len(search_result))
16181636
# Get download plugin using first product assuming product from several provider
16191637
# aren't mixed into a search result
16201638
download_plugin = self._plugins_manager.get_download_plugin(
16211639
search_result[0]
16221640
)
16231641
paths = download_plugin.download_all(
16241642
search_result,
1643+
self,
16251644
downloaded_callback=downloaded_callback,
16261645
progress_callback=progress_callback,
16271646
wait=wait,

eodag/api/search_result.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1616
# See the License for the specific language governing permissions and
1717
# limitations under the License.
18+
import logging
1819
from collections import UserList
1920

2021
from shapely.geometry import GeometryCollection, shape
@@ -26,16 +27,24 @@
2627
from eodag.plugins.crunch.filter_overlap import FilterOverlap
2728
from eodag.plugins.crunch.filter_property import FilterProperty
2829

30+
logger = logging.getLogger("eodag.search_result")
31+
2932

3033
class SearchResult(UserList):
3134
"""An object representing a collection of :class:`~eodag.api.product._product.EOProduct` resulting from a search.
3235
3336
:param products: A list of products resulting from a search
3437
:type products: list(:class:`~eodag.api.product._product.EOProduct`)
38+
:ivar search_kwargs: The search kwargs used by eodag to search for the product
39+
:vartype search_kwargs: Any
40+
:ivar crunchers: The list of crunchers used to filter these products
41+
:vartype crunchers: list(tuple(subclass of :class:`~eodag.plugins.crunch.base.Crunch`, dict))
3542
"""
3643

3744
def __init__(self, products):
3845
super(SearchResult, self).__init__(products)
46+
self.search_kwargs = None
47+
self.crunchers = []
3948

4049
def crunch(self, cruncher, **search_params):
4150
"""Do some crunching with the underlying EO products.
@@ -47,8 +56,24 @@ def crunch(self, cruncher, **search_params):
4756
:returns: The result of the application of the crunching method to the EO products
4857
:rtype: :class:`~eodag.api.search_result.SearchResult`
4958
"""
50-
crunched_results = cruncher.proceed(self, **search_params)
51-
return SearchResult(crunched_results)
59+
for results_cruncher in self.crunchers:
60+
if (
61+
cruncher.__class__.__name__ == results_cruncher.__class__.__name__
62+
and cruncher.config == results_cruncher.config
63+
):
64+
logger.info(
65+
(
66+
"The cruncher '{}' has already been used for these search results with "
67+
"the following parameter(s): {}. Please change parameters or use an other cruncher"
68+
).format(cruncher.__class__.__name__, cruncher.config)
69+
)
70+
return self
71+
crunched_results_list = cruncher.proceed(self, **search_params)
72+
crunched_results = SearchResult(crunched_results_list)
73+
crunched_results.search_kwargs = self.search_kwargs
74+
self.crunchers.append(cruncher)
75+
crunched_results.crunchers = self.crunchers
76+
return crunched_results
5277

5378
def filter_date(self, start=None, end=None):
5479
"""

eodag/plugins/apis/base.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ def download(
108108
def download_all(
109109
self,
110110
products,
111+
dag,
111112
auth=None,
112113
downloaded_callback=None,
113114
progress_callback=None,
@@ -120,6 +121,8 @@ def download_all(
120121
121122
:param products: Products to download
122123
:type products: :class:`~eodag.api.search_result.SearchResult`
124+
:param dag: The gateway to download products
125+
:type dag: :class:`~eodag.api.core.EODataAccessGateway`
123126
:param auth: (optional) The configuration of a plugin of type Authentication
124127
:type auth: :class:`~eodag.config.PluginConfig`
125128
:param downloaded_callback: (optional) A method or a callable object which takes
@@ -136,10 +139,10 @@ def download_all(
136139
:param timeout: (optional) If download fails, maximum time in minutes before stop retrying
137140
to download
138141
:type timeout: int
139-
:param kwargs: `outputs_prefix` (str), `extract` (bool), `delete_archive` (bool)
140-
and `dl_url_params` (dict) can be provided as additional kwargs
141-
and will override any other values defined in a configuration
142-
file or with environment variables.
142+
:param kwargs: `outputs_prefix` (str), `extract` (bool), `delete_archive` (bool),
143+
`dl_url_params` (dict) and `exhaust` (bool) can be provided as
144+
additional kwargs and will override any other values defined in a
145+
configuration file or with environment variables.
143146
:type kwargs: Union[str, bool, dict]
144147
:returns: List of absolute paths to the downloaded products in the local
145148
filesystem (e.g. ``['/tmp/product.zip']`` on Linux or

eodag/plugins/apis/cds.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ def download(self, product, auth=None, progress_callback=None, **kwargs):
230230
def download_all(
231231
self,
232232
products,
233+
dag,
233234
auth=None,
234235
downloaded_callback=None,
235236
progress_callback=None,
@@ -242,6 +243,7 @@ def download_all(
242243
"""
243244
return super(CdsApi, self).download_all(
244245
products,
246+
dag,
245247
auth=auth,
246248
downloaded_callback=downloaded_callback,
247249
progress_callback=progress_callback,

eodag/plugins/apis/ecmwf.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ def download(self, product, auth=None, progress_callback=None, **kwargs):
218218
def download_all(
219219
self,
220220
products,
221+
dag,
221222
auth=None,
222223
downloaded_callback=None,
223224
progress_callback=None,
@@ -230,6 +231,7 @@ def download_all(
230231
"""
231232
return super(EcmwfApi, self).download_all(
232233
products,
234+
dag,
233235
auth=auth,
234236
downloaded_callback=downloaded_callback,
235237
progress_callback=progress_callback,

eodag/plugins/apis/usgs.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,7 @@ def download_request(product, fs_path, progress_callback, **kwargs):
361361
def download_all(
362362
self,
363363
products,
364+
dag,
364365
auth=None,
365366
downloaded_callback=None,
366367
progress_callback=None,
@@ -373,6 +374,7 @@ def download_all(
373374
"""
374375
return super(UsgsApi, self).download_all(
375376
products,
377+
dag,
376378
auth=auth,
377379
downloaded_callback=downloaded_callback,
378380
progress_callback=progress_callback,

eodag/plugins/download/aws.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -934,6 +934,7 @@ def get_chunk_dest_path(self, product, chunk, dir_prefix=None, build_safe=False)
934934
def download_all(
935935
self,
936936
products,
937+
dag,
937938
auth=None,
938939
downloaded_callback=None,
939940
progress_callback=None,
@@ -944,6 +945,7 @@ def download_all(
944945
"""
945946
return super(AwsDownload, self).download_all(
946947
products,
948+
dag,
947949
auth=auth,
948950
downloaded_callback=downloaded_callback,
949951
progress_callback=progress_callback,

0 commit comments

Comments
 (0)