Repository: planetlabs/planet-client-python

Issue:
Support source_type in SubscriptionsClient list_subscriptions
**Expected behavior**
The `source_type` parameter was added to the GET subscriptions endpoint some time ago

**Actual behavior (describe the problem)**
The parameter is not currently supported, making retrieval of subscriptions in organizations with many different sources less efficient.

**Related Issues**
#752 

**Workaround**
Filter subscriptions by source type after retrieving from API



Discussion:

Patch:
diff --git a/docs/cli/cli-subscriptions.md b/docs/cli/cli-subscriptions.md
index 9f7801e..4c42eec 100644
--- a/docs/cli/cli-subscriptions.md
+++ b/docs/cli/cli-subscriptions.md
@@ -134,7 +134,7 @@ parameter higher, or you can set it to 0 and there will be no limit.
 You can get nicer formatting with `--pretty` or pipe it into `jq`, just like the other Planet
 CLI’s.
 
-The `list` command also supports filtering by the status of the subscription:
+The `list` command supports filtering by the status of the subscription:
 
 ```sh
 planet subscriptions list --status running
@@ -143,6 +143,15 @@ planet subscriptions list --status running
 gives you just the currently active subscriptions. The other available statuses are:
 `cancelled`, `preparing`, `pending`, `completed`, `suspended`, and `failed`.
 
+The `list` command also supports filtering by the source type of the subscription:
+
+```sh
+planet subscriptions list --source-type catalog
+```
+
+only gives you subscriptions with the `catalog` source type. For the full list of available source types, 
+see [Subscription Source Types](https://developers.planet.com/docs/subscriptions/source/#subscription-source-types).
+
 ### Get Subscription
 
 To get the full details on a single subscription you can take the id from your list and use the
diff --git a/planet/cli/subscriptions.py b/planet/cli/subscriptions.py
index 46c6fec..1620549 100644
--- a/planet/cli/subscriptions.py
+++ b/planet/cli/subscriptions.py
@@ -78,14 +78,21 @@ def subscriptions(ctx, base_url):
     multiple=True,
     default=None,
     help="Select subscriptions in one or more states. Default is all.")
+@click.option(
+    '--source-type',
+    default=None,
+    help="Filter subscriptions by source type. See documentation for all "
+    "available types. Default is all.")
 @limit
 @click.pass_context
 @translate_exceptions
 @coro
-async def list_subscriptions_cmd(ctx, status, limit, pretty):
+async def list_subscriptions_cmd(ctx, status, source_type, limit, pretty):
     """Prints a sequence of JSON-encoded Subscription descriptions."""
     async with subscriptions_client(ctx) as client:
-        async for sub in client.list_subscriptions(status=status, limit=limit):
+        async for sub in client.list_subscriptions(status=status,
+                                                   source_type=source_type,
+                                                   limit=limit):
             echo_json(sub, pretty)
 
 
diff --git a/planet/clients/subscriptions.py b/planet/clients/subscriptions.py
index 1a2e020..0cdf2c4 100644
--- a/planet/clients/subscriptions.py
+++ b/planet/clients/subscriptions.py
@@ -1,7 +1,7 @@
 """Planet Subscriptions API Python client."""
 
 import logging
-from typing import AsyncIterator, Optional, Sequence
+from typing import AsyncIterator, Optional, Sequence, Dict, Union
 
 from typing_extensions import Literal
 
@@ -61,6 +61,7 @@ class SubscriptionsClient:
 
     async def list_subscriptions(self,
                                  status: Optional[Sequence[str]] = None,
+                                 source_type: Optional[str] = None,
                                  limit: int = 100) -> AsyncIterator[dict]:
         """Iterate over list of account subscriptions with optional filtering.
 
@@ -73,6 +74,7 @@ class SubscriptionsClient:
             status (Set[str]): pass subscriptions with status in this
                 set, filter out subscriptions with status not in this
                 set.
+            source_type (str): Product source type filter, to return only subscriptions with the matching source type
             limit (int): limit the number of subscriptions in the
                 results. When set to 0, no maximum is applied.
             TODO: user_id
@@ -89,7 +91,11 @@ class SubscriptionsClient:
             """Navigates pages of messages about subscriptions."""
             ITEMS_KEY = 'subscriptions'
 
-        params = {'status': [val for val in status or {}]}
+        params: Dict[str, Union[str, Sequence[str]]] = {
+            'status': [val for val in status or {}]
+        }
+        if source_type is not None:
+            params['source_type'] = source_type  # type: ignore
 
         try:
             response = await self._session.request(method='GET',


Test patch:
diff --git a/tests/integration/test_subscriptions_api.py b/tests/integration/test_subscriptions_api.py
index 7fbc9ba..d237b63 100644
--- a/tests/integration/test_subscriptions_api.py
+++ b/tests/integration/test_subscriptions_api.py
@@ -66,7 +66,20 @@ api_mock.route(M(url=TEST_URL),
                M(params__contains={'status': 'preparing'})).mock(
                    side_effect=[Response(200, json={'subscriptions': []})])
 
-# 4. No status requested. Response is the same as for 1.
+# 4. source_type: catalog requested. Response is the same as for 1.
+api_mock.route(
+    M(url=TEST_URL),
+    M(params__contains={'source_type': 'catalog'})).mock(side_effect=[
+        Response(200, json=page)
+        for page in result_pages(status={'running'}, size=40)
+    ])
+
+# 5. source_type: soil_water_content requested. Response has a single empty page.
+api_mock.route(M(url=TEST_URL),
+               M(params__contains={'source_type': 'soil_water_content'})).mock(
+                   side_effect=[Response(200, json={'subscriptions': []})])
+
+# 6. No status or source_type requested. Response is the same as for 1.
 api_mock.route(M(url=TEST_URL)).mock(
     side_effect=[Response(200, json=page) for page in result_pages(size=40)])
 
@@ -182,6 +195,24 @@ async def test_list_subscriptions_success(
         ]) == count
 
 
+@pytest.mark.parametrize("source_type, count",
+                         [("catalog", 100), ("soil_water_content", 0),
+                          (None, 100)])
+@pytest.mark.anyio
+@api_mock
+async def test_list_subscriptions_source_type_success(
+    source_type,
+    count,
+):
+    """Account subscriptions iterator yields expected descriptions."""
+    async with Session() as session:
+        client = SubscriptionsClient(session, base_url=TEST_URL)
+        assert len([
+            sub
+            async for sub in client.list_subscriptions(source_type=source_type)
+        ]) == count
+
+
 @pytest.mark.anyio
 @failing_api_mock
 async def test_create_subscription_failure():
diff --git a/tests/integration/test_subscriptions_cli.py b/tests/integration/test_subscriptions_cli.py
index 0349af7..50bd822 100644
--- a/tests/integration/test_subscriptions_cli.py
+++ b/tests/integration/test_subscriptions_cli.py
@@ -53,6 +53,8 @@ def invoke():
 
 @pytest.mark.parametrize('options,expected_count',
                          [(['--status=running'], 100), ([], 100),
+                          (['--source-type=catalog'], 100),
+                          (['--source-type=soil_water_content'], 0),
                           (['--limit=1', '--status=running'], 1),
                           (['--limit=2', '--pretty', '--status=running'], 2),
                           (['--limit=1', '--status=preparing'], 0)])