Skip to content

orders + subscriptions: support user_id query param #1188

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Aug 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/cli/cli-orders.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ The `list` command supports filtering on a variety of fields:
* `--last-modified`: Filter on the order's last modified time or an interval of last modified times.
* `--hosting`: Filter on orders containing a hosting location (e.g. SentinelHub). Accepted values are `true` or `false`.
* `--destination-ref`: Filter on orders created with the provided destination reference.
* `--user-id`: Filter by user ID. Only available to organization admins. Accepts "all" or a specific user ID.

Datetime args (`--created-on` and `--last-modified`) can either be a date-time or an interval, open or closed. Date and time expressions adhere to RFC 3339. Open intervals are expressed using double-dots.
* A date-time: `2018-02-12T23:20:50Z`
Expand Down Expand Up @@ -120,6 +121,16 @@ To list orders with a name containing `xyz`:
planet orders list --name-contains xyz
```

To list orders for all users in your organization (organization admin only):
```sh
planet orders list --user-id all
```

To list orders for a specific user ID (organization admin only):
```sh
planet orders list --user-id 12345
```

#### Sorting

The `list` command also supports sorting the orders on one or more fields: `name`, `created_on`, `state`, and `last_modified`. The sort direction can be specified by appending ` ASC` or ` DESC` to the field name (default is ascending).
Expand Down
11 changes: 11 additions & 0 deletions docs/cli/cli-subscriptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ The `list` command supports filtering on a variety of fields:
* `--status`: Filter on the status of the subscription. Status options include `running`, `cancelled`, `preparing`, `pending`, `completed`, `suspended`, and `failed`. Multiple status args are allowed.
* `--updated`: Filter on the subscription update time or an interval of updated times.
* `--destination-ref`: Filter on subscriptions created with the provided destination reference.
* `--user-id`: Filter by user ID. Only available to organization admins. Accepts "all" or a specific user ID.

Datetime args (`--created`, `end-time`, `--start-time`, and `--updated`) can either be a date-time or an interval, open or closed. Date and time expressions adhere to RFC 3339. Open intervals are expressed using double-dots.
* A date-time: `2018-02-12T23:20:50Z`
Expand All @@ -171,6 +172,16 @@ To list subscriptions with an end time after Jan 1, 2025:
planet subscriptions list --end-time 2025-01-01T00:00:00Z/..
```

To list subscriptions for all users in your organization (organization admin only):
```sh
planet subscriptions list --user-id all
```

To list subscriptions for a specific user ID (organization admin only):
```sh
planet subscriptions list --user-id 12345
```

To list subscriptions with a hosting location:
```sh
planet subscriptions list --hosting true
Expand Down
4 changes: 4 additions & 0 deletions planet/cli/orders.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ def orders(ctx, base_url):
@click.option(
'--destination-ref',
help="Filter by orders created with the provided destination reference.")
@click.option('--user-id',
help="Filter by user ID. Accepts 'all' or a specific user ID.")
@limit
@pretty
async def list(ctx,
Expand All @@ -147,6 +149,7 @@ async def list(ctx,
hosting,
sort_by,
destination_ref,
user_id,
limit,
pretty):
"""List orders
Expand All @@ -167,6 +170,7 @@ async def list(ctx,
hosting=hosting,
sort_by=sort_by,
destination_ref=destination_ref,
user_id=user_id,
limit=limit):
echo_json(o, pretty)

Expand Down
6 changes: 5 additions & 1 deletion planet/cli/subscriptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ def subscriptions(ctx, base_url):
'--destination-ref',
help="Filter subscriptions created with the provided destination reference."
)
@click.option('--user-id',
help="Filter by user ID. Accepts 'all' or a specific user ID.")
@limit
@click.option('--page-size',
type=click.INT,
Expand All @@ -130,6 +132,7 @@ async def list_subscriptions_cmd(ctx,
updated,
limit,
destination_ref,
user_id,
page_size,
pretty):
"""Prints a sequence of JSON-encoded Subscription descriptions."""
Expand All @@ -146,7 +149,8 @@ async def list_subscriptions_cmd(ctx,
'sort_by': sort_by,
'updated': updated,
'limit': limit,
'destination_ref': destination_ref
'destination_ref': destination_ref,
'user_id': user_id
}
if page_size is not None:
list_subscriptions_kwargs['page_size'] = page_size
Expand Down
11 changes: 8 additions & 3 deletions planet/clients/orders.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import asyncio
import logging
import time
from typing import AsyncIterator, Callable, Dict, List, Optional, Sequence, TypeVar, Union
from typing import Any, AsyncIterator, Callable, Dict, List, Optional, TypeVar, Union
import uuid
import json
import hashlib
Expand Down Expand Up @@ -474,7 +474,8 @@ async def list_orders(
last_modified: Optional[str] = None,
hosting: Optional[bool] = None,
destination_ref: Optional[str] = None,
sort_by: Optional[str] = None) -> AsyncIterator[dict]:
sort_by: Optional[str] = None,
user_id: Optional[Union[str, int]] = None) -> AsyncIterator[dict]:
"""Iterate over the list of stored orders.

By default, order descriptions are sorted by creation date with the last created
Expand Down Expand Up @@ -510,6 +511,8 @@ async def list_orders(
* "name"
* "name DESC"
* "name,state DESC,last_modified"
user_id (str or int): filter by user ID. Only available to organization admins.
Accepts "all" or a specific user ID.

Datetime args (created_on and last_modified) can either be a date-time or an
interval, open or closed. Date and time expressions adhere to RFC 3339. Open
Expand All @@ -528,7 +531,7 @@ async def list_orders(
planet.exceptions.ClientError: If state is not valid.
"""
url = self._orders_url()
params: Dict[str, Union[str, Sequence[str], bool]] = {}
params: Dict[str, Any] = {}
if source_type is not None:
params["source_type"] = source_type
else:
Expand All @@ -547,6 +550,8 @@ async def list_orders(
params["sort_by"] = sort_by
if destination_ref is not None:
params["destination_ref"] = destination_ref
if user_id is not None:
params["user_id"] = user_id
if state:
if state not in ORDER_STATE_SEQUENCE:
raise exceptions.ClientError(
Expand Down
9 changes: 6 additions & 3 deletions planet/clients/subscriptions.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Planet Subscriptions API Python client."""

import logging
from typing import Any, AsyncIterator, Dict, Optional, Sequence, TypeVar, List
from typing import Any, AsyncIterator, Dict, Optional, Sequence, TypeVar, List, Union

from typing_extensions import Literal

Expand Down Expand Up @@ -71,6 +71,7 @@ async def list_subscriptions(self,
sort_by: Optional[str] = None,
updated: Optional[str] = None,
destination_ref: Optional[str] = None,
user_id: Optional[Union[str, int]] = None,
page_size: int = 500) -> AsyncIterator[dict]:
"""Iterate over list of account subscriptions with optional filtering.

Expand Down Expand Up @@ -108,6 +109,8 @@ async def list_subscriptions(self,
updated (str): filter by updated time or interval.
destination_ref (str): filter by subscriptions created with the
provided destination reference.
user_id (str or int): filter by user ID. Only available to organization admins.
Accepts "all" or a specific user ID.
page_size (int): number of subscriptions to return per page.

Datetime args (created, end_time, start_time, updated) can either be a
Expand All @@ -127,8 +130,6 @@ async def list_subscriptions(self,
ClientError: on a client error.
"""

# TODO from old doc string, which breaks strict document checking:
# Add Parameter user_id
class _SubscriptionsPager(Paged):
"""Navigates pages of messages about subscriptions."""
ITEMS_KEY = 'subscriptions'
Expand Down Expand Up @@ -156,6 +157,8 @@ class _SubscriptionsPager(Paged):
params['updated'] = updated
if destination_ref is not None:
params['destination_ref'] = destination_ref
if user_id is not None:
params['user_id'] = user_id

params['page_size'] = page_size

Expand Down
31 changes: 18 additions & 13 deletions planet/sync/orders.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# License for the specific language governing permissions and limitations under
# the License.
"""Functionality for interacting with the orders api"""
from typing import Any, Callable, Dict, Iterator, List, Optional
from typing import Any, Callable, Dict, Iterator, List, Optional, Union

from pathlib import Path
from ..http import Session
Expand Down Expand Up @@ -252,17 +252,19 @@ def wait(self,
return self._client._call_sync(
self._client.wait(order_id, state, delay, max_attempts, callback))

def list_orders(self,
state: Optional[str] = None,
limit: int = 100,
source_type: Optional[str] = None,
name: Optional[str] = None,
name__contains: Optional[str] = None,
created_on: Optional[str] = None,
last_modified: Optional[str] = None,
hosting: Optional[bool] = None,
destination_ref: Optional[str] = None,
sort_by: Optional[str] = None) -> Iterator[dict]:
def list_orders(
self,
state: Optional[str] = None,
limit: int = 100,
source_type: Optional[str] = None,
name: Optional[str] = None,
name__contains: Optional[str] = None,
created_on: Optional[str] = None,
last_modified: Optional[str] = None,
hosting: Optional[bool] = None,
destination_ref: Optional[str] = None,
sort_by: Optional[str] = None,
user_id: Optional[Union[str, int]] = None) -> Iterator[dict]:
"""Iterate over the list of stored orders.

By default, order descriptions are sorted by creation date with the last created
Expand Down Expand Up @@ -296,6 +298,8 @@ def list_orders(self,
* "name"
* "name DESC"
* "name,state DESC,last_modified"
user_id (str or int): filter by user ID. Only available to organization admins.
Accepts "all" or a specific user ID.
limit (int): maximum number of results to return. When set to 0, no
maximum is applied.

Expand All @@ -320,4 +324,5 @@ def list_orders(self,
last_modified,
hosting,
destination_ref,
sort_by))
sort_by,
user_id))
5 changes: 4 additions & 1 deletion planet/sync/subscriptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ def list_subscriptions(self,
sort_by: Optional[str] = None,
updated: Optional[str] = None,
destination_ref: Optional[str] = None,
user_id: Optional[Union[str, int]] = None,
page_size: int = 500) -> Iterator[dict]:
"""Iterate over list of account subscriptions with optional filtering.

Expand Down Expand Up @@ -82,10 +83,11 @@ def list_subscriptions(self,
updated (str): filter by updated time or interval.
destination_ref (str): filter by subscriptions created with the
provided destination reference.
user_id (str or int): filter by user ID. Only available to organization admins.
Accepts "all" or a specific user ID.
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
date-time or an interval, open or closed. Date and time expressions adhere
Expand Down Expand Up @@ -117,6 +119,7 @@ def list_subscriptions(self,
sort_by,
updated,
destination_ref,
user_id,
page_size))

def create_subscription(self, request: Dict) -> Dict:
Expand Down
50 changes: 50 additions & 0 deletions tests/integration/test_orders_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,56 @@ async def test_list_orders_filtering_and_sorting(order_descriptions, session):
]


@respx.mock
@pytest.mark.anyio
@pytest.mark.parametrize("user_id", ["all", "123", 456])
async def test_list_orders_user_id_filtering(order_descriptions,
session,
user_id):
"""Test user_id parameter filtering for organization admins."""
list_url = TEST_ORDERS_URL + f'?source_type=all&user_id={user_id}'

order1, order2, _ = order_descriptions

page1_response = {
"_links": {
"_self": "string"
}, "orders": [order1, order2]
}
mock_resp = httpx.Response(HTTPStatus.OK, json=page1_response)
respx.get(list_url).return_value = mock_resp

cl = OrdersClient(session, base_url=TEST_URL)

# if the value of user_id doesn't get sent as a url parameter,
# the mock will fail and this test will fail
assert [order1,
order2] == [o async for o in cl.list_orders(user_id=user_id)]


@respx.mock
def test_list_orders_user_id_sync(order_descriptions, session):
"""Test sync client user_id parameter for organization admins."""
list_url = TEST_ORDERS_URL + '?source_type=all&user_id=all'

order1, order2, _ = order_descriptions

page1_response = {
"_links": {
"_self": "string"
}, "orders": [order1, order2]
}
mock_resp = httpx.Response(HTTPStatus.OK, json=page1_response)
respx.get(list_url).return_value = mock_resp

pl = Planet()
pl.orders._client._base_url = TEST_URL

# if the value of user_id doesn't get sent as a url parameter,
# the mock will fail and this test will fail
assert [order1, order2] == list(pl.orders.list_orders(user_id='all'))


@respx.mock
def test_list_orders_state_success_sync(order_descriptions, session):
list_url = TEST_ORDERS_URL + '?source_type=all&state=failed'
Expand Down
24 changes: 24 additions & 0 deletions tests/integration/test_orders_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,30 @@ def test_cli_orders_list_filtering_and_sorting(invoke, order_descriptions):
assert result.output == sequence + '\n'


@respx.mock
@pytest.mark.parametrize("user_id", ["all", "123"])
def test_cli_orders_list_user_id(invoke, order_descriptions, user_id):
"""Test CLI user_id parameter for organization admins."""
list_url = TEST_ORDERS_URL + f'?source_type=all&user_id={user_id}'

order1, order2, _ = order_descriptions

page1_response = {
"_links": {
"_self": "string"
}, "orders": [order1, order2]
}
mock_resp = httpx.Response(HTTPStatus.OK, json=page1_response)
respx.get(list_url).return_value = mock_resp

# if the value of user_id doesn't get sent as a url parameter,
# the mock will fail and this test will fail
result = invoke(['list', '--user-id', user_id])
assert result.exit_code == 0
sequence = '\n'.join([json.dumps(o) for o in [order1, order2]])
assert result.output == sequence + '\n'


@respx.mock
@pytest.mark.parametrize("limit,limited_list_length", [(None, 100), (0, 102),
(1, 1)])
Expand Down
12 changes: 12 additions & 0 deletions tests/integration/test_subscriptions_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,18 @@ async def test_list_subscriptions_filtering_and_sorting():
]) == 2


@pytest.mark.parametrize("user_id", ["all", "123", 456])
@pytest.mark.anyio
@api_mock
async def test_list_subscriptions_user_id_filtering(user_id):
"""Test user_id parameter filtering for organization admins."""
async with Session() as session:
client = SubscriptionsClient(session, base_url=TEST_URL)
assert len([
sub async for sub in client.list_subscriptions(user_id=user_id)
]) == 100


@pytest.mark.parametrize("page_size, count", [(50, 100), (100, 100)])
@pytest.mark.anyio
@api_mock
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/test_subscriptions_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def _invoke(extra_args, runner=None, **kwargs):
'--hosting=true',
'--sort-by=name DESC'
],
2)])
2), (['--user-id=all'], 100), (['--user-id=12345'], 100)])
@api_mock
# Remember, parameters come before fixtures in the function definition.
def test_subscriptions_list_options(invoke, options, expected_count):
Expand Down