diff --git a/docs/cli/cli-orders.md b/docs/cli/cli-orders.md index 12b809c8..22f405de 100644 --- a/docs/cli/cli-orders.md +++ b/docs/cli/cli-orders.md @@ -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` @@ -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). diff --git a/docs/cli/cli-subscriptions.md b/docs/cli/cli-subscriptions.md index 3e08e44c..72449e4f 100644 --- a/docs/cli/cli-subscriptions.md +++ b/docs/cli/cli-subscriptions.md @@ -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` @@ -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 diff --git a/planet/cli/orders.py b/planet/cli/orders.py index 4bb645fd..d5a1fd7b 100644 --- a/planet/cli/orders.py +++ b/planet/cli/orders.py @@ -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, @@ -147,6 +149,7 @@ async def list(ctx, hosting, sort_by, destination_ref, + user_id, limit, pretty): """List orders @@ -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) diff --git a/planet/cli/subscriptions.py b/planet/cli/subscriptions.py index 4eaf1b5e..7884247f 100644 --- a/planet/cli/subscriptions.py +++ b/planet/cli/subscriptions.py @@ -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, @@ -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.""" @@ -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 diff --git a/planet/clients/orders.py b/planet/clients/orders.py index 5fbe13ba..ba4bf03f 100644 --- a/planet/clients/orders.py +++ b/planet/clients/orders.py @@ -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 @@ -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 @@ -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 @@ -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: @@ -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( diff --git a/planet/clients/subscriptions.py b/planet/clients/subscriptions.py index f6cae276..4e3ab91a 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 Any, AsyncIterator, Dict, Optional, Sequence, TypeVar, List +from typing import Any, AsyncIterator, Dict, Optional, Sequence, TypeVar, List, Union from typing_extensions import Literal @@ -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. @@ -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 @@ -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' @@ -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 diff --git a/planet/sync/orders.py b/planet/sync/orders.py index 8071dd26..a215bf7b 100644 --- a/planet/sync/orders.py +++ b/planet/sync/orders.py @@ -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 @@ -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 @@ -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. @@ -320,4 +324,5 @@ def list_orders(self, last_modified, hosting, destination_ref, - sort_by)) + sort_by, + user_id)) diff --git a/planet/sync/subscriptions.py b/planet/sync/subscriptions.py index 74d51b9f..a21630a4 100644 --- a/planet/sync/subscriptions.py +++ b/planet/sync/subscriptions.py @@ -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. @@ -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 @@ -117,6 +119,7 @@ def list_subscriptions(self, sort_by, updated, destination_ref, + user_id, page_size)) def create_subscription(self, request: Dict) -> Dict: diff --git a/tests/integration/test_orders_api.py b/tests/integration/test_orders_api.py index b251723b..d2f8e253 100644 --- a/tests/integration/test_orders_api.py +++ b/tests/integration/test_orders_api.py @@ -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' diff --git a/tests/integration/test_orders_cli.py b/tests/integration/test_orders_cli.py index dfe38249..8f0a663d 100644 --- a/tests/integration/test_orders_cli.py +++ b/tests/integration/test_orders_cli.py @@ -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)]) diff --git a/tests/integration/test_subscriptions_api.py b/tests/integration/test_subscriptions_api.py index 25b299de..dc743018 100644 --- a/tests/integration/test_subscriptions_api.py +++ b/tests/integration/test_subscriptions_api.py @@ -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 diff --git a/tests/integration/test_subscriptions_cli.py b/tests/integration/test_subscriptions_cli.py index f252e3ce..f40ce048 100644 --- a/tests/integration/test_subscriptions_cli.py +++ b/tests/integration/test_subscriptions_cli.py @@ -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):