Repository: CS-SI/eodag

Issue:
Avoid trying to order an OFFLINE product
From @remi-braun in https://github.com/CS-SI/eodag/issues/214

> Maybe add the LTA order as a parameter, as it is not needed in every usecase (sometimes, sb just want to download everything he can without triggering any LTA retrieval)

LTA: Long Term Archive

The `HTTPDownload` plugin (used by creodias, onda, theia, peps, sobloo) can automatically try to order a product if its status is *OFFLINE*, and can download it afterwards.

The feature request is to add a way to avoid trying to order an *OFFLINE* product.

This could be done in at least two complementary ways:
1. Document the way to filter out *OFFLINE* products from the result of a search. This can be done as follows currently:
```python
from eodag.plugins.crunch.filter_property import FilterProperty
dag.set_preferred_provider("peps")
prods, _ = dag.search(...)
online_prods = prods.crunch(FilterProperty({"storageStatus": "ONLINE", "operator": "eq"}))
dag.download_all(online_prods)
```
2. Add a parameter to pass as a kwarg to `EODataAccessGateway.download_all` (not sure it's relevant for `download`), e.g. `skip_offline`, that would be passed to the HTTPPlugin and, if `True`, would stop trying to download this product, which would lead to `download_all` trying to download the next product.

Discussion:
github-actions[bot]: ## Unit Test Results
    2 files  ±0      2 suites  ±0   11m 23s [:stopwatch:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v1.20/README.md#the-symbols "duration of all tests") +49s
252 tests ±0  250 [:heavy_check_mark:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v1.20/README.md#the-symbols "passed tests") ±0  2 [:zzz:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v1.20/README.md#the-symbols "skipped / disabled tests") ±0  0 [:x:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v1.20/README.md#the-symbols "failed tests") ±0 
504 runs  ±0  500 [:heavy_check_mark:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v1.20/README.md#the-symbols "passed tests") ±0  4 [:zzz:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v1.20/README.md#the-symbols "skipped / disabled tests") ±0  0 [:x:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v1.20/README.md#the-symbols "failed tests") ±0 

Results for commit 77ae4d8d. ± Comparison against base commit be026c21.

github-actions[bot]: <strong> </strong>

| File | Coverage |   |
| - | :-: | :-: |
| **All files** | `79%` | :white_check_mark: |
| eodag/api/search\_result.py | `84%` | :white_check_mark: |

<strong> </strong>

| File | Coverage |   |
| - | :-: | :-: |
| **All files** | `79%` | :white_check_mark: |
| eodag/api/search\_result.py | `84%` | :white_check_mark: |
| eodag/api/search\_result.py | `83%` | :white_check_mark: |

_Minimum allowed coverage is `70%`_

<p align="right">Generated by :monkey: cobertura-action against 77ae4d8de012d2375a443d5a10c46c25449bcbf1 </p>

Patch:
diff --git a/docs/api_reference/searchresult.rst b/docs/api_reference/searchresult.rst
index cb845cfe..7957cf0b 100644
--- a/docs/api_reference/searchresult.rst
+++ b/docs/api_reference/searchresult.rst
@@ -22,6 +22,7 @@ Crunch
    SearchResult.filter_latest_by_name
    SearchResult.filter_overlap
    SearchResult.filter_property
+   SearchResult.filter_online
 
 Conversion
 ----------
@@ -41,4 +42,4 @@ Interface
    SearchResult.__geo_interface__
 
 .. autoclass:: SearchResult
-   :members: crunch, filter_date, filter_latest_intersect, filter_latest_by_name, filter_overlap, filter_property, from_geojson, as_geojson_object, as_shapely_geometry_object, as_wkt_object, __geo_interface__
+   :members: crunch, filter_date, filter_latest_intersect, filter_latest_by_name, filter_overlap, filter_property, filter_online, from_geojson, as_geojson_object, as_shapely_geometry_object, as_wkt_object, __geo_interface__
diff --git a/docs/notebooks/api_user_guide/6_crunch.ipynb b/docs/notebooks/api_user_guide/6_crunch.ipynb
index 5c9b45cb..5c4d429b 100644
--- a/docs/notebooks/api_user_guide/6_crunch.ipynb
+++ b/docs/notebooks/api_user_guide/6_crunch.ipynb
@@ -470,6 +470,51 @@
     "all([p.properties[\"cloudCover\"] < 1 for p in filtered_products])"
    ]
   },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Filter for online products"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Sometimes you may want to avoid ordering OFFLINE products, and only download the one marked ONLINE.\n",
+    "\n",
+    "You can already filter for online products using [FilterProperty](../../plugins_reference/generated/eodag.plugins.crunch.filter_property.FilterProperty.rst#eodag.plugins.crunch.filter_property.FilterProperty) like this:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "filtered_products = search_results.crunch(\n",
+    "    FilterProperty(dict(storageStatus=\"ONLINE\", operator=\"eq\"))\n",
+    ")\n",
+    "print(f\"{len(search_results) - len(filtered_products)} products are online.\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "While this code do the job, it is quite verbose. The better way is to use [SearchResult.filter_online()](../../api_reference/searchresult.rst#eodag.api.search_result.SearchResult.filter_online)."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "filtered_products = search_results.filter_online()\n",
+    "print(f\"{len(search_results) - len(filtered_products)} products are online.\")"
+   ]
+  },
   {
    "cell_type": "markdown",
    "metadata": {},
diff --git a/eodag/api/search_result.py b/eodag/api/search_result.py
index 182e7e54..eaea0653 100644
--- a/eodag/api/search_result.py
+++ b/eodag/api/search_result.py
@@ -102,6 +102,13 @@ class SearchResult(UserList):
         """
         return self.crunch(FilterProperty(dict(operator=operator, **search_property)))
 
+    def filter_online(self):
+        """
+        Use cruncher :class:`~eodag.plugins.crunch.filter_property.FilterProperty`,
+        filter for online products.
+        """
+        return self.filter_property(storageStatus="ONLINE")
+
     @staticmethod
     def from_geojson(feature_collection):
         """Builds an :class:`~eodag.api.search_result.SearchResult` object from its representation as geojson


Test patch:
diff --git a/tests/units/test_search_result.py b/tests/units/test_search_result.py
index 608cd86f..e85e3233 100644
--- a/tests/units/test_search_result.py
+++ b/tests/units/test_search_result.py
@@ -22,13 +22,34 @@ from collections import UserList
 import geojson
 from shapely.geometry.collection import GeometryCollection
 
-from tests.context import SearchResult
+from tests.context import EOProduct, SearchResult
 
 
 class TestSearchResult(unittest.TestCase):
     def setUp(self):
         super(TestSearchResult, self).setUp()
         self.search_result = SearchResult([])
+        self.search_result2 = SearchResult(
+            [
+                EOProduct(
+                    provider=None,
+                    properties={"geometry": "POINT (0 0)", "storageStatus": "ONLINE"},
+                ),
+                EOProduct(
+                    provider=None,
+                    properties={"geometry": "POINT (0 0)", "storageStatus": "OFFLINE"},
+                ),
+            ]
+        )
+
+    def test_search_result_filter_online(self):
+        """SearchResult.filter_online must only keep online results"""
+        filtered_products = self.search_result2.filter_online()
+        origin_size = len(self.search_result2)
+        filtered_size = len(filtered_products)
+        self.assertFalse(origin_size == filtered_size)
+        for product in filtered_products:
+            assert product.properties["storageStatus"] == "ONLINE"
 
     def test_search_result_geo_interface(self):
         """SearchResult must provide a FeatureCollection geo-interface"""