Repository: planetlabs/planet-client-python

Issue:
Use PEP 525 async generator to improve data and orders client design
[PEP525](https://peps.python.org/pep-0525/) added async generators to Python 3.6. An async generator is an `async def` function or method that uses `yield` instead of `return`. Using it for SDK methods that have multiple results gives us a cleaner design that is in harmony with the designs of async Python software like aiohttp and httpx.

Here is a short bit of code that uses the data client and orders client together. We search the catalog for items using DataClient and then use a list of item ids in creating an order with OrdersClient.

```python
async def main():
    """Use DataClient and OrdersClient to search for and order scenes.
    """
    async with Session() as sess:
        data_client = DataClient(sess)

        # A reader might be led to believe that since we're awaiting the results
        # of search(), the return value has all the results materialized in it.
        search_result_iterator = await data_client.search(["PSScene"], filter_config)

        # But it does not. We still need to asynchronously iterate.
        item_ids = [item["id"] async for item in search_result_iterator]

        client = OrdersClient(sess)
        order_request = {
            "name": "my_order",
            "products": [
                {
                    "item_ids": item_ids,
                    "item_type": "PSScene",
                    "product_bundle": "analytic_udm2",
                }
            ]
        }
        order = await client.create_order(order_request)

        # Wait for order to be completed.
        order_id = order["id"]
        state = await client.wait(order_id, state="success")
        ...
```

To reiterate what I wrote in a comment, a reader of the code might be led to believe that since we're awaiting the results of search(), the return value has all the results already materialized in it. And requiring further iteration then looks confusing. I think it looks odd to await a method call and then asynchronously iterate over the return value. And Python doesn't require it. Neither httpx's [aiter_bytes](https://www.python-httpx.org/async/#streaming-responses) nor aiohttp's [iter_chunked](https://docs.aiohttp.org/en/stable/client_quickstart.html#streaming-response-content) return chunkers. They are async generators.

As an improvement, I propose to make DataClient.search an async generator and allow more concise usage like this:

```python
async def main():
    """Use DataClient and OrderClient to search for and order scenes.
    """
    async with Session() as sess:
        data_client = DataClient(sess)
        item_ids = [item["id"] async for item in data_client.search(["PSScene"], filter_config)]
        ...
```

The new implementation would be almost trivial. It would mean, for DataClient, changing the code at 
 https://github.com/planetlabs/planet-client-python/blob/main/planet/clients/data.py#L185 from

```python
        return Items(request, self._do_request, limit=limit)
```

to

```python
        async for item in Items(request, self._do_request, limit=limit):
            yield item
```

This is not unlike what httpx does here: https://github.com/encode/httpx/blob/8152c4facd0f71e0f376287e41a0810a60fec9c6/httpx/_models.py#L921. It *does* have a chunker, but it's used internally, not by httpx users.

What makes the implementation not trivial is updating tests. That's where most of the work is. I think it's worth doing. 

To support less common situations where one really needs a reference to the search results generator, say to interleave calls to `next(search_results)` with other function calls, one can assign `search_results = client.search()` (without `await`) and then use the awaitable next method of the async generator.

```python
import asyncio

async def func():
    for i in range(10):
        yield i

async def main():
    gen = func()
    print("first")
    # In Python 3.10 this is: print(await anext(gen))
    print(await gen.__anext__())
    print("second")
    print(await gen.__anext__())

asyncio.run(main())
first
0
second
1
```

Discussion:

Patch:
diff --git a/docs/cli/cli-orders.md b/docs/cli/cli-orders.md
index f2561c7..7804dff 100644
--- a/docs/cli/cli-orders.md
+++ b/docs/cli/cli-orders.md
@@ -467,23 +467,7 @@ curl -s https://raw.githubusercontent.com/planetlabs/planet-client-python/main/d
 
 ### Harmonize
 
-The harmonize tool allows you to compare data to different generations of satellites by radiometrically harmonizing imagery captured by one satellite instrument type to imagery captured by another. To harmonize your data to a sensor you must define the sensor you wish to harmonize with in your `tools.json`. Currently, only "PS2" (Dove Classic) and "Sentinel-2" are supported as target sensors. The Sentinel-2 target only harmonizes PSScene surface reflectance bundle types (`analytic_8b_sr_udm2`, `analytic_sr_udm2`). The PS2 target only works on analytic bundles from Dove-R (`PS2.SD`).
-
-```json
-[
-  {
-    "harmonize": {
-      "target_sensor": "Sentinel-2"
-      }
-    }
-]
-```
-
-You may create an order request by calling `tools.json` with `--tools`.
-
-```console
-planet orders request psscene analytic_sr_udm2 --name "Harmonized data" --id 20200925_161029_69_2223 --tools tools.json
-```
+TODO
 
 ### STAC Metadata
 
diff --git a/docs/python/sdk-guide.md b/docs/python/sdk-guide.md
index e3495bd..7864847 100644
--- a/docs/python/sdk-guide.md
+++ b/docs/python/sdk-guide.md
@@ -109,13 +109,19 @@ Otherwise, the JSON blob is a list of the individual results.
 >>> async def main():
 ...     async with Session() as sess:
 ...         client = OrdersClient(sess)
-...         orders = client.list_orders()
-...         orders_list = collect(orders)
+...         orders_aiter = client.list_orders_aiter()
+...         orders_list = collect(orders_aiter)
 ...
 >>> asyncio.run(main())
 
 ```
 
+Alternatively, these results can be converted to a list directly with
+
+```python
+>>>         orders_list = [o async for o in client.list_orders_aiter()]
+```
+
 
 ### Orders Client
 
diff --git a/docs/resources/index.md b/docs/resources/index.md
index 8da0223..335931d 100644
--- a/docs/resources/index.md
+++ b/docs/resources/index.md
@@ -2,45 +2,26 @@
 title: Resources
 ---
 
-## Examples
+## This Build
 
-### SDK examples
+[Planet](https://planet.com) Software Development Kit (SDK) for Python [![Build Status](https://travis-ci.org/planetlabs/planet-client-python.svg?branch=master)](https://travis-ci.org/planetlabs/planet-client-python)
 
-The following examples were created specifically to show you how to use the SDK and CLI:
-
-* [download_multiple_assets.py](https://github.com/planetlabs/planet-client-python/blob/main/examples/download_multiple_assets.py) - this Python script orders, activates, and downloads two assets
-* [orders_create_and_download_multiple_orders.py](https://github.com/planetlabs/planet-client-python/blob/main/examples/orders_create_and_download_multiple_orders.py) - this Python script creates two orders, each with unique Area of Interest (AoI), preventing a combined download
-* [Planet API Python Client](https://github.com/planetlabs/notebooks/blob/master/jupyter-notebooks/data-api-tutorials/planet_python_client_introduction.ipynb) - a Python notebook to introduce Planet’s Data API and the `planet` module
-* [Orders API & Planet SDK](https://github.com/planetlabs/notebooks/blob/665f165e59f2c3584976ad2dde569c649e631c0b/jupyter-notebooks/orders_api_tutorials/Planet_SDK_Orders_demo.ipynb) - a Python notebook shows how to get started with Planet SDK and the Orders API.
-* [Analysis Ready Data Tutorial Part 1: Introduction and Best Practices](https://github.com/planetlabs/notebooks/blob/6cc220ff6db246353af4798be219ee1fe7e858b0/jupyter-notebooks/analysis-ready-data/ard_1_intro_and_best_practices.ipynb) - this Python notebook uses the SDK to prepare Analysis Ready Data.
-* [Analysis Ready Data Tutorial Part 2](https://github.com/planetlabs/notebooks/blob/6cc220ff6db246353af4798be219ee1fe7e858b0/jupyter-notebooks/analysis-ready-data/ard_2_use_case_1.ipynb) - the first use case in this Python notebook leverages the SDK’s `order_request` feature to prepare an NDVI time stack and the second use case visualizes the NDVI imagery.
-
-### Other examples
-
-Besides the SDK-specific examples, above, you can find many examples that show how to access Planet data in the documentation and Planet School at the [Planet Developers Center](https://developers.planet.com).Also, more working examples are on the [Planet Labs Python notebooks](https://github.com/planetlabs/notebooks) on GitHub.
 
 ## Planet APIs
 
-This pre-release SDK has implemented interfaces for several Planet APIs. Check out the documentation for the underlying API:
+## Planet Tutorials
 
-* [Data](https://developers.planet.com/docs/apis/data/)
-* [Orders](https://developers.planet.com/apis/orders/)
-* [Subscriptions](https://developers.planet.com/docs/subscriptions/)
+## Contact Support
 
 ## Email Developer Relations
 
-We are eager to share this pre-release with you and encourage you to test your workflows rigorously. Based on your feedback, we may roll out additional updates to improve your experience. Besides joining the discussion, and filing issues and pull requests here, feel free to share your general feedback with us at developers@planet.com.
-## Contribute to this open source project
-
-To contribute or develop with this library, see
-[CONTRIBUTING](https://github.com/planetlabs/planet-client-python/blob/main/CONTRIBUTING.md).
-
 ## Build Status
 
-[Planet Software Development Kit (SDK) for Python main branch](https://github.com/planetlabs/planet-client-python)
-
 [![Build Status](https://travis-ci.org/planetlabs/planet-client-python.svg?branch=master)](https://travis-ci.org/planetlabs/planet-client-python)
 
-## Version 1 of this SDK
+[Planet](https://planet.com) Software Development Kit (SDK) for Python.
 
-[Version 1 of this SDK](https://github.com/planetlabs/planet-client-python/tree/1.5.2) is significantly different (see the [documentation](https://planet-sdk-for-python.readthedocs.io/en/latest/)). Version 2 is not backward compatible. Make sure to create a separate virtual environment if you need to work with both versions. For more information on how to do this, see the [Virtual Environments and the Planet SDK for Python](https://planet-sdk-for-python-v2.readthedocs.io/en/latest/get-started/venv-tutorial/).
\ No newline at end of file
+## Contribute to this open source project
+
+To contribute or develop with this library, see
+[CONTRIBUTING](https://github.com/planetlabs/planet-client-python/CONTRIBUTING.md).
diff --git a/planet/cli/data.py b/planet/cli/data.py
index 73322cd..61688d6 100644
--- a/planet/cli/data.py
+++ b/planet/cli/data.py
@@ -272,12 +272,12 @@ async def search(ctx, item_types, filter, limit, name, sort, pretty):
     parameter will be applied to the stored quick search.
     """
     async with data_client(ctx) as cl:
-        items = await cl.search(item_types,
-                                filter,
-                                name=name,
-                                sort=sort,
-                                limit=limit)
-        async for item in items:
+        items_aiter = cl.search_aiter(item_types,
+                                      filter,
+                                      name=name,
+                                      sort=sort,
+                                      limit=limit)
+        async for item in items_aiter:
             echo_json(item, pretty)
 
 
diff --git a/planet/cli/orders.py b/planet/cli/orders.py
index 60d52ae..b126a4d 100644
--- a/planet/cli/orders.py
+++ b/planet/cli/orders.py
@@ -67,8 +67,8 @@ async def list(ctx, state, limit, pretty):
     optionally pretty-printed.
     '''
     async with orders_client(ctx) as cl:
-        orders = await cl.list_orders(state=state, limit=limit)
-        async for o in orders:
+        orders_aiter = cl.list_orders_aiter(state=state, limit=limit)
+        async for o in orders_aiter:
             echo_json(o, pretty)
 
 
diff --git a/planet/cli/subscriptions.py b/planet/cli/subscriptions.py
index 42a4dad..41d2283 100644
--- a/planet/cli/subscriptions.py
+++ b/planet/cli/subscriptions.py
@@ -42,8 +42,9 @@ async def list_subscriptions_cmd(ctx, status, limit, pretty):
     """Prints a sequence of JSON-encoded Subscription descriptions."""
     async with CliSession(auth=ctx.obj['AUTH']) as session:
         client = SubscriptionsClient(session)
-        filtered_subs = client.list_subscriptions(status=status, limit=limit)
-        async for sub in filtered_subs:
+        subs_aiter = client.list_subscriptions_aiter(status=status,
+                                                     limit=limit)
+        async for sub in subs_aiter:
             echo_json(sub, pretty)
 
 
@@ -157,8 +158,8 @@ async def list_subscription_results_cmd(ctx,
     """Gets results of a subscription and prints the API response."""
     async with CliSession(auth=ctx.obj['AUTH']) as session:
         client = SubscriptionsClient(session)
-        filtered_results = client.get_results(subscription_id,
-                                              status=status,
-                                              limit=limit)
-        async for result in filtered_results:
+        results_aiter = client.get_results_aiter(subscription_id,
+                                                 status=status,
+                                                 limit=limit)
+        async for result in results_aiter:
             echo_json(result, pretty)
diff --git a/planet/clients/data.py b/planet/clients/data.py
index fdba667..5f8399b 100644
--- a/planet/clients/data.py
+++ b/planet/clients/data.py
@@ -93,57 +93,18 @@ class DataClient:
     def _item_url(self, item_type, item_id):
         return f'{self._base_url}/item-types/{item_type}/items/{item_id}'
 
-    async def search(self,
-                     item_types: List[str],
-                     search_filter: dict,
-                     name: Optional[str] = None,
-                     sort: Optional[str] = None,
-                     limit: int = 100) -> AsyncIterator[dict]:
-        """Execute a quick search.
+    async def search_aiter(self,
+                           item_types: List[str],
+                           search_filter: dict,
+                           name: Optional[str] = None,
+                           sort: Optional[str] = None,
+                           limit: int = 100) -> AsyncIterator[dict]:
+        """Iterate over results from a quick search.
 
         Quick searches are saved for a short period of time (~month). The
         `name` parameter of the search defaults to the id of the generated
         search id if `name` is not specified.
 
-        To filter to items you have access to download which are of standard
-        (aka not test) quality, use the following:
-
-        ```python
-        >>> from planet import data_filter
-        >>> data_filter.and_filter([
-        ...     data_filter.permission_filter(),
-        ...     data_filter.std_quality_filter()
-        >>> ])
-
-        ```
-
-        To avoid filtering out any imagery, supply a blank AndFilter, which can
-        be created with `data_filter.and_filter([])`.
-
-        Example:
-
-        ```python
-        >>> import asyncio
-        >>> from planet import Session, DataClient
-        >>>
-        >>> async def main():
-        ...     item_types = ['PSScene']
-        ...     sfilter = {
-        ...         "type":"DateRangeFilter",
-        ...         "field_name":"acquired",
-        ...         "config":{
-        ...             "gt":"2019-12-31T00:00:00Z",
-        ...             "lte":"2020-01-31T00:00:00Z"
-        ...         }
-        ...     }
-        ...     async with Session() as sess:
-        ...         cl = DataClient(sess)
-        ...         items = await cl.quick_search(item_types, sfilter)
-        ...
-        >>> asyncio.run(main())
-
-        ```
-
         Parameters:
             item_types: The item types to include in the search.
             search_filter: Structured search criteria.
@@ -153,8 +114,8 @@ class DataClient:
             limit: Maximum number of results to return. When set to 0, no
                 maximum is applied.
 
-        Returns:
-            Returns an iterator over all items matching the search.
+        Yields:
+            Description of an item.
 
         Raises:
             planet.exceptions.APIError: On API error.
@@ -178,7 +139,8 @@ class DataClient:
                                                url=url,
                                                json=request_json,
                                                params=params)
-        return Items(response, self._session.request, limit=limit)
+        async for i in Items(response, self._session.request, limit=limit):
+            yield i
 
     async def create_search(self,
                             name: str,
@@ -262,11 +224,11 @@ class DataClient:
                                                json=request)
         return response.json()
 
-    async def list_searches(self,
-                            sort: str = 'created desc',
-                            search_type: str = 'any',
-                            limit: int = 100) -> AsyncIterator[dict]:
-        """List all saved searches available to the authenticated user.
+    async def list_searches_aiter(self,
+                                  sort: str = 'created desc',
+                                  search_type: str = 'any',
+                                  limit: int = 100) -> AsyncIterator[dict]:
+        """Iterate through list of searches available to the user.
 
         NOTE: the term 'saved' is overloaded here. We want to list saved
         searches that are 'quick' or list saved searches that are 'saved'? Do
@@ -279,8 +241,8 @@ class DataClient:
             limit: Maximum number of results to return. When set to 0, no
                 maximum is applied.
 
-        Returns:
-            An iterator over all searches that match filter.
+        Yields:
+            Description of a search.
 
         Raises:
             planet.exceptions.APIError: On API error.
@@ -300,7 +262,8 @@ class DataClient:
         url = f'{self._searches_url()}'
 
         response = await self._session.request(method='GET', url=url)
-        return Searches(response, self._session.request, limit=limit)
+        async for s in Searches(response, self._session.request, limit=limit):
+            yield s
 
     async def delete_search(self, search_id: str):
         """Delete an existing saved search.
@@ -332,18 +295,18 @@ class DataClient:
         response = await self._session.request(method='GET', url=url)
         return response.json()
 
-    async def run_search(self,
-                         search_id: str,
-                         limit: int = 100) -> AsyncIterator[dict]:
-        """Execute a saved search.
+    async def run_search_aiter(self,
+                               search_id: str,
+                               limit: int = 100) -> AsyncIterator[dict]:
+        """Iterate over results from a saved search.
 
         Parameters:
             search_id: Stored search identifier.
             limit: Maximum number of results to return. When set to 0, no
                 maximum is applied.
 
-        Returns:
-            Returns an iterator over all items matching the search.
+        Yields:
+            Description of an item.
 
         Raises:
             planet.exceptions.APIError: On API error.
@@ -351,7 +314,8 @@ class DataClient:
         url = f'{self._searches_url()}/{search_id}/results'
 
         response = await self._session.request(method='GET', url=url)
-        return Items(response, self._session.request, limit=limit)
+        async for i in Items(response, self._session.request, limit=limit):
+            yield i
 
     async def get_stats(self,
                         item_types: List[str],
diff --git a/planet/clients/orders.py b/planet/clients/orders.py
index 733e96f..b5c1cc5 100644
--- a/planet/clients/orders.py
+++ b/planet/clients/orders.py
@@ -463,10 +463,10 @@ class OrdersClient:
 
         return current_state
 
-    async def list_orders(self,
-                          state: Optional[str] = None,
-                          limit: int = 100) -> AsyncIterator[dict]:
-        """Get all order requests.
+    async def list_orders_aiter(self,
+                                state: Optional[str] = None,
+                                limit: int = 100) -> AsyncIterator[dict]:
+        """Iterate over the list of stored order requests.
 
         Parameters:
             state: Filter orders to given state.
@@ -474,7 +474,7 @@ class OrdersClient:
                 maximum is applied.
 
         Returns:
-            User orders that match the query
+            Iterator over user orders that match the query
 
         Raises:
             planet.exceptions.APIError: On API error.
@@ -493,4 +493,5 @@ class OrdersClient:
         response = await self._session.request(method='GET',
                                                url=url,
                                                params=params)
-        return Orders(response, self._session.request, limit=limit)
+        async for o in Orders(response, self._session.request, limit=limit):
+            yield o
diff --git a/planet/clients/subscriptions.py b/planet/clients/subscriptions.py
index 888abfc..5aba2d4 100644
--- a/planet/clients/subscriptions.py
+++ b/planet/clients/subscriptions.py
@@ -31,10 +31,11 @@ class SubscriptionsClient:
     def __init__(self, session: Session) -> None:
         self._session = session
 
-    async def list_subscriptions(self,
-                                 status: Optional[Set[str]] = None,
-                                 limit: int = 100) -> AsyncIterator[dict]:
-        """Get account subscriptions with optional filtering.
+    async def list_subscriptions_aiter(
+            self,
+            status: Optional[Set[str]] = None,
+            limit: int = 100) -> AsyncIterator[dict]:
+        """Iterate over list of account subscriptions with optional filtering.
 
         Note:
             The name of this method is based on the API's method name.
@@ -192,11 +193,11 @@ class SubscriptionsClient:
             sub = resp.json()
             return sub
 
-    async def get_results(self,
-                          subscription_id: str,
-                          status: Optional[Set[str]] = None,
-                          limit: int = 100) -> AsyncIterator[dict]:
-        """Get Results of a Subscription.
+    async def get_results_aiter(self,
+                                subscription_id: str,
+                                status: Optional[Set[str]] = None,
+                                limit: int = 100) -> AsyncIterator[dict]:
+        """Iterate over results of a Subscription.
 
         Note:
             The name of this method is based on the API's method name. This
diff --git a/planet/order_request.py b/planet/order_request.py
index 586c44f..34b4cbb 100644
--- a/planet/order_request.py
+++ b/planet/order_request.py
@@ -429,12 +429,10 @@ def toar_tool(scale_factor: Optional[int] = None, ) -> dict:
     return _tool('toar', parameters)
 
 
-def harmonize_tool(target_sensor: str) -> dict:
+def harmonize_tool() -> dict:
     '''Create the API spec representation of a harmonize tool.
 
-    Currently, only "PS2" (Dove Classic) and "Sentinel-2" are supported as
-    target sensors. The Sentinel-2 target only harmonizes PSScene
-    surface reflectance bundle types (analytic_8b_sr_udm2, analytic_sr_udm2).
-    The PS2 target only works on analytic bundles from Dove-R (PS2.SD).
+    Currently, only "PS2" (Dove Classic) is supported as a target sensor, and
+    it will transform only items captured by “PS2.SD” (Dove-R).
     '''
-    return _tool('harmonize', {'target_sensor': target_sensor})
+    return _tool('harmonize', {'target_sensor': 'PS2'})


Test patch:
diff --git a/tests/integration/test_data_api.py b/tests/integration/test_data_api.py
index 2e9522f..aba2bcd 100644
--- a/tests/integration/test_data_api.py
+++ b/tests/integration/test_data_api.py
@@ -67,10 +67,10 @@ def search_response(item_descriptions):
 
 @respx.mock
 @pytest.mark.asyncio
-async def test_search_basic(item_descriptions,
-                            search_filter,
-                            search_response,
-                            session):
+async def test_search_aiter_basic(item_descriptions,
+                                  search_filter,
+                                  search_response,
+                                  session):
 
     quick_search_url = f'{TEST_URL}/quick-search'
     next_page_url = f'{TEST_URL}/blob/?page_marker=IAmATest'
@@ -89,8 +89,10 @@ async def test_search_basic(item_descriptions,
     respx.get(next_page_url).return_value = mock_resp2
 
     cl = DataClient(session, base_url=TEST_URL)
-    items = await cl.search(['PSScene'], search_filter, name='quick_search')
-    items_list = [i async for i in items]
+    item_aiter = cl.search_aiter(['PSScene'],
+                                 search_filter,
+                                 name='quick_search')
+    items_list = [i async for i in item_aiter]
 
     # check that request is correct
     expected_request = {
@@ -107,10 +109,10 @@ async def test_search_basic(item_descriptions,
 
 @respx.mock
 @pytest.mark.asyncio
-async def test_search_sort(item_descriptions,
-                           search_filter,
-                           search_response,
-                           session):
+async def test_search_aiter_sort(item_descriptions,
+                                 search_filter,
+                                 search_response,
+                                 session):
 
     sort = 'acquired asc'
     quick_search_url = f'{TEST_URL}/quick-search?_sort={sort}'
@@ -123,18 +125,18 @@ async def test_search_sort(item_descriptions,
     # if the sort parameter is not used correctly, the client will not send
     # the request to the mocked endpoint and this test will fail
     cl = DataClient(session, base_url=TEST_URL)
-    items = await cl.search(['PSScene'], search_filter, sort=sort)
+    item_aiter = cl.search_aiter(['PSScene'], search_filter, sort=sort)
 
     # run through the iterator to actually initiate the call
-    [i async for i in items]
+    [i async for i in item_aiter]
 
 
 @respx.mock
 @pytest.mark.asyncio
-async def test_search_limit(item_descriptions,
-                            search_filter,
-                            search_response,
-                            session):
+async def test_search_aiter_limit(item_descriptions,
+                                  search_filter,
+                                  search_response,
+                                  session):
 
     quick_search_url = f'{TEST_URL}/quick-search'
 
@@ -146,8 +148,8 @@ async def test_search_limit(item_descriptions,
     respx.post(quick_search_url).return_value = mock_resp
 
     cl = DataClient(session, base_url=TEST_URL)
-    items = await cl.search(['PSScene'], search_filter, limit=2)
-    items_list = [i async for i in items]
+    item_aiter = cl.search_aiter(['PSScene'], search_filter, limit=2)
+    items_list = [i async for i in item_aiter]
 
     # check only the first two results were returned
     assert items_list == item_descriptions[:2]
@@ -295,18 +297,18 @@ async def test_update_search_basic(search_filter, session):
 @respx.mock
 @pytest.mark.asyncio
 @pytest.mark.parametrize("limit, expected_list_length", [(None, 4), (3, 3)])
-async def test_list_searches_success(limit,
-                                     expected_list_length,
-                                     search_result,
-                                     session):
+async def test_list_searches_aiter_success(limit,
+                                           expected_list_length,
+                                           search_result,
+                                           session):
     page1_response = {"_links": {}, "searches": [search_result] * 4}
     route = respx.get(TEST_SEARCHES_URL)
     route.return_value = httpx.Response(200, json=page1_response)
 
     cl = DataClient(session, base_url=TEST_URL)
 
-    searches = await cl.list_searches(limit=limit)
-    searches_list_length = len([s async for s in searches])
+    search_aiter = cl.list_searches_aiter(limit=limit)
+    searches_list_length = len([s async for s in search_aiter])
     assert searches_list_length == expected_list_length
 
     assert route.called
@@ -318,17 +320,19 @@ async def test_list_searches_success(limit,
     "sort, search_type, expectation",
     [('DOESNOTEXIST', 'ANY', pytest.raises(exceptions.ClientError)),
      ('CREATED DESC', 'DOESNOTEXIST', pytest.raises(exceptions.ClientError))])
-async def test_list_searches_args_do_not_match(sort,
-                                               search_type,
-                                               expectation,
-                                               session):
+async def test_list_searches_aiter_args_do_not_match(sort,
+                                                     search_type,
+                                                     expectation,
+                                                     session):
     route = respx.get(TEST_SEARCHES_URL)
     route.return_value = httpx.Response(200, json={})
 
     cl = DataClient(session, base_url=TEST_URL)
 
     with expectation:
-        await cl.list_searches(sort=sort, search_type=search_type)
+        searches_aiter = cl.list_searches_aiter(sort=sort,
+                                                search_type=search_type)
+        [s async for s in searches_aiter]
 
     assert not route.called
 
@@ -353,7 +357,7 @@ async def test_delete_search(retcode, expectation, session):
 
 @respx.mock
 @pytest.mark.asyncio
-async def test_run_search_success(item_descriptions, session):
+async def test_run_search_aiter_success(item_descriptions, session):
     sid = 'search_id'
     route = respx.get(f'{TEST_SEARCHES_URL}/{sid}/results')
 
@@ -372,8 +376,8 @@ async def test_run_search_success(item_descriptions, session):
     respx.get(next_page_url).return_value = mock_resp2
 
     cl = DataClient(session, base_url=TEST_URL)
-    items = await cl.run_search(sid)
-    items_list = [i async for i in items]
+    item_aiter = cl.run_search_aiter(sid)
+    items_list = [i async for i in item_aiter]
 
     assert route.called
 
@@ -383,17 +387,17 @@ async def test_run_search_success(item_descriptions, session):
 
 @respx.mock
 @pytest.mark.asyncio
-async def test_run_search_doesnotexist(session):
+async def test_run_search_aiter_doesnotexist(session):
     sid = 'search_id'
     route = respx.get(f'{TEST_SEARCHES_URL}/{sid}/results')
     route.return_value = httpx.Response(404)
 
     cl = DataClient(session, base_url=TEST_URL)
     with pytest.raises(exceptions.APIError):
-        items = await cl.run_search(sid)
+        item_aiter = cl.run_search_aiter(sid)
         # this won't throw the error until the iterator is processed
         # issue 476
-        [i async for i in items]
+        [i async for i in item_aiter]
 
     assert route.called
 
diff --git a/tests/integration/test_orders_api.py b/tests/integration/test_orders_api.py
index bb1a63e..8ef8caf 100644
--- a/tests/integration/test_orders_api.py
+++ b/tests/integration/test_orders_api.py
@@ -97,7 +97,7 @@ def test_OrderStates_passed():
 
 @respx.mock
 @pytest.mark.asyncio
-async def test_list_orders_basic(order_descriptions, session):
+async def test_list_orders_aiter_basic(order_descriptions, session):
     next_page_url = TEST_ORDERS_URL + 'blob/?page_marker=IAmATest'
 
     order1, order2, order3 = order_descriptions
@@ -116,13 +116,13 @@ async def test_list_orders_basic(order_descriptions, session):
     respx.get(next_page_url).return_value = mock_resp2
 
     cl = OrdersClient(session, base_url=TEST_URL)
-    orders = await cl.list_orders()
-    assert order_descriptions == [o async for o in orders]
+    order_aiter = cl.list_orders_aiter()
+    assert order_descriptions == [o async for o in order_aiter]
 
 
 @respx.mock
 @pytest.mark.asyncio
-async def test_list_orders_state(order_descriptions, session):
+async def test_list_orders_aiter_state_success(order_descriptions, session):
     list_url = TEST_ORDERS_URL + '?source_type=all&state=failed'
 
     order1, order2, _ = order_descriptions
@@ -139,26 +139,27 @@ async def test_list_orders_state(order_descriptions, session):
 
     # if the value of state doesn't get sent as a url parameter,
     # the mock will fail and this test will fail
-    orders = await cl.list_orders(state='failed')
-    assert [order1, order2] == [o async for o in orders]
+    order_aiter = cl.list_orders_aiter(state='failed')
+    assert [order1, order2] == [o async for o in order_aiter]
 
 
 @pytest.mark.asyncio
-async def test_list_orders_state_invalid_state(session):
+async def test_list_orders_aiter_state_invalid(session):
     cl = OrdersClient(session, base_url=TEST_URL)
 
     with pytest.raises(exceptions.ClientError):
-        await cl.list_orders(state='invalidstate')
+        order_aiter = cl.list_orders_aiter(state='invalidstate')
+        [o async for o in order_aiter]
 
 
 @respx.mock
 @pytest.mark.asyncio
 @pytest.mark.parametrize("limit,limited_list_length", [(None, 100), (0, 102),
                                                        (1, 1)])
-async def test_list_orders_limit(order_descriptions,
-                                 session,
-                                 limit,
-                                 limited_list_length):
+async def test_list_orders_aiter_limit(order_descriptions,
+                                       session,
+                                       limit,
+                                       limited_list_length):
     nono_page_url = None
 
     # Creating 102 (3x34) order descriptions
@@ -182,8 +183,8 @@ async def test_list_orders_limit(order_descriptions,
 
     cl = OrdersClient(session, base_url=TEST_URL)
 
-    orders = await cl.list_orders(limit=limit)
-    assert len([o async for o in orders]) == limited_list_length
+    order_aiter = cl.list_orders_aiter(limit=limit)
+    assert len([o async for o in order_aiter]) == limited_list_length
 
 
 @respx.mock
diff --git a/tests/integration/test_subscriptions_api.py b/tests/integration/test_subscriptions_api.py
index 4658660..65d84df 100644
--- a/tests/integration/test_subscriptions_api.py
+++ b/tests/integration/test_subscriptions_api.py
@@ -156,19 +156,19 @@ res_api_mock.route(
 
 @pytest.mark.asyncio
 @failing_api_mock
-async def test_list_subscriptions_failure():
+async def test_list_subscriptions_aiter_failure():
     """ServerError is raised if there is an internal server error (500)."""
     with pytest.raises(ServerError):
         async with Session() as session:
             client = SubscriptionsClient(session)
-            _ = [sub async for sub in client.list_subscriptions()]
+            _ = [sub async for sub in client.list_subscriptions_aiter()]
 
 
 @pytest.mark.parametrize("status, count", [({"created"}, 100), ({"failed"}, 0),
                                            (None, 100)])
 @pytest.mark.asyncio
 @api_mock
-async def test_list_subscriptions_success(
+async def test_list_subscriptions_aiter_success(
     status,
     count,
 ):
@@ -176,7 +176,7 @@ async def test_list_subscriptions_success(
     async with Session() as session:
         client = SubscriptionsClient(session)
         assert len([
-            sub async for sub in client.list_subscriptions(status=status)
+            sub async for sub in client.list_subscriptions_aiter(status=status)
         ]) == count
 
 
@@ -266,21 +266,21 @@ async def test_get_subscription_success(monkeypatch):
 
 @pytest.mark.asyncio
 @failing_api_mock
-async def test_get_results_failure():
+async def test_get_results_aiter_failure():
     """APIError is raised if there is a server error."""
     with pytest.raises(APIError):
         async with Session() as session:
             client = SubscriptionsClient(session)
-            _ = [res async for res in client.get_results("lolwut")]
+            _ = [res async for res in client.get_results_aiter("lolwut")]
 
 
 @pytest.mark.asyncio
 @res_api_mock
-async def test_get_results_success():
+async def test_get_results_aiter_success():
     """Subscription description fetched, has the expected items."""
     async with Session() as session:
         client = SubscriptionsClient(session)
-        results = [res async for res in client.get_results("42")]
+        results = [res async for res in client.get_results_aiter("42")]
         assert len(results) == 100
 
 
@@ -313,9 +313,9 @@ paging_cycle_api_mock.route(
 
 @pytest.mark.asyncio
 @paging_cycle_api_mock
-async def test_list_subscriptions_cycle_break():
+async def test_list_subscriptions_aiter_cycle_break():
     """PagingError is raised if there is a paging cycle."""
     with pytest.raises(PagingError):
         async with Session() as session:
             client = SubscriptionsClient(session)
-            _ = [sub async for sub in client.list_subscriptions()]
+            _ = [sub async for sub in client.list_subscriptions_aiter()]
diff --git a/tests/unit/test_order_request.py b/tests/unit/test_order_request.py
index 49e3554..0163357 100644
--- a/tests/unit/test_order_request.py
+++ b/tests/unit/test_order_request.py
@@ -251,10 +251,3 @@ def test_toar_tool():
     tt_empty = order_request.toar_tool()
     expected_empty = {'toar': {}}
     assert tt_empty == expected_empty
-
-
-@pytest.mark.parametrize("target_sensor", ["PS2", "Sentinel-2"])
-def test_harmonization_tool(target_sensor):
-    ht = order_request.harmonize_tool(target_sensor)
-    expected = {'harmonize': {'target_sensor': target_sensor}}
-    assert ht == expected