Repository: planetlabs/planet-client-python
Pull request #1096 (2025-01-29 17:03:00)

Add page_size arg to list_subscriptions()
This MR adds a new `page_size` arg to the `SubscriptionsClient.list_subscriptions()` method.  Without specifying a page size, `list_subscriptions()` will fall back on a default page size of 20 subscriptions per page that is defined in the API view handler.  For large numbers of subscriptions, this results in long execution times because `list_subscriptions()` is only getting 20 subscriptions per page, which results in more requests (and latency in transport) than is necessary.  

For example, I tested this out on a sample of 645 subscriptions.  The average execution time to get all 645 subscriptions using the default page size of 20 (`list_subscriptions()`) takes ~36 seconds because `list_subscriptions()` is making ~33 requests to the API:
```
(planet-client-python-3.12) ➜  Desktop python example.py
Average execution time: 36.07816209201701 seconds
Average execution time: 37.37259578681551 seconds
Average execution time: 36.77448189887218 seconds
Average execution time: 36.13904633792117 seconds
Average execution time: 35.90926657919772 seconds
```

When I define a page_size of 700 (`list_subscriptions(page_size=700)`) the average execution time to get all 645 subscriptions takes only ~3 seconds because `list_subscriptions()` only has to make one request to the API with the larger page size:
```
(planet-client-python-3.12) ➜  Desktop python example.py                          
Average execution time: 3.206969585036859 seconds
Average execution time: 2.6358285231981426 seconds
Average execution time: 2.8017055389937013 seconds
Average execution time: 2.6492277118377388 seconds
Average execution time: 2.728695012163371 seconds
```

Here is the contents of `example.py` that I used to test and record execution times:
```python
from planet import Session, SubscriptionsClient

async def get_subscriptions():
    async with Session() as sess:
        cl = SubscriptionsClient(sess)
        subscriptions = cl.list_subscriptions(page_size=700, limit=5000)
        sub_list = [i async for i in subscriptions]
        return sub_list

async def main():
    subs = await get_subscriptions()
    return subs

def run_main():
    return asyncio.run(main())

if __name__ == '__main__':
    import asyncio
    import timeit
    execution_times = timeit.repeat(run_main, number=10, repeat=5)
    for execution_time in execution_times:
        print(f"Average execution time: {execution_time} seconds")
```


Patch:
diff --git a/docs/cli/cli-subscriptions.md b/docs/cli/cli-subscriptions.md
index 5c822d5..f8dab7b 100644
--- a/docs/cli/cli-subscriptions.md
+++ b/docs/cli/cli-subscriptions.md
@@ -128,7 +128,7 @@ planet subscriptions list
 ```
 
 outputs the JSON for your first 100 subscriptions. If you'd like more you can set the `--limit`
-parameter higher, or you can set it to 0 and there will be no limit.
+parameter higher, or you can set it to 0 and there will be no limit.  By default the `list` command will request Subscriptions API with a page size of 500 subscriptions, and can be set lower or higher with the `--page-size` parameter.
 
 You can get nicer formatting with `--pretty` or pipe it into `jq`, just like the other Planet
 CLI’s.
diff --git a/planet/cli/subscriptions.py b/planet/cli/subscriptions.py
index d911ad2..a50f18e 100644
--- a/planet/cli/subscriptions.py
+++ b/planet/cli/subscriptions.py
@@ -118,6 +118,9 @@ def subscriptions(ctx, base_url):
     documentation
     for examples.""")
 @limit
+@click.option('--page-size',
+              type=click.INT,
+              help='Number of subscriptions to return per page.')
 @click.pass_context
 @translate_exceptions
 @coro
@@ -133,6 +136,7 @@ async def list_subscriptions_cmd(ctx,
                                  sort_by,
                                  updated,
                                  limit,
+                                 page_size,
                                  pretty):
     """Prints a sequence of JSON-encoded Subscription descriptions."""
     async with subscriptions_client(ctx) as client:
@@ -147,7 +151,8 @@ async def list_subscriptions_cmd(ctx,
                 status=status,
                 sort_by=sort_by,
                 updated=updated,
-                limit=limit):
+                limit=limit,
+                page_size=page_size):
             echo_json(sub, pretty)
 
 
diff --git a/planet/clients/subscriptions.py b/planet/clients/subscriptions.py
index 64d0d03..ffce142 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, Awaitable, Dict, Optional, Sequence, TypeVar, Union
+from typing import Any, AsyncIterator, Awaitable, Dict, Optional, Sequence, TypeVar
 
 from typing_extensions import Literal
 
@@ -65,19 +65,19 @@ class SubscriptionsClient:
         """block on an async function call, using the call_sync method of the session"""
         return self._session._call_sync(f)
 
-    async def list_subscriptions(
-            self,
-            status: Optional[Sequence[str]] = None,
-            limit: int = 100,
-            created: Optional[str] = None,
-            end_time: Optional[str] = None,
-            hosting: Optional[bool] = None,
-            name__contains: Optional[str] = None,
-            name: Optional[str] = None,
-            source_type: Optional[str] = None,
-            start_time: Optional[str] = None,
-            sort_by: Optional[str] = None,
-            updated: Optional[str] = None) -> AsyncIterator[dict]:
+    async def list_subscriptions(self,
+                                 status: Optional[Sequence[str]] = None,
+                                 limit: int = 100,
+                                 created: Optional[str] = None,
+                                 end_time: Optional[str] = None,
+                                 hosting: Optional[bool] = None,
+                                 name__contains: Optional[str] = None,
+                                 name: Optional[str] = None,
+                                 source_type: Optional[str] = None,
+                                 start_time: Optional[str] = None,
+                                 sort_by: Optional[str] = None,
+                                 updated: Optional[str] = None,
+                                 page_size: int = 500) -> AsyncIterator[dict]:
         """Iterate over list of account subscriptions with optional filtering.
 
         Note:
@@ -112,6 +112,7 @@ class SubscriptionsClient:
             updated (str): filter by updated time or interval.
             limit (int): limit the number of subscriptions in the
                 results. When set to 0, no maximum is applied.
+            page_size (int): number of subscriptions to return per page.
             TODO: user_id
 
         Datetime args (created, end_time, start_time, updated) can either be a
@@ -135,7 +136,7 @@ class SubscriptionsClient:
             """Navigates pages of messages about subscriptions."""
             ITEMS_KEY = 'subscriptions'
 
-        params: Dict[str, Union[str, Sequence[str], bool]] = {}
+        params: Dict[str, Any] = {}
         if created is not None:
             params['created'] = created
         if end_time is not None:
@@ -157,6 +158,8 @@ class SubscriptionsClient:
         if updated is not None:
             params['updated'] = updated
 
+        params['page_size'] = page_size
+
         try:
             response = await self._session.request(method='GET',
                                                    url=self._base_url,
diff --git a/planet/sync/subscriptions.py b/planet/sync/subscriptions.py
index 64153e2..edab358 100644
--- a/planet/sync/subscriptions.py
+++ b/planet/sync/subscriptions.py
@@ -45,7 +45,8 @@ class SubscriptionsAPI:
                            source_type: Optional[str] = None,
                            start_time: Optional[str] = None,
                            sort_by: Optional[str] = None,
-                           updated: Optional[str] = None) -> Iterator[dict]:
+                           updated: Optional[str] = None,
+                           page_size: int = 500) -> Iterator[dict]:
         """Iterate over list of account subscriptions with optional filtering.
 
         Note:
@@ -80,6 +81,7 @@ class SubscriptionsAPI:
             updated (str): filter by updated time or interval.
             limit (int): limit the number of subscriptions in the
                 results. When set to 0, no maximum is applied.
+            page_size (int): number of subscriptions to return per page.
             TODO: user_id
 
         Datetime args (created, end_time, start_time, updated) can either be a
@@ -109,7 +111,8 @@ class SubscriptionsAPI:
                                                   source_type,
                                                   start_time,
                                                   sort_by,
-                                                  updated)
+                                                  updated,
+                                                  page_size)
 
         try:
             while True:


Test patch:
diff --git a/tests/integration/test_subscriptions_api.py b/tests/integration/test_subscriptions_api.py
index 290ffcb..6f17cdd 100644
--- a/tests/integration/test_subscriptions_api.py
+++ b/tests/integration/test_subscriptions_api.py
@@ -267,6 +267,17 @@ async def test_list_subscriptions_filtering_and_sorting():
         ]) == 2
 
 
+@pytest.mark.parametrize("page_size, count", [(50, 100), (100, 100)])
+@pytest.mark.anyio
+@api_mock
+async def test_list_subscriptions_page_size_success(page_size, count):
+    async with Session() as session:
+        client = SubscriptionsClient(session, base_url=TEST_URL)
+        assert len([
+            sub async for sub in client.list_subscriptions(page_size=page_size)
+        ]) == count
+
+
 @pytest.mark.anyio
 @failing_api_mock
 async def test_create_subscription_failure():