Repository: CS-SI/eodag (python)

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.

Pull request:
Online filter
Close #215 

Changes made to the SearchResult class (https://github.com/CS-SI/eodag/pull/359) added an easier way  to filter-out OFFLINE products.
In the original issue, that kind of code did the trick:

```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)
```

Currently, with the filters attached we can do:

```python
[...]
online_prods = prods.filter_property(storageStatus="ONLINE")
[...]
```

This PR simply creates an alternative, that seems like a basic filter.

```python
[...]
online_prods = prods.filter_online()
[...]
```

Another suggestion in the issue was to add a parameter to `EODataAccessGateway.download_all`, but I think that the only goal of this method is to perform downloads. It should not modify arbitrarily the list of products passed.


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"""