From 4615abc6f21bdff75dc172245e299f44d06da70b Mon Sep 17 00:00:00 2001 From: Jennifer Reiber Kyle Date: Fri, 28 Jan 2022 16:47:08 -0800 Subject: [PATCH 01/13] move SECRET_FILE_PATH to constants module --- planet/auth.py | 7 +++---- planet/constants.py | 3 +++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/planet/auth.py b/planet/auth.py index 8540053e3..6c62ef602 100644 --- a/planet/auth.py +++ b/planet/auth.py @@ -21,15 +21,14 @@ import httpx import jwt -from . import constants, http, models +from . import http, models +from .constants import PLANET_BASE_URL, SECRET_FILE_PATH from .exceptions import AuthException - LOGGER = logging.getLogger(__name__) -BASE_URL = constants.PLANET_BASE_URL + 'v0/auth/' +BASE_URL = PLANET_BASE_URL + 'v0/auth/' ENV_API_KEY = 'PL_API_KEY' -SECRET_FILE_PATH = os.path.join(os.path.expanduser('~'), '.planet.json') class Auth(metaclass=abc.ABCMeta): diff --git a/planet/constants.py b/planet/constants.py index 8cb5fa33a..dc320a828 100644 --- a/planet/constants.py +++ b/planet/constants.py @@ -12,5 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. '''Constants used across the code base''' +import os PLANET_BASE_URL = 'https://api.planet.com/' + +SECRET_FILE_PATH = os.path.join(os.path.expanduser('~'), '.planet.json') From e5ac4999bc8f8685d655f046f6d4da79288a1446 Mon Sep 17 00:00:00 2001 From: Jennifer Reiber Kyle Date: Fri, 28 Jan 2022 22:33:37 -0800 Subject: [PATCH 02/13] move auth cli tests to integration tests --- tests/conftest.py | 3 + tests/integration/test_auth_cli.py | 88 ++++++++++++++++++++++++------ tests/unit/test_cli_auth.py | 74 ------------------------- 3 files changed, 74 insertions(+), 91 deletions(-) delete mode 100644 tests/unit/test_cli_auth.py diff --git a/tests/conftest.py b/tests/conftest.py index 6f4b57022..1dc027885 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -19,6 +19,7 @@ from planet.auth import _SecretFile + _here = Path(os.path.abspath(os.path.dirname(__file__))) _test_data_path = _here / 'data' @@ -29,6 +30,8 @@ def test_secretfile_read(): def mockreturn(self): return {'key': 'testkey'} + # monkeypatch fixture is not available above a function scope + # usage: https://docs.pytest.org/en/6.2.x/reference.html#pytest.MonkeyPatch with pytest.MonkeyPatch.context() as mp: mp.setattr(_SecretFile, 'read', mockreturn) yield diff --git a/tests/integration/test_auth_cli.py b/tests/integration/test_auth_cli.py index 02a57f052..fb38f1bb0 100644 --- a/tests/integration/test_auth_cli.py +++ b/tests/integration/test_auth_cli.py @@ -12,7 +12,7 @@ # License for the specific language governing permissions and limitations under # the License. from http import HTTPStatus -from unittest.mock import MagicMock +import json from click.testing import CliRunner import httpx @@ -20,36 +20,90 @@ import pytest import respx -import planet from planet.cli import cli TEST_URL = 'http://MockNotRealURL/' -@pytest.fixture(autouse=True) -def patch_session(monkeypatch): - '''Make sure we don't actually make any http calls''' - monkeypatch.setattr(planet, 'Session', MagicMock(spec=planet.Session)) +# skip the global mock of _SecretFile.read +# for this module +@pytest.fixture(autouse=True, scope='module') +def test_secretfile_read(): + return -@respx.mock -@pytest.mark.asyncio -def test_cli_auth_init_base_url(): - '''Test base url option +@pytest.fixture +def redirect_secretfile(tmp_path): + '''patch the cli so it works with a temporary secretfile - Uses the auth init path to ensure the base url is changed to the mocked - url. So, ends up testing the auth init path somewhat as well + this is to avoid collisions with the actual planet secretfile ''' - login_url = TEST_URL + 'login' + secretfile_path = tmp_path / 'secret.json' + + with pytest.MonkeyPatch.context() as mp: + mp.setattr(cli.auth.planet.auth, 'SECRET_FILE_PATH', secretfile_path) + yield secretfile_path + + +@respx.mock +def test_cli_auth_init_success(redirect_secretfile): + """Test the successful auth init path + + Also tests the base-url command, since we will get an exception + if the base url is not changed to the mocked url + """ + login_url = f'{TEST_URL}login' - payload = {'api_key': 'iamakey'} + payload = {'api_key': 'test_cli_auth_init_success_key'} resp = {'token': jwt.encode(payload, 'key')} mock_resp = httpx.Response(HTTPStatus.OK, json=resp) respx.post(login_url).return_value = mock_resp result = CliRunner().invoke( - cli.main, - args=['auth', '--base-url', TEST_URL, 'init'], - input='email\npw\n') + cli.main, + args=['auth', '--base-url', TEST_URL, 'init'], + input='email\npw\n') + # we would get a 'url not mocked' exception if the base url wasn't + # changed to the mocked url assert not result.exception + + assert 'Initialized' in result.output + + +@respx.mock +def test_cli_auth_init_bad_pw(redirect_secretfile): + login_url = f'{TEST_URL}login' + + resp = {"errors": None, + "message": "Invalid email or password", + "status": 401, + "success": False} + mock_resp = httpx.Response(401, json=resp) + respx.post(login_url).return_value = mock_resp + + result = CliRunner().invoke( + cli.main, + args=['auth', '--base-url', TEST_URL, 'init'], + input='email\npw\n') + + assert result.exception + assert 'Error: Incorrect email or password.\n' in result.output + + +def test_cli_auth_value_success(redirect_secretfile): + key = 'test_cli_auth_value_success_key' + content = {'key': key} + with open(redirect_secretfile, 'w') as f: + json.dump(content, f) + + result = CliRunner().invoke(cli.main, ['auth', 'value']) + assert not result.exception + assert result.output == f'{key}\n' + + +def test_cli_auth_value_failure(redirect_secretfile): + result = CliRunner().invoke(cli.main, ['auth', 'value']) + assert result.exception + assert 'Error: Auth information does not exist or is corrupted.' \ + in result.output diff --git a/tests/unit/test_cli_auth.py b/tests/unit/test_cli_auth.py deleted file mode 100644 index ff98e46d6..000000000 --- a/tests/unit/test_cli_auth.py +++ /dev/null @@ -1,74 +0,0 @@ -# Copyright 2022 Planet Labs, PBC. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may not -# use this file except in compliance with the License. You may obtain a copy of -# the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations under -# the License. -from unittest.mock import MagicMock - -from click.testing import CliRunner -import pytest - -import planet -from planet.cli import cli - - -@pytest.fixture -def runner(): - return CliRunner() - - -@pytest.fixture(autouse=True) -def patch_session(monkeypatch): - '''Make sure we don't actually make any http calls''' - monkeypatch.setattr(planet, 'Session', MagicMock(spec=planet.Session)) - - -def test_cli_auth_init_bad_pw(runner, monkeypatch): - def apiexcept(*args, **kwargs): - raise planet.exceptions.APIException('nope') - monkeypatch.setattr(planet.Auth, 'from_login', apiexcept) - result = runner.invoke( - cli.main, - args=['auth', 'init'], - input='email\npw\n') - assert 'Error: nope' in result.output - - -def test_cli_auth_init_success(runner, monkeypatch): - mock_api_auth = MagicMock(spec=planet.auth.APIKeyAuth) - mock_auth = MagicMock(spec=planet.Auth) - mock_auth.from_login.return_value = mock_api_auth - monkeypatch.setattr(planet, 'Auth', mock_auth) - - result = runner.invoke( - cli.main, - args=['auth', 'init'], - input='email\npw\n') - mock_auth.from_login.assert_called_once() - mock_api_auth.write.assert_called_once() - assert 'Initialized' in result.output - - -def test_cli_auth_value_failure(runner, monkeypatch): - def authexception(*args, **kwargs): - raise planet.auth.AuthException - - monkeypatch.setattr(planet.Auth, 'from_file', authexception) - - result = runner.invoke(cli.main, ['auth', 'value']) - assert 'Error: Auth information does not exist or is corrupted.' \ - in result.output - - -def test_cli_auth_value_success(runner): - result = runner.invoke(cli.main, ['auth', 'value']) - assert not result.exception - assert result.output == 'testkey\n' From 88cfe830c14e63ad198fe4b8acea05b2ef9a6f31 Mon Sep 17 00:00:00 2001 From: Jennifer Reiber Kyle Date: Fri, 28 Jan 2022 23:39:24 -0800 Subject: [PATCH 03/13] base_url is just the planet base url, use f-strings for url building, remove ref to auth in example The orders API includes multiple api endpoints. They all begin with 'https://api.planet.com/' but then the relative paths are: 'compute/ops/stats/orders/v2/', 'compute/ops/orders/v2/', and 'compute/ops/bulk/orders/v2/'. The auth API endpoint begins with the same url and the relative path is 'v0/auth/'. Setting the base url for auth to 'https://api.planet.com/v0/auth/' makes sense for the auth client, but for the orders client we cannot use one of the relative paths for the base url. We could use `compute/ops` but that doesn't actually get us to the orders API endpoint. Backing off to just have the base url be 'https://api.planet.com/' seems less confusing than having the base url for the orders API be some portion of the full path to the endpoint and having the base url for the auth API be the full path. --- planet/auth.py | 14 ++++--- planet/clients/orders.py | 29 ++++++------- tests/integration/test_auth_api.py | 15 +++---- tests/integration/test_auth_cli.py | 10 ++--- tests/integration/test_orders_api.py | 63 +++++++++++++--------------- 5 files changed, 59 insertions(+), 72 deletions(-) diff --git a/planet/auth.py b/planet/auth.py index 6c62ef602..f9c4592f4 100644 --- a/planet/auth.py +++ b/planet/auth.py @@ -27,7 +27,8 @@ LOGGER = logging.getLogger(__name__) -BASE_URL = PLANET_BASE_URL + 'v0/auth/' +AUTH_PATH = 'v0/auth/' + ENV_API_KEY = 'PL_API_KEY' @@ -109,8 +110,8 @@ def from_login( Parameters: email: Planet account email address. password: Planet account password. - base_url: The base URL to use. Defaults to production - authentication API base url. + base_url: The base URL to use. Defaults to Planet production + services base url. ''' cl = AuthClient(base_url=base_url) auth_data = cl.login(email, password) @@ -154,9 +155,10 @@ def __init__( ): """ Parameters: - base_url: Alternate authentication api base URL. + base_url: The base URL to use. Defaults to Planet production + services base url. """ - self._base_url = base_url or BASE_URL + self._base_url = base_url or PLANET_BASE_URL if not self._base_url.endswith('/'): self._base_url += '/' @@ -178,7 +180,7 @@ def login( A JSON object containing an `api_key` property with the user's API_KEY. ''' - url = self._base_url + 'login' + url = f'{self._base_url}{AUTH_PATH}login' data = {'email': email, 'password': password } diff --git a/planet/clients/orders.py b/planet/clients/orders.py index 274b8f61c..691e85954 100644 --- a/planet/clients/orders.py +++ b/planet/clients/orders.py @@ -20,15 +20,15 @@ import typing import uuid -from .. import constants, exceptions +from .. import exceptions +from ..constants import PLANET_BASE_URL from ..http import Session from ..models import Order, Orders, Request, Response, StreamingBody -BASE_URL = constants.PLANET_BASE_URL + 'compute/ops/' -STATS_PATH = 'stats/orders/v2/' -ORDERS_PATH = 'orders/v2/' -BULK_PATH = 'bulk/orders/v2/' +STATS_PATH = 'compute/ops/stats/orders/v2/' +ORDERS_PATH = 'compute/ops/orders/v2/' +BULK_PATH = 'compute/ops/bulk/orders/v2/' # Order states https://developers.planet.com/docs/orders/ordering/#order-states ORDERS_STATES_COMPLETE = ['success', 'partial', 'cancelled', 'failed'] @@ -52,16 +52,13 @@ class OrdersClient(): >>> from planet import Session, OrdersClient >>> >>> async def main(): - ... auth = ('example_api_key', '') - ... async with Session(auth=auth) as sess: + ... async with Session() as sess: ... cl = OrdersClient(sess) ... # use client here ... >>> asyncio.run(main()) ``` - - """ def __init__( self, @@ -71,12 +68,12 @@ def __init__( """ Parameters: session: Open session connected to server. - base_url: The base URL to use. Defaults to production orders API - base url. + base_url: The base URL to use. Defaults to Planet production + services base url. """ self._session = session - self._base_url = base_url or BASE_URL + self._base_url = base_url or PLANET_BASE_URL if not self._base_url.endswith('/'): self._base_url += '/' @@ -92,17 +89,17 @@ def _check_order_id(oid): raise OrdersClientException(msg) def _orders_url(self): - return self._base_url + ORDERS_PATH + return f'{self._base_url}{ORDERS_PATH}' def _stats_url(self): - return self._base_url + STATS_PATH + return f'{self._base_url}{STATS_PATH}' def _order_url(self, order_id): self._check_order_id(order_id) - return self._orders_url() + order_id + return f'{self._orders_url()}{order_id}' def _bulk_url(self): - return self._base_url + BULK_PATH + return f'{self._base_url}{BULK_PATH}' def _request(self, url, method, data=None, params=None, json=None): return Request(url, method=method, data=data, params=params, json=json) diff --git a/tests/integration/test_auth_api.py b/tests/integration/test_auth_api.py index 0a6e53266..9e2d425be 100644 --- a/tests/integration/test_auth_api.py +++ b/tests/integration/test_auth_api.py @@ -20,22 +20,21 @@ import respx from planet import exceptions -from planet.auth import AuthClient +from planet.auth import AUTH_PATH, AuthClient TEST_URL = 'http://MockNotRealURL/' +TEST_LOGIN_URL = f'{TEST_URL}{AUTH_PATH}login' LOGGER = logging.getLogger(__name__) @respx.mock def test_AuthClient_success(): - login_url = TEST_URL + 'login' - payload = {'api_key': 'iamakey'} resp = {'token': jwt.encode(payload, 'key')} mock_resp = httpx.Response(HTTPStatus.OK, json=resp) - respx.post(login_url).return_value = mock_resp + respx.post(TEST_LOGIN_URL).return_value = mock_resp cl = AuthClient(base_url=TEST_URL) auth_data = cl.login('email', 'password') @@ -45,8 +44,6 @@ def test_AuthClient_success(): @respx.mock def test_AuthClient_invalid_email(): - login_url = TEST_URL + 'login' - resp = { "errors": { "email": [ @@ -58,7 +55,7 @@ def test_AuthClient_invalid_email(): "success": False } mock_resp = httpx.Response(400, json=resp) - respx.post(login_url).return_value = mock_resp + respx.post(TEST_LOGIN_URL).return_value = mock_resp cl = AuthClient(base_url=TEST_URL) with pytest.raises(exceptions.APIException, @@ -68,8 +65,6 @@ def test_AuthClient_invalid_email(): @respx.mock def test_AuthClient_invalid_password(): - login_url = TEST_URL + 'login' - resp = { "errors": None, "message": "Invalid email or password", @@ -77,7 +72,7 @@ def test_AuthClient_invalid_password(): "success": False } mock_resp = httpx.Response(401, json=resp) - respx.post(login_url).return_value = mock_resp + respx.post(TEST_LOGIN_URL).return_value = mock_resp cl = AuthClient(base_url=TEST_URL) with pytest.raises(exceptions.APIException, diff --git a/tests/integration/test_auth_cli.py b/tests/integration/test_auth_cli.py index fb38f1bb0..a1945be94 100644 --- a/tests/integration/test_auth_cli.py +++ b/tests/integration/test_auth_cli.py @@ -20,9 +20,11 @@ import pytest import respx +from planet.auth import AUTH_PATH from planet.cli import cli TEST_URL = 'http://MockNotRealURL/' +TEST_LOGIN_URL = f'{TEST_URL}{AUTH_PATH}login' # skip the global mock of _SecretFile.read @@ -52,12 +54,10 @@ def test_cli_auth_init_success(redirect_secretfile): Also tests the base-url command, since we will get an exception if the base url is not changed to the mocked url """ - login_url = f'{TEST_URL}login' - payload = {'api_key': 'test_cli_auth_init_success_key'} resp = {'token': jwt.encode(payload, 'key')} mock_resp = httpx.Response(HTTPStatus.OK, json=resp) - respx.post(login_url).return_value = mock_resp + respx.post(TEST_LOGIN_URL).return_value = mock_resp result = CliRunner().invoke( cli.main, @@ -73,14 +73,12 @@ def test_cli_auth_init_success(redirect_secretfile): @respx.mock def test_cli_auth_init_bad_pw(redirect_secretfile): - login_url = f'{TEST_URL}login' - resp = {"errors": None, "message": "Invalid email or password", "status": 401, "success": False} mock_resp = httpx.Response(401, json=resp) - respx.post(login_url).return_value = mock_resp + respx.post(TEST_LOGIN_URL).return_value = mock_resp result = CliRunner().invoke( cli.main, diff --git a/tests/integration/test_orders_api.py b/tests/integration/test_orders_api.py index 3e9a0ecab..472fd5b5e 100644 --- a/tests/integration/test_orders_api.py +++ b/tests/integration/test_orders_api.py @@ -25,9 +25,13 @@ import respx from planet import OrdersClient, clients, exceptions, reporting - +from planet.clients.orders import BULK_PATH, ORDERS_PATH, STATS_PATH TEST_URL = 'http://MockNotRealURL/' +TEST_BULK_URL = f'{TEST_URL}{BULK_PATH}' +TEST_DOWNLOAD_URL = f'{TEST_URL}comp/ops/download/' +TEST_ORDERS_URL = f'{TEST_URL}{ORDERS_PATH}' +TEST_STATS_URL = f'{TEST_URL}{STATS_PATH}' LOGGER = logging.getLogger(__name__) @@ -67,12 +71,12 @@ def create_download_mock(downloaded_content, order_description, oid): def f(): # Create mock HTTP response - dl_url = TEST_URL + 'download/1?token=IAmAToken' + dl_url = TEST_DOWNLOAD_URL + '/1?token=IAmAToken' order_description['_links']['results'] = [ {'location': dl_url}, ] - get_url = TEST_URL + 'orders/v2/' + oid + get_url = TEST_ORDERS_URL + oid mock_resp = httpx.Response(HTTPStatus.OK, json=order_description) respx.get(get_url).return_value = mock_resp @@ -91,7 +95,7 @@ def f(): @respx.mock @pytest.mark.asyncio async def test_list_orders_basic(order_descriptions, session): - list_url = TEST_URL + 'orders/v2/' + list_url = TEST_ORDERS_URL next_page_url = list_url + 'blob/?page_marker=IAmATest' order1, order2, order3 = order_descriptions @@ -103,7 +107,7 @@ async def test_list_orders_basic(order_descriptions, session): "orders": [order1, order2] } mock_resp1 = httpx.Response(HTTPStatus.OK, json=page1_response) - respx.get(list_url).return_value = mock_resp1 + respx.get(TEST_ORDERS_URL).return_value = mock_resp1 page2_response = { "_links": { @@ -123,7 +127,7 @@ async def test_list_orders_basic(order_descriptions, session): @respx.mock @pytest.mark.asyncio async def test_list_orders_state(order_descriptions, session): - list_url = TEST_URL + 'orders/v2/?state=failed' + list_url = TEST_ORDERS_URL + '?state=failed' order1, order2, _ = order_descriptions @@ -159,7 +163,7 @@ async def test_list_orders_limit(order_descriptions, session): # registering a response. if the client tries to get the next # page, an error will occur - list_url = TEST_URL + 'orders/v2/' + list_url = TEST_ORDERS_URL nono_page_url = list_url + '?page_marker=OhNoNo' order1, order2, order3 = order_descriptions @@ -195,8 +199,6 @@ async def test_list_orders_limit(order_descriptions, session): @respx.mock @pytest.mark.asyncio async def test_list_orders_asjson(order_descriptions, session): - list_url = TEST_URL + 'orders/v2/' - order1, order2, order3 = order_descriptions page1_response = { @@ -204,7 +206,7 @@ async def test_list_orders_asjson(order_descriptions, session): "orders": [order1] } mock_resp1 = httpx.Response(HTTPStatus.OK, json=page1_response) - respx.get(list_url).return_value = mock_resp1 + respx.get(TEST_ORDERS_URL).return_value = mock_resp1 cl = OrdersClient(session, base_url=TEST_URL) orders = await cl.list_orders(as_json=True) @@ -214,9 +216,8 @@ async def test_list_orders_asjson(order_descriptions, session): @respx.mock @pytest.mark.asyncio async def test_create_order(oid, order_description, order_request, session): - create_url = TEST_URL + 'orders/v2/' mock_resp = httpx.Response(HTTPStatus.OK, json=order_description) - respx.post(create_url).return_value = mock_resp + respx.post(TEST_ORDERS_URL).return_value = mock_resp cl = OrdersClient(session, base_url=TEST_URL) order = await cl.create_order(order_request) @@ -227,8 +228,6 @@ async def test_create_order(oid, order_description, order_request, session): @respx.mock @pytest.mark.asyncio async def test_create_order_bad_item_type(order_request, session): - create_url = TEST_URL + 'orders/v2/' - resp = { "field": { "Products": [ @@ -245,7 +244,7 @@ async def test_create_order_bad_item_type(order_request, session): ] } mock_resp = httpx.Response(400, json=resp) - respx.post(create_url).return_value = mock_resp + respx.post(TEST_ORDERS_URL).return_value = mock_resp order_request['products'][0]['item_type'] = 'invalid' cl = OrdersClient(session, base_url=TEST_URL) @@ -261,8 +260,6 @@ async def test_create_order_bad_item_type(order_request, session): @pytest.mark.asyncio async def test_create_order_item_id_does_not_exist( order_request, session, match_pytest_raises): - create_url = TEST_URL + 'orders/v2/' - resp = { "field": { "Details": [ @@ -279,7 +276,7 @@ async def test_create_order_item_id_does_not_exist( ] } mock_resp = httpx.Response(400, json=resp) - respx.post(create_url).return_value = mock_resp + respx.post(TEST_ORDERS_URL).return_value = mock_resp order_request['products'][0]['item_ids'] = \ '4500474_2133707_2021-05-20_2419' cl = OrdersClient(session, base_url=TEST_URL) @@ -295,7 +292,7 @@ async def test_create_order_item_id_does_not_exist( @respx.mock @pytest.mark.asyncio async def test_get_order(oid, order_description, session): - get_url = TEST_URL + 'orders/v2/' + oid + get_url = TEST_ORDERS_URL + oid mock_resp = httpx.Response(HTTPStatus.OK, json=order_description) respx.get(get_url).return_value = mock_resp @@ -316,7 +313,7 @@ async def test_get_order_invalid_id(session): @pytest.mark.asyncio async def test_get_order_id_doesnt_exist( oid, session, match_pytest_raises): - get_url = TEST_URL + 'orders/v2/' + oid + get_url = TEST_ORDERS_URL + oid msg = f'Could not load order ID: {oid}.' resp = { @@ -334,7 +331,7 @@ async def test_get_order_id_doesnt_exist( @respx.mock @pytest.mark.asyncio async def test_cancel_order(oid, order_description, session): - cancel_url = TEST_URL + 'orders/v2/' + oid + cancel_url = TEST_ORDERS_URL + oid order_description['state'] = 'cancelled' mock_resp = httpx.Response(HTTPStatus.OK, json=order_description) respx.put(cancel_url).return_value = mock_resp @@ -356,7 +353,7 @@ async def test_cancel_order_invalid_id(session): @pytest.mark.asyncio async def test_cancel_order_id_doesnt_exist( oid, session, match_pytest_raises): - cancel_url = TEST_URL + 'orders/v2/' + oid + cancel_url = TEST_ORDERS_URL + oid msg = f'No such order ID: {oid}.' resp = { @@ -375,7 +372,7 @@ async def test_cancel_order_id_doesnt_exist( @pytest.mark.asyncio async def test_cancel_order_id_cannot_be_cancelled( oid, session, match_pytest_raises): - cancel_url = TEST_URL + 'orders/v2/' + oid + cancel_url = TEST_ORDERS_URL + oid msg = 'Order not in a cancellable state' resp = { @@ -393,7 +390,7 @@ async def test_cancel_order_id_cannot_be_cancelled( @respx.mock @pytest.mark.asyncio async def test_cancel_orders_by_ids(session, oid, oid2): - bulk_cancel_url = TEST_URL + 'bulk/orders/v2/cancel' + bulk_cancel_url = TEST_BULK_URL + 'cancel' test_ids = [oid, oid2] example_result = { "result": { @@ -434,7 +431,7 @@ async def test_cancel_orders_by_ids_invalid_id(session, oid): @respx.mock @pytest.mark.asyncio async def test_cancel_orders_all(session): - bulk_cancel_url = TEST_URL + 'bulk/orders/v2/cancel' + bulk_cancel_url = TEST_BULK_URL + 'cancel' example_result = { "result": { @@ -460,7 +457,7 @@ async def test_cancel_orders_all(session): @respx.mock @pytest.mark.asyncio async def test_poll(oid, order_description, session): - get_url = TEST_URL + 'orders/v2/' + oid + get_url = TEST_ORDERS_URL + oid order_description2 = copy.deepcopy(order_description) order_description2['state'] = 'running' @@ -513,8 +510,6 @@ async def test_poll_invalid_state(oid, session): @respx.mock @pytest.mark.asyncio async def test_aggegated_order_stats(session): - stats_url = TEST_URL + 'stats/orders/v2/' - LOGGER.debug(f'url: {stats_url}') example_stats = { "organization": { "queued_orders": 0, @@ -526,7 +521,7 @@ async def test_aggegated_order_stats(session): } } mock_resp = httpx.Response(HTTPStatus.OK, json=example_stats) - respx.get(stats_url).return_value = mock_resp + respx.get(TEST_STATS_URL).return_value = mock_resp cl = OrdersClient(session, base_url=TEST_URL) res = await cl.aggregated_order_stats() @@ -537,7 +532,7 @@ async def test_aggegated_order_stats(session): @respx.mock @pytest.mark.asyncio async def test_download_asset_md(tmpdir, session): - dl_url = TEST_URL + 'download/?token=IAmAToken' + dl_url = TEST_DOWNLOAD_URL + '1?token=IAmAToken' md_json = {'key': 'value'} md_headers = { @@ -557,7 +552,7 @@ async def test_download_asset_md(tmpdir, session): @respx.mock @pytest.mark.asyncio async def test_download_asset_img(tmpdir, open_test_img, session): - dl_url = TEST_URL + 'download/?token=IAmAToken' + dl_url = TEST_DOWNLOAD_URL + '1?token=IAmAToken' img_headers = { 'Content-Type': 'image/tiff', @@ -597,14 +592,14 @@ async def test_download_order_success(tmpdir, order_description, oid, session): ''' # Mock an HTTP response for download - dl_url1 = TEST_URL + 'download/1?token=IAmAToken' - dl_url2 = TEST_URL + 'download/2?token=IAmAnotherToken' + dl_url1 = TEST_DOWNLOAD_URL + '1?token=IAmAToken' + dl_url2 = TEST_DOWNLOAD_URL + '2?token=IAmAnotherToken' order_description['_links']['results'] = [ {'location': dl_url1}, {'location': dl_url2} ] - get_url = TEST_URL + 'orders/v2/' + oid + get_url = TEST_ORDERS_URL + oid mock_resp = httpx.Response(HTTPStatus.OK, json=order_description) respx.get(get_url).return_value = mock_resp From b9392939301110c6e5c967840f1b427e4cfe4c9c Mon Sep 17 00:00:00 2001 From: Jennifer Reiber Kyle Date: Sun, 30 Jan 2022 15:24:37 -0800 Subject: [PATCH 04/13] base_url is specified at main (planet) level instead of api (planet orders) level --- planet/cli/auth.py | 9 +++------ planet/cli/cli.py | 5 ++++- planet/cli/orders.py | 9 ++------- tests/integration/test_auth_cli.py | 4 ++-- 4 files changed, 11 insertions(+), 16 deletions(-) diff --git a/planet/cli/auth.py b/planet/cli/auth.py index 1f5aeaeac..a57328868 100644 --- a/planet/cli/auth.py +++ b/planet/cli/auth.py @@ -24,12 +24,9 @@ @click.group() @click.pass_context -@click.option('-u', '--base-url', - default=None, - help='Assign custom base Auth API URL.') -def auth(ctx, base_url): +def auth(ctx): '''Commands for working with Planet authentication''' - ctx.obj['BASE_URL'] = base_url + pass @auth.command() @@ -43,7 +40,7 @@ def auth(ctx, base_url): )) def init(ctx, email, password): '''Obtain and store authentication information''' - base_url = ctx.obj["BASE_URL"] + base_url = ctx.obj['BASE_URL'] plauth = planet.Auth.from_login(email, password, base_url=base_url) plauth.write() click.echo('Initialized') diff --git a/planet/cli/cli.py b/planet/cli/cli.py index db734999f..c097856b1 100644 --- a/planet/cli/cli.py +++ b/planet/cli/cli.py @@ -26,17 +26,20 @@ @click.group() @click.pass_context +@click.option('--base-url', default=None, + help='Assign custom base url for Planet services.') @click.option('-v', '--verbose', count=True, help=('Specify verbosity level of between 0 and 2 corresponding ' 'to log levels warning, info, and debug respectively.')) @click.version_option(version=planet.__version__) -def main(ctx, verbose): +def main(ctx, base_url, verbose): '''Planet API Client''' _configure_logging(verbose) # ensure that ctx.obj exists and is a dict (in case `cli()` is called # by means other than the `if` block below) ctx.ensure_object(dict) + ctx.obj['BASE_URL'] = base_url def _configure_logging(verbosity): diff --git a/planet/cli/orders.py b/planet/cli/orders.py index ab03c78f4..a19b91ead 100644 --- a/planet/cli/orders.py +++ b/planet/cli/orders.py @@ -41,14 +41,9 @@ async def orders_client(ctx): @click.group() @click.pass_context -@click.option('-u', '--base-url', - default=None, - help='Assign custom base Orders API URL.') -def orders(ctx, base_url): +def orders(ctx): '''Commands for interacting with the Orders API''' - auth = planet.Auth.from_file() - ctx.obj['AUTH'] = auth - ctx.obj['BASE_URL'] = base_url + ctx.obj['AUTH'] = planet.Auth.from_file() @orders.command() diff --git a/tests/integration/test_auth_cli.py b/tests/integration/test_auth_cli.py index a1945be94..aed9ae2c0 100644 --- a/tests/integration/test_auth_cli.py +++ b/tests/integration/test_auth_cli.py @@ -61,7 +61,7 @@ def test_cli_auth_init_success(redirect_secretfile): result = CliRunner().invoke( cli.main, - args=['auth', '--base-url', TEST_URL, 'init'], + args=['--base-url', TEST_URL, 'auth', 'init'], input='email\npw\n') # we would get a 'url not mocked' exception if the base url wasn't @@ -82,7 +82,7 @@ def test_cli_auth_init_bad_pw(redirect_secretfile): result = CliRunner().invoke( cli.main, - args=['auth', '--base-url', TEST_URL, 'init'], + args=['--base-url', TEST_URL, 'auth', 'init'], input='email\npw\n') assert result.exception From 535847c281812839fb6cbe63cc26f261e23585cf Mon Sep 17 00:00:00 2001 From: Jennifer Reiber Kyle Date: Mon, 31 Jan 2022 16:57:58 -0800 Subject: [PATCH 05/13] simplify tests, move order_descriptions fixture to conftest --- tests/integration/conftest.py | 12 +++++++ tests/integration/test_orders_api.py | 51 ++++++---------------------- 2 files changed, 23 insertions(+), 40 deletions(-) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index dfea98b11..1044a4cfe 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. import contextlib +import copy import pytest @@ -33,3 +34,14 @@ def cm(ex, msg): with pytest.raises(ex, match=f'^{msg}$') as pt: yield pt return cm + + +@pytest.fixture +def order_descriptions(order_description): + order1 = order_description + order1['id'] = 'oid1' + order2 = copy.deepcopy(order_description) + order2['id'] = 'oid2' + order3 = copy.deepcopy(order_description) + order3['id'] = 'oid3' + return [order1, order2, order3] diff --git a/tests/integration/test_orders_api.py b/tests/integration/test_orders_api.py index 472fd5b5e..d17c55692 100644 --- a/tests/integration/test_orders_api.py +++ b/tests/integration/test_orders_api.py @@ -36,23 +36,6 @@ LOGGER = logging.getLogger(__name__) -@pytest.fixture -def order_descriptions(order_description): - order1 = order_description - order1['id'] = 'oid1' - order2 = copy.deepcopy(order_description) - order2['id'] = 'oid2' - order3 = copy.deepcopy(order_description) - order3['id'] = 'oid3' - return [order1, order2, order3] - - -@pytest.fixture -def oid2(): - # obtained from uuid.uuid1() - return '5ece1dc0-ea81-11eb-837c-acde48001122' - - @pytest.fixture def downloaded_content(): return {'key': 'downloaded_file'} @@ -95,8 +78,7 @@ def f(): @respx.mock @pytest.mark.asyncio async def test_list_orders_basic(order_descriptions, session): - list_url = TEST_ORDERS_URL - next_page_url = list_url + 'blob/?page_marker=IAmATest' + next_page_url = TEST_ORDERS_URL + 'blob/?page_marker=IAmATest' order1, order2, order3 = order_descriptions @@ -141,6 +123,9 @@ async def test_list_orders_state(order_descriptions, session): respx.get(list_url).return_value = mock_resp cl = OrdersClient(session, base_url=TEST_URL) + + # 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') oids = list(o.id for o in orders) @@ -158,13 +143,7 @@ async def test_list_orders_state_invalid_state(session): @respx.mock @pytest.mark.asyncio async def test_list_orders_limit(order_descriptions, session): - # check that the client doesn't try to get the next page when the - # limit is already reached by providing link to next page but not - # registering a response. if the client tries to get the next - # page, an error will occur - - list_url = TEST_ORDERS_URL - nono_page_url = list_url + '?page_marker=OhNoNo' + nono_page_url = TEST_ORDERS_URL + '?page_marker=OhNoNo' order1, order2, order3 = order_descriptions @@ -175,23 +154,14 @@ async def test_list_orders_limit(order_descriptions, session): "orders": [order1, order2] } mock_resp = httpx.Response(HTTPStatus.OK, json=page1_response) - - page2_response = { - "_links": { - "_self": "string", - }, - "orders": [order3] - } - mock_resp2 = httpx.Response(HTTPStatus.OK, json=page2_response) - - respx.route(method="GET", url__eq=list_url).mock(return_value=mock_resp) - nono_route = respx.route(method="GET", url__eq=nono_page_url).mock( - return_value=mock_resp2) + respx.get(TEST_ORDERS_URL).return_value = mock_resp cl = OrdersClient(session, base_url=TEST_URL) + + # since nono_page_url is not mocked, an error will occur if the client + # attempts to access the next page when the limit is already reached orders = await cl.list_orders(limit=1) - assert not nono_route.called oids = [o.id for o in orders] assert oids == ['oid1'] @@ -389,8 +359,9 @@ async def test_cancel_order_id_cannot_be_cancelled( @respx.mock @pytest.mark.asyncio -async def test_cancel_orders_by_ids(session, oid, oid2): +async def test_cancel_orders_by_ids(session, oid): bulk_cancel_url = TEST_BULK_URL + 'cancel' + oid2 = '5ece1dc0-ea81-11eb-837c-acde48001122' test_ids = [oid, oid2] example_result = { "result": { From 5ce2d8a9ba8fa7a7fca88972aa10e4cac0ae85f4 Mon Sep 17 00:00:00 2001 From: Jennifer Reiber Kyle Date: Mon, 31 Jan 2022 16:58:32 -0800 Subject: [PATCH 06/13] orders cli unit tests to integration tests --- tests/integration/test_orders_cli.py | 579 +++++++++++++++++++++++++++ tests/unit/test_cli_orders.py | 351 ---------------- 2 files changed, 579 insertions(+), 351 deletions(-) create mode 100644 tests/integration/test_orders_cli.py delete mode 100644 tests/unit/test_cli_orders.py diff --git a/tests/integration/test_orders_cli.py b/tests/integration/test_orders_cli.py new file mode 100644 index 000000000..0ab36a6c8 --- /dev/null +++ b/tests/integration/test_orders_cli.py @@ -0,0 +1,579 @@ +# Copyright 2022 Planet Labs, PBC. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. +'''Test Orders CLI''' +from http import HTTPStatus +import json +from pathlib import Path +from unittest.mock import Mock + +from click.testing import CliRunner +import httpx +import pytest +import respx + +from planet.cli import cli +from planet.clients.orders import ORDERS_PATH + +TEST_URL = 'http://MockNotRealURL/' +TEST_ORDERS_URL = f'{TEST_URL}{ORDERS_PATH}' +TEST_DOWNLOAD_URL = f'{TEST_URL}comp/ops/download/' + +# NOTE: These tests use a lot of the same mocked responses as test_orders_api. + + +@pytest.fixture +def invoke(): + def _invoke(extra_args, runner=None): + runner = runner or CliRunner() + args = ['--base-url', TEST_URL] + extra_args + return runner.invoke(cli.main, args=args) + return _invoke + + +@respx.mock +def test_cli_orders_list_basic(invoke, order_descriptions): + next_page_url = TEST_ORDERS_URL + 'blob/?page_marker=IAmATest' + order1, order2, order3 = order_descriptions + + page1_response = { + "_links": { + "_self": "string", + "next": next_page_url}, + "orders": [order1, order2] + } + mock_resp1 = httpx.Response(HTTPStatus.OK, json=page1_response) + respx.get(TEST_ORDERS_URL).return_value = mock_resp1 + + page2_response = { + "_links": { + "_self": next_page_url}, + "orders": [order3] + } + mock_resp2 = httpx.Response(HTTPStatus.OK, json=page2_response) + respx.get(next_page_url).return_value = mock_resp2 + + result = invoke(['orders', 'list']) + assert not result.exception + assert [order1, order2, order3] == json.loads(result.output) + + +@respx.mock +def test_cli_orders_list_empty(invoke): + page1_response = { + "_links": { + "_self": "string" + }, + "orders": [] + } + mock_resp = httpx.Response(HTTPStatus.OK, json=page1_response) + respx.get(TEST_ORDERS_URL).return_value = mock_resp + + result = invoke(['orders', 'list']) + assert not result.exception + assert [] == json.loads(result.output) + + +@respx.mock +def test_cli_orders_list_state(invoke, order_descriptions): + list_url = TEST_ORDERS_URL + '?state=failed' + + 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 state doesn't get sent as a url parameter, + # the mock will fail and this test will fail + result = invoke(['orders', 'list', '--state', 'failed']) + assert not result.exception + assert [order1, order2] == json.loads(result.output) + + +@respx.mock +def test_cli_orders_list_limit(invoke, order_descriptions): + order1, order2, _ = order_descriptions + + page1_response = { + "_links": { + "_self": "string" + }, + "orders": [order1, order2] + } + mock_resp = httpx.Response(HTTPStatus.OK, json=page1_response) + + # limiting is done within the client, no change to api call + respx.get(TEST_ORDERS_URL).return_value = mock_resp + + result = invoke(['orders', 'list', '--limit', '1']) + assert not result.exception + assert [order1] == json.loads(result.output) + + +@respx.mock +def test_cli_orders_list_pretty(invoke, monkeypatch, order_description): + mock_echo_json = Mock() + monkeypatch.setattr(cli.orders, 'echo_json', mock_echo_json) + + page1_response = { + "_links": { + "_self": "string" + }, + "orders": [order_description] + } + mock_resp = httpx.Response(HTTPStatus.OK, json=page1_response) + respx.get(TEST_ORDERS_URL).return_value = mock_resp + + result = invoke(['orders', 'list', '--pretty']) + assert not result.exception + mock_echo_json.assert_called_once_with([order_description], True) + + +@respx.mock +def test_cli_orders_get(invoke, oid, order_description): + get_url = TEST_ORDERS_URL + oid + mock_resp = httpx.Response(HTTPStatus.OK, json=order_description) + respx.get(get_url).return_value = mock_resp + + result = invoke(['orders', 'get', oid]) + assert not result.exception + assert order_description == json.loads(result.output) + + +@respx.mock +def test_cli_orders_get_id_not_found(invoke, oid): + get_url = TEST_ORDERS_URL + oid + error_json = {'message': 'A descriptive error message'} + mock_resp = httpx.Response(404, json=error_json) + respx.get(get_url).return_value = mock_resp + + result = invoke(['orders', 'get', oid]) + assert result.exception + assert 'Error: A descriptive error message\n' == result.output + + +@respx.mock +def test_cli_orders_cancel(invoke, oid, order_description): + cancel_url = TEST_ORDERS_URL + oid + order_description['state'] = 'cancelled' + mock_resp = httpx.Response(HTTPStatus.OK, json=order_description) + respx.put(cancel_url).return_value = mock_resp + + result = invoke(['orders', 'cancel', oid]) + assert not result.exception + assert 'Cancelled\n' == result.output + + +@respx.mock +def test_cli_orders_cancel_id_not_found(invoke, oid): + cancel_url = TEST_ORDERS_URL + oid + error_json = {'message': 'A descriptive error message'} + mock_resp = httpx.Response(404, json=error_json) + respx.put(cancel_url).return_value = mock_resp + + result = invoke(['orders', 'cancel', oid]) + assert result.exception + assert 'Error: A descriptive error message\n' == result.output + + +@pytest.fixture +def mock_download_response(oid, order_description): + def _func(): + # Mock an HTTP response for polling and download + order_description['state'] = 'success' + dl_url1 = TEST_DOWNLOAD_URL + '1?token=IAmAToken' + dl_url2 = TEST_DOWNLOAD_URL + '2?token=IAmAnotherToken' + order_description['_links']['results'] = [ + {'location': dl_url1}, + {'location': dl_url2} + ] + + get_url = TEST_ORDERS_URL + oid + mock_resp = httpx.Response(HTTPStatus.OK, json=order_description) + respx.get(get_url).return_value = mock_resp + + mock_resp1 = httpx.Response( + HTTPStatus.OK, + json={'key': 'value'}, + headers={ + 'Content-Type': 'application/json', + 'Content-Disposition': 'attachment; filename="m1.json"' + }) + respx.get(dl_url1).return_value = mock_resp1 + + mock_resp2 = httpx.Response( + HTTPStatus.OK, + json={'key2': 'value2'}, + headers={ + 'Content-Type': 'application/json', + 'Content-Disposition': 'attachment; filename="m2.json"' + }) + respx.get(dl_url2).return_value = mock_resp2 + return _func + + +@respx.mock +def test_cli_orders_download(invoke, mock_download_response, oid): + mock_download_response() + + runner = CliRunner() + with runner.isolated_filesystem() as folder: + result = invoke(['orders', 'download', oid], runner=runner) + assert not result.exception + + # output is progress reporting plus the message + message = 'Downloaded 2 files.\n' + assert message in result.output + assert message != result.output + + # Check that the files were downloaded and have the correct contents + f1_path = Path(folder) / 'm1.json' + assert json.load(open(f1_path)) == {'key': 'value'} + f2_path = Path(folder) / 'm2.json' + assert json.load(open(f2_path)) == {'key2': 'value2'} + + +@respx.mock +def test_cli_orders_download_dest(invoke, mock_download_response, oid): + mock_download_response() + + runner = CliRunner() + with runner.isolated_filesystem() as folder: + dest_dir = Path(folder) / 'foobar' + dest_dir.mkdir() + result = invoke(['orders', 'download', '--dest', 'foobar', oid], + runner=runner) + assert not result.exception + + # Check that the files were downloaded to the custom directory + f1_path = dest_dir / 'm1.json' + assert json.load(open(f1_path)) == {'key': 'value'} + f2_path = dest_dir / 'm2.json' + assert json.load(open(f2_path)) == {'key2': 'value2'} + + +@respx.mock +def test_cli_orders_download_overwrite( + invoke, mock_download_response, oid, write_to_tmp_json_file): + mock_download_response() + + runner = CliRunner() + with runner.isolated_filesystem() as folder: + filepath = Path(folder) / 'm1.json' + write_to_tmp_json_file({'foo': 'bar'}, filepath) + + # check the file doesn't get overwritten by default + result = invoke(['orders', 'download', oid], + runner=runner) + assert not result.exception + assert json.load(open(filepath)) == {'foo': 'bar'} + + # check the file gets overwritten + result = invoke(['orders', 'download', '--overwrite', oid], + runner=runner) + assert json.load(open(filepath)) == {'key': 'value'} + + +@pytest.mark.skip('https://github.com/planetlabs/planet-client-python/issues/352') # noqa +@respx.mock +def test_cli_orders_download_quiet(invoke, mock_download_response, oid): + mock_download_response() + + runner = CliRunner() + with runner.isolated_filesystem(): + result = invoke(['orders', 'download', '-q', oid], runner=runner) + assert not result.exception + + # no progress reporting, just the message + message = 'Downloaded 2 files.\n' + assert message == result.output + + +@respx.mock +def test_cli_orders_create_basic(invoke, order_description): + mock_resp = httpx.Response(HTTPStatus.OK, json=order_description) + respx.post(TEST_ORDERS_URL).return_value = mock_resp + + result = invoke([ + 'orders', 'create', + '--name', 'test', + '--id', '4500474_2133707_2021-05-20_2419', + '--bundle', 'analytic', + '--item-type', 'PSOrthoTile' + ]) + assert not result.exception + assert order_description == json.loads(result.output) + + order_request = { + "name": "test", + "products": [{ + "item_ids": ["4500474_2133707_2021-05-20_2419"], + "item_type": "PSOrthoTile", + "product_bundle": "analytic" + }], + } + sent_request = json.loads(respx.calls.last.request.content) + assert sent_request == order_request + + +def test_cli_orders_create_basic_item_type_invalid(invoke): + result = invoke([ + 'orders', 'create', + '--name', 'test', + '--id', '4500474_2133707_2021-05-20_2419', + '--bundle', 'analytic', + '--item-type', 'invalid' + ]) + assert result.exception + assert 'Error: Invalid value: item_type' in result.output + + +@respx.mock +def test_cli_orders_create_clip( + invoke, geom_geojson, order_description, write_to_tmp_json_file): + mock_resp = httpx.Response(HTTPStatus.OK, json=order_description) + respx.post(TEST_ORDERS_URL).return_value = mock_resp + + aoi_file = write_to_tmp_json_file(geom_geojson, 'aoi.geojson') + + result = invoke([ + 'orders', 'create', + '--name', 'test', + '--id', '4500474_2133707_2021-05-20_2419', + '--bundle', 'analytic', + '--item-type', 'PSOrthoTile', + '--clip', aoi_file + ]) + assert not result.exception + + order_request = { + "name": "test", + "products": [{ + "item_ids": ["4500474_2133707_2021-05-20_2419"], + "item_type": "PSOrthoTile", + "product_bundle": "analytic", + }], + "tools": [{'clip': {'aoi': geom_geojson}}] + } + sent_request = json.loads(respx.calls.last.request.content) + assert sent_request == order_request + + +@respx.mock +def test_cli_orders_create_clip_featureclass( + invoke, featureclass_geojson, geom_geojson, order_description, + write_to_tmp_json_file): + """Tests that the clip option takes in feature class geojson as well""" + mock_resp = httpx.Response(HTTPStatus.OK, json=order_description) + respx.post(TEST_ORDERS_URL).return_value = mock_resp + + fc_file = write_to_tmp_json_file(featureclass_geojson, 'fc.geojson') + + result = invoke([ + 'orders', 'create', + '--name', 'test', + '--id', '4500474_2133707_2021-05-20_2419', + '--bundle', 'analytic', + '--item-type', 'PSOrthoTile', + '--clip', fc_file + ]) + assert not result.exception + + order_request = { + "name": "test", + "products": [{ + "item_ids": ["4500474_2133707_2021-05-20_2419"], + "item_type": "PSOrthoTile", + "product_bundle": "analytic", + }], + "tools": [{'clip': {'aoi': geom_geojson}}] + } + sent_request = json.loads(respx.calls.last.request.content) + assert sent_request == order_request + + +def test_cli_orders_create_clip_invalid_geometry( + invoke, point_geom_geojson, write_to_tmp_json_file): + aoi_file = write_to_tmp_json_file(point_geom_geojson, 'aoi.geojson') + + result = invoke([ + 'orders', 'create', + '--name', 'test', + '--id', '4500474_2133707_2021-05-20_2419', + '--bundle', 'analytic', + '--item-type', 'PSOrthoTile', + '--clip', aoi_file + ]) + assert result.exception + error_msg = ('Error: Invalid value: Invalid geometry type: ' + + 'Point is not Polygon.') + assert error_msg in result.output + + +def test_cli_orders_create_clip_and_tools( + invoke, geom_geojson, write_to_tmp_json_file): + # interestingly, it is important that both clip and tools + # option values lead to valid json files + aoi_file = write_to_tmp_json_file(geom_geojson, 'aoi.geojson') + + result = invoke([ + 'orders', 'create', + '--name', 'test', + '--id', '4500474_2133707_2021-05-20_2419', + '--bundle', 'analytic', + '--item-type', 'PSOrthoTile', + '--clip', aoi_file, + '--tools', aoi_file + ]) + assert result.exception + assert "Specify only one of '--clip' or '--tools'" in result.output + + +@respx.mock +def test_cli_orders_create_cloudconfig( + invoke, geom_geojson, order_description, write_to_tmp_json_file): + mock_resp = httpx.Response(HTTPStatus.OK, json=order_description) + respx.post(TEST_ORDERS_URL).return_value = mock_resp + + config_json = { + 'amazon_s3': { + 'aws_access_key_id': 'aws_access_key_id', + 'aws_secret_access_key': 'aws_secret_access_key', + 'bucket': 'bucket', + 'aws_region': 'aws_region' + }, + 'archive_type': 'zip' + } + config_file = write_to_tmp_json_file(config_json, 'config.json') + + result = invoke([ + 'orders', 'create', + '--name', 'test', + '--id', '4500474_2133707_2021-05-20_2419', + '--bundle', 'analytic', + '--item-type', 'PSOrthoTile', + '--cloudconfig', config_file + ]) + assert not result.exception + + order_request = { + "name": "test", + "products": [{ + "item_ids": ["4500474_2133707_2021-05-20_2419"], + "item_type": "PSOrthoTile", + "product_bundle": "analytic", + }], + "delivery": config_json + } + sent_request = json.loads(respx.calls.last.request.content) + assert sent_request == order_request + + +@respx.mock +def test_cli_orders_create_email( + invoke, geom_geojson, order_description): + mock_resp = httpx.Response(HTTPStatus.OK, json=order_description) + respx.post(TEST_ORDERS_URL).return_value = mock_resp + + result = invoke([ + 'orders', 'create', + '--name', 'test', + '--id', '4500474_2133707_2021-05-20_2419', + '--bundle', 'analytic', + '--item-type', 'PSOrthoTile', + '--email' + ]) + assert not result.exception + + order_request = { + "name": "test", + "products": [{ + "item_ids": ["4500474_2133707_2021-05-20_2419"], + "item_type": "PSOrthoTile", + "product_bundle": "analytic", + }], + "notifications": {"email": True} + } + sent_request = json.loads(respx.calls.last.request.content) + assert sent_request == order_request + + +@respx.mock +def test_cli_orders_create_tools( + invoke, geom_geojson, order_description, write_to_tmp_json_file): + mock_resp = httpx.Response(HTTPStatus.OK, json=order_description) + respx.post(TEST_ORDERS_URL).return_value = mock_resp + + tools_json = [{'clip': {'aoi': geom_geojson}}, {'composite': {}}] + tools_file = write_to_tmp_json_file(tools_json, 'tools.json') + + result = invoke([ + 'orders', 'create', + '--name', 'test', + '--id', '4500474_2133707_2021-05-20_2419', + '--bundle', 'analytic', + '--item-type', 'PSOrthoTile', + '--tools', tools_file + ]) + assert not result.exception + + order_request = { + "name": "test", + "products": [{ + "item_ids": ["4500474_2133707_2021-05-20_2419"], + "item_type": "PSOrthoTile", + "product_bundle": "analytic", + }], + "tools": tools_json + } + sent_request = json.loads(respx.calls.last.request.content) + assert sent_request == order_request + + +def test_cli_orders_read_file_json_doesnotexist(invoke): + result = invoke([ + 'orders', 'create', + '--name', 'test', + '--id', '4500474_2133707_2021-05-20_2419', + '--bundle', 'analytic', + '--item-type', 'PSOrthoTile', + '--tools', 'doesnnotexist.json' + ]) + assert result.exception + error_msg = ("Error: Invalid value for '--tools': 'doesnnotexist.json': " + + "No such file or directory") + assert error_msg in result.output + + +def test_cli_orders_read_file_json_invalidjson(invoke, tmp_path): + invalid_filename = tmp_path / 'invalid.json' + with open(invalid_filename, 'w') as fp: + fp.write('[Invali]d j*son') + + result = invoke([ + 'orders', 'create', + '--name', 'test', + '--id', '4500474_2133707_2021-05-20_2419', + '--bundle', 'analytic', + '--item-type', 'PSOrthoTile', + '--tools', invalid_filename + ]) + assert result.exception + error_msg = "Error: File does not contain valid json." + assert error_msg in result.output diff --git a/tests/unit/test_cli_orders.py b/tests/unit/test_cli_orders.py deleted file mode 100644 index ff9c16a8f..000000000 --- a/tests/unit/test_cli_orders.py +++ /dev/null @@ -1,351 +0,0 @@ -# Copyright 2022 Planet Labs, PBC. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may not -# use this file except in compliance with the License. You may obtain a copy of -# the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations under -# the License. -from unittest.mock import MagicMock, Mock - -from click.testing import CliRunner -import pytest - -import planet -from planet.cli import cli - - -@pytest.fixture -def runner(): - return CliRunner() - - -@pytest.fixture(autouse=True) -def patch_session(monkeypatch): - '''Make sure we don't actually make any http calls''' - monkeypatch.setattr(planet, 'Session', MagicMock(spec=planet.Session)) - - -@pytest.fixture -def patch_ordersclient(monkeypatch): - def patch(to_patch, patch_with): - monkeypatch.setattr(cli.orders.OrdersClient, to_patch, patch_with) - return patch - - -def test_cli_orders_list_empty(runner, patch_ordersclient): - async def lo(*arg, **kwarg): - return [] - patch_ordersclient('list_orders', lo) - - result = runner.invoke(cli.main, ['orders', 'list']) - assert not result.exception - assert '[]' in result.output - - -def test_cli_orders_list_success(runner, patch_ordersclient): - async def lo(*arg, **kwarg): - return [{'order': 'yep'}] - patch_ordersclient('list_orders', lo) - - result = runner.invoke(cli.main, ['orders', 'list']) - assert not result.exception - assert '{"order": "yep"}' in result.output - - -def test_cli_orders_get(runner, patch_ordersclient, order_description, oid): - async def go(*arg, **kwarg): - return planet.models.Order(order_description) - patch_ordersclient('get_order', go) - - result = runner.invoke( - cli.main, ['orders', 'get', oid]) - assert not result.exception - - -def test_cli_orders_cancel(runner, patch_ordersclient, order_description, oid): - async def co(*arg, **kwarg): - return '' - patch_ordersclient('cancel_order', co) - - result = runner.invoke( - cli.main, ['orders', 'cancel', oid]) - assert not result.exception - - -def test_cli_orders_download(runner, patch_ordersclient, oid): - all_test_files = ['file1.json', 'file2.zip', 'file3.tiff', 'file4.jpg'] - - async def do(*arg, **kwarg): - return all_test_files - patch_ordersclient('download_order', do) - - async def poll(*arg, **kwarg): - return - patch_ordersclient('poll', poll) - - # Number of files in all_test_files - expected = 'Downloaded 4 files.\n' - - # allow for some progress reporting - result = runner.invoke( - cli.main, ['orders', 'download', oid]) - assert not result.exception - assert expected in result.output - - # test quiet option, should be no progress reporting - result = runner.invoke( - cli.main, ['orders', 'download', '-q', oid]) - assert not result.exception - assert expected == result.output - - -class AsyncMock(Mock): - '''Mock an async function''' - async def __call__(self, *args, **kwargs): - return super().__call__(*args, **kwargs) - - -@pytest.fixture -def cloudconfig(): - return { - 'amazon_s3': { - 'aws_access_key_id': 'aws_access_key_id', - 'aws_secret_access_key': 'aws_secret_access_key', - 'bucket': 'bucket', - 'aws_region': 'aws_region' - }, - 'archive_type': 'zip', - } - - -@pytest.fixture -def clipaoi(feature_geojson, write_to_tmp_json_file): - return write_to_tmp_json_file(feature_geojson, 'clip.json') - - -@pytest.fixture -def tools_json(geom_geojson): - return [ - { - 'clip': {'aoi': geom_geojson} - }, { - 'composite': {} - } - ] - - -@pytest.fixture -def tools(tools_json, write_to_tmp_json_file): - return write_to_tmp_json_file(tools_json, 'tools.json') - - -@pytest.fixture -def mock_create_order(patch_ordersclient, order_description): - mock_create_order = AsyncMock( - return_value=planet.models.Order(order_description)) - patch_ordersclient('create_order', mock_create_order) - return mock_create_order - - -@pytest.fixture -def test_id(order_request): - return order_request['products'][0]['item_ids'][0] - - -def test_cli_read_file_geojson(clipaoi, geom_geojson): - with open(clipaoi, 'r') as cfile: - res = cli.orders.read_file_geojson({}, 'clip', cfile) - assert res == geom_geojson - - -@pytest.fixture -def create_order_basic_cmds(order_request, test_id): - product = order_request['products'][0] - return [ - 'orders', 'create', - '--name', order_request['name'], - '--id', test_id, - '--bundle', product['product_bundle'], - '--item-type', product['item_type'] - ] - - -@pytest.fixture -def name(order_request): - return order_request['name'] - - -@pytest.fixture -def products(order_request, test_id): - product = order_request['products'][0] - return [ - planet.order_request.product( - [test_id], - product['product_bundle'], - product['item_type']) - ] - - -def test_cli_orders_create_cloudconfig( - runner, mock_create_order, create_order_basic_cmds, name, products, - cloudconfig, write_to_tmp_json_file - ): - cc_file = write_to_tmp_json_file(cloudconfig, 'cloudconfig.json') - basic_result = runner.invoke( - cli.main, create_order_basic_cmds + ['--cloudconfig', cc_file] - ) - assert not basic_result.exception - - mock_create_order.assert_called_once() - - expected_details = { - 'name': name, - 'products': products, - 'delivery': cloudconfig - } - mock_create_order.assert_called_with(expected_details) - - -def test_cli_orders_create_clip( - runner, mock_create_order, create_order_basic_cmds, name, products, - clipaoi, geom_geojson - ): - basic_result = runner.invoke( - cli.main, create_order_basic_cmds + ['--clip', clipaoi] - ) - assert not basic_result.exception - - mock_create_order.assert_called_once() - - expected_details = { - 'name': name, - 'products': products, - 'tools': [{'clip': {'aoi': geom_geojson}}] - } - mock_create_order.assert_called_with(expected_details) - - -def test_cli_orders_create_tools( - runner, mock_create_order, create_order_basic_cmds, name, products, - tools, tools_json): - basic_result = runner.invoke( - cli.main, create_order_basic_cmds + ['--tools', tools] - ) - assert not basic_result.exception - - mock_create_order.assert_called_once() - - expected_details = { - 'name': name, - 'products': products, - 'tools': tools_json - } - mock_create_order.assert_called_with(expected_details) - - -def test_cli_orders_create_validate_id( - runner, mock_create_order, order_request, test_id - ): - # uuid generated with https://www.uuidgenerator.net/ - test_id2 = '65f4aa35-b46b-48ba-b165-12b49986795c' - success_ids = ','.join([test_id, test_id2]) - fail_ids = '1,,2' - - product = order_request['products'][0] - - # id string is correct format - success_mult_ids_result = runner.invoke( - cli.main, [ - 'orders', 'create', - '--name', order_request['name'], - '--id', success_ids, - '--bundle', product['product_bundle'], - '--item-type', product['item_type'] - ]) - - assert not success_mult_ids_result.exception - - # id string is wrong format - failed_mult_ids_result = runner.invoke( - cli.main, [ - 'orders', 'create', - '--name', order_request['name'], - '--id', fail_ids, - '--bundle', product['product_bundle'], - '--item-type', product['item_type'] - ]) - assert failed_mult_ids_result.exception - assert "id cannot be empty" in failed_mult_ids_result.output - - -def test_cli_orders_create_validate_item_type( - runner, mock_create_order, order_request, test_id - ): - # item type is not valid for bundle - failed_item_type_result = runner.invoke( - cli.main, [ - 'orders', 'create', - '--name', order_request['name'], - '--id', test_id, - '--bundle', 'analytic_udm2', - '--item-type', 'PSScene3Band' - ]) - assert failed_item_type_result.exception - assert "Invalid value: item_type" in failed_item_type_result.output - - -def test_cli_orders_create_validate_cloudconfig( - runner, mock_create_order, create_order_basic_cmds, - tmp_path, - ): - # write invalid text to file - cloudconfig = tmp_path / 'cc.json' - with open(cloudconfig, 'w') as fp: - fp.write('') - - wrong_format_result = runner.invoke( - cli.main, create_order_basic_cmds + ['--cloudconfig', cloudconfig] - ) - assert wrong_format_result.exception - assert "File does not contain valid json." \ - in wrong_format_result.output - - # cloudconfig file doesn't exist - doesnotexistfile = tmp_path / 'doesnotexist.json' - doesnotexit_result = runner.invoke( - cli.main, create_order_basic_cmds + ['--cloudconfig', doesnotexistfile] - ) - assert doesnotexit_result.exception - assert "No such file or directory" in doesnotexit_result.output - - -def test_cli_orders_create_validate_tools( - runner, mock_create_order, create_order_basic_cmds, - tools, clipaoi, - ): - - clip_and_tools_result = runner.invoke( - cli.main, - create_order_basic_cmds + ['--tools', tools, '--clip', clipaoi] - ) - assert clip_and_tools_result.exception - - -def test_cli_orders_create_validate_clip( - runner, mock_create_order, create_order_basic_cmds, - point_geom_geojson, write_to_tmp_json_file - ): - clip_point = write_to_tmp_json_file(point_geom_geojson, 'point.json') - - clip_point_result = runner.invoke( - cli.main, create_order_basic_cmds + ['--clip', clip_point] - ) - assert clip_point_result.exception - assert "Invalid geometry type: Point is not Polygon" in \ - clip_point_result.output From 4fb6aa9e2e3ef0086c4eef4f4c5fd2bc221f2ba4 Mon Sep 17 00:00:00 2001 From: Jennifer Reiber Kyle Date: Mon, 31 Jan 2022 20:46:01 -0800 Subject: [PATCH 07/13] increase cli test coverage, increase passing coverage to 98% --- setup.cfg | 2 +- tests/integration/test_orders_cli.py | 25 ++++++++++++++++++++++--- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/setup.cfg b/setup.cfg index 5a74fcecd..3c994c7b6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -10,7 +10,7 @@ addopts = --cov=tests --cov-report=term-missing --cov-report=xml - --cov-fail-under 95 + --cov-fail-under 98 -rxXs [coverage:run] diff --git a/tests/integration/test_orders_cli.py b/tests/integration/test_orders_cli.py index 0ab36a6c8..1e1fd3872 100644 --- a/tests/integration/test_orders_cli.py +++ b/tests/integration/test_orders_cli.py @@ -305,15 +305,22 @@ def test_cli_orders_download_quiet(invoke, mock_download_response, oid): assert message == result.output +@pytest.mark.parametrize( + "id_string, expected_ids", + [('4500474_2133707_2021-05-20_2419', ['4500474_2133707_2021-05-20_2419']), + ('4500474_2133707_2021-05-20_2419,4500474_2133707_2021-05-20_2420', + ['4500474_2133707_2021-05-20_2419', '4500474_2133707_2021-05-20_2420']) + ]) @respx.mock -def test_cli_orders_create_basic(invoke, order_description): +def test_cli_orders_create_basic_success( + expected_ids, id_string, invoke, order_description): mock_resp = httpx.Response(HTTPStatus.OK, json=order_description) respx.post(TEST_ORDERS_URL).return_value = mock_resp result = invoke([ 'orders', 'create', '--name', 'test', - '--id', '4500474_2133707_2021-05-20_2419', + '--id', id_string, '--bundle', 'analytic', '--item-type', 'PSOrthoTile' ]) @@ -323,7 +330,7 @@ def test_cli_orders_create_basic(invoke, order_description): order_request = { "name": "test", "products": [{ - "item_ids": ["4500474_2133707_2021-05-20_2419"], + "item_ids": expected_ids, "item_type": "PSOrthoTile", "product_bundle": "analytic" }], @@ -344,6 +351,18 @@ def test_cli_orders_create_basic_item_type_invalid(invoke): assert 'Error: Invalid value: item_type' in result.output +def test_cli_orders_create_id_empty(invoke): + result = invoke([ + 'orders', 'create', + '--name', 'test', + '--id', '', + '--bundle', 'analytic', + '--item-type', 'invalid' + ]) + assert result.exit_code + assert 'id cannot be empty string.' in result.output + + @respx.mock def test_cli_orders_create_clip( invoke, geom_geojson, order_description, write_to_tmp_json_file): From 7152fc7fa0b20042d7e9b46e3fdd19bc88e548d4 Mon Sep 17 00:00:00 2001 From: Jennifer Reiber Kyle Date: Tue, 1 Feb 2022 17:03:15 -0800 Subject: [PATCH 08/13] undo base_url change, change where slashes appear when building api urls, reduce passing test coverage to 97% --- planet/auth.py | 11 ++-- planet/cli/auth.py | 7 ++- planet/cli/cli.py | 5 +- planet/cli/orders.py | 6 ++- planet/clients/orders.py | 29 +++++----- planet/constants.py | 2 +- setup.cfg | 2 +- tests/integration/test_auth_api.py | 6 +-- tests/integration/test_auth_cli.py | 9 ++-- tests/integration/test_orders_api.py | 43 +++++++-------- tests/integration/test_orders_cli.py | 81 ++++++++++++++-------------- 11 files changed, 98 insertions(+), 103 deletions(-) diff --git a/planet/auth.py b/planet/auth.py index f9c4592f4..797925616 100644 --- a/planet/auth.py +++ b/planet/auth.py @@ -27,8 +27,7 @@ LOGGER = logging.getLogger(__name__) -AUTH_PATH = 'v0/auth/' - +BASE_URL = f'{PLANET_BASE_URL}/v0/auth' ENV_API_KEY = 'PL_API_KEY' @@ -158,9 +157,9 @@ def __init__( base_url: The base URL to use. Defaults to Planet production services base url. """ - self._base_url = base_url or PLANET_BASE_URL - if not self._base_url.endswith('/'): - self._base_url += '/' + self._base_url = base_url or BASE_URL + if self._base_url.endswith('/'): + self._base_url = self._base_url[:-1] def login( self, @@ -180,7 +179,7 @@ def login( A JSON object containing an `api_key` property with the user's API_KEY. ''' - url = f'{self._base_url}{AUTH_PATH}login' + url = f'{self._base_url}/login' data = {'email': email, 'password': password } diff --git a/planet/cli/auth.py b/planet/cli/auth.py index a57328868..c8ba813b5 100644 --- a/planet/cli/auth.py +++ b/planet/cli/auth.py @@ -24,9 +24,12 @@ @click.group() @click.pass_context -def auth(ctx): +@click.option('-u', '--base-url', + default=None, + help='Assign custom base Auth API URL.') +def auth(ctx, base_url): '''Commands for working with Planet authentication''' - pass + ctx.obj['BASE_URL'] = base_url @auth.command() diff --git a/planet/cli/cli.py b/planet/cli/cli.py index c097856b1..db734999f 100644 --- a/planet/cli/cli.py +++ b/planet/cli/cli.py @@ -26,20 +26,17 @@ @click.group() @click.pass_context -@click.option('--base-url', default=None, - help='Assign custom base url for Planet services.') @click.option('-v', '--verbose', count=True, help=('Specify verbosity level of between 0 and 2 corresponding ' 'to log levels warning, info, and debug respectively.')) @click.version_option(version=planet.__version__) -def main(ctx, base_url, verbose): +def main(ctx, verbose): '''Planet API Client''' _configure_logging(verbose) # ensure that ctx.obj exists and is a dict (in case `cli()` is called # by means other than the `if` block below) ctx.ensure_object(dict) - ctx.obj['BASE_URL'] = base_url def _configure_logging(verbosity): diff --git a/planet/cli/orders.py b/planet/cli/orders.py index a19b91ead..2f3134ccb 100644 --- a/planet/cli/orders.py +++ b/planet/cli/orders.py @@ -41,9 +41,13 @@ async def orders_client(ctx): @click.group() @click.pass_context -def orders(ctx): +@click.option('-u', '--base-url', + default=None, + help='Assign custom base Orders API URL.') +def orders(ctx, base_url): '''Commands for interacting with the Orders API''' ctx.obj['AUTH'] = planet.Auth.from_file() + ctx.obj['BASE_URL'] = base_url @orders.command() diff --git a/planet/clients/orders.py b/planet/clients/orders.py index 691e85954..5a5e755b4 100644 --- a/planet/clients/orders.py +++ b/planet/clients/orders.py @@ -26,9 +26,10 @@ from ..models import Order, Orders, Request, Response, StreamingBody -STATS_PATH = 'compute/ops/stats/orders/v2/' -ORDERS_PATH = 'compute/ops/orders/v2/' -BULK_PATH = 'compute/ops/bulk/orders/v2/' +BASE_URL = f'{PLANET_BASE_URL}compute/ops' +STATS_PATH = '/stats/orders/v2' +ORDERS_PATH = '/orders/v2' +BULK_PATH = '/bulk/orders/v2' # Order states https://developers.planet.com/docs/orders/ordering/#order-states ORDERS_STATES_COMPLETE = ['success', 'partial', 'cancelled', 'failed'] @@ -73,9 +74,9 @@ def __init__( """ self._session = session - self._base_url = base_url or PLANET_BASE_URL - if not self._base_url.endswith('/'): - self._base_url += '/' + self._base_url = base_url or BASE_URL + if self._base_url.endswith('/'): + self._base_url = self._base_url[:-1] @staticmethod def _check_order_id(oid): @@ -94,13 +95,6 @@ def _orders_url(self): def _stats_url(self): return f'{self._base_url}{STATS_PATH}' - def _order_url(self, order_id): - self._check_order_id(order_id) - return f'{self._orders_url()}{order_id}' - - def _bulk_url(self): - return f'{self._base_url}{BULK_PATH}' - def _request(self, url, method, data=None, params=None, json=None): return Request(url, method=method, data=data, params=params, json=json) @@ -187,7 +181,8 @@ async def get_order( OrdersClientException: If order_id is not valid UUID. planet.exceptions.APIException: On API error. ''' - url = self._order_url(order_id) + self._check_order_id(order_id) + url = f'{self._orders_url()}/{order_id}' req = self._request(url, method='GET') @@ -221,7 +216,9 @@ async def cancel_order( OrdersClientException: If order_id is not valid UUID. planet.exceptions.APIException: On API error. ''' - url = self._order_url(order_id) + self._check_order_id(order_id) + url = f'{self._orders_url()}/{order_id}' + req = self._request(url, method='PUT') try: @@ -249,7 +246,7 @@ async def cancel_orders( Raises: planet.exceptions.APIException: On API error. ''' - url = self._bulk_url() + 'cancel' + url = f'{self._base_url}{BULK_PATH}/cancel' cancel_body = {} if order_ids: for oid in order_ids: diff --git a/planet/constants.py b/planet/constants.py index dc320a828..7dad8351b 100644 --- a/planet/constants.py +++ b/planet/constants.py @@ -14,6 +14,6 @@ '''Constants used across the code base''' import os -PLANET_BASE_URL = 'https://api.planet.com/' +PLANET_BASE_URL = 'https://api.planet.com' SECRET_FILE_PATH = os.path.join(os.path.expanduser('~'), '.planet.json') diff --git a/setup.cfg b/setup.cfg index 3c994c7b6..aa5bf8a28 100644 --- a/setup.cfg +++ b/setup.cfg @@ -10,7 +10,7 @@ addopts = --cov=tests --cov-report=term-missing --cov-report=xml - --cov-fail-under 98 + --cov-fail-under 97 -rxXs [coverage:run] diff --git a/tests/integration/test_auth_api.py b/tests/integration/test_auth_api.py index 9e2d425be..079f6b259 100644 --- a/tests/integration/test_auth_api.py +++ b/tests/integration/test_auth_api.py @@ -20,11 +20,11 @@ import respx from planet import exceptions -from planet.auth import AUTH_PATH, AuthClient +from planet.auth import AuthClient -TEST_URL = 'http://MockNotRealURL/' -TEST_LOGIN_URL = f'{TEST_URL}{AUTH_PATH}login' +TEST_URL = 'http://MockNotRealURL/api/path' +TEST_LOGIN_URL = f'{TEST_URL}/login' LOGGER = logging.getLogger(__name__) diff --git a/tests/integration/test_auth_cli.py b/tests/integration/test_auth_cli.py index aed9ae2c0..861129f8c 100644 --- a/tests/integration/test_auth_cli.py +++ b/tests/integration/test_auth_cli.py @@ -20,11 +20,10 @@ import pytest import respx -from planet.auth import AUTH_PATH from planet.cli import cli -TEST_URL = 'http://MockNotRealURL/' -TEST_LOGIN_URL = f'{TEST_URL}{AUTH_PATH}login' +TEST_URL = 'http://MockNotRealURL/api/path' +TEST_LOGIN_URL = f'{TEST_URL}/login' # skip the global mock of _SecretFile.read @@ -61,7 +60,7 @@ def test_cli_auth_init_success(redirect_secretfile): result = CliRunner().invoke( cli.main, - args=['--base-url', TEST_URL, 'auth', 'init'], + args=['auth', '--base-url', TEST_URL, 'init'], input='email\npw\n') # we would get a 'url not mocked' exception if the base url wasn't @@ -82,7 +81,7 @@ def test_cli_auth_init_bad_pw(redirect_secretfile): result = CliRunner().invoke( cli.main, - args=['--base-url', TEST_URL, 'auth', 'init'], + args=['auth', '--base-url', TEST_URL, 'init'], input='email\npw\n') assert result.exception diff --git a/tests/integration/test_orders_api.py b/tests/integration/test_orders_api.py index d17c55692..a34c76872 100644 --- a/tests/integration/test_orders_api.py +++ b/tests/integration/test_orders_api.py @@ -25,13 +25,13 @@ import respx from planet import OrdersClient, clients, exceptions, reporting -from planet.clients.orders import BULK_PATH, ORDERS_PATH, STATS_PATH +# from planet.clients.orders import BULK_PATH, ORDERS_PATH, STATS_PATH -TEST_URL = 'http://MockNotRealURL/' -TEST_BULK_URL = f'{TEST_URL}{BULK_PATH}' -TEST_DOWNLOAD_URL = f'{TEST_URL}comp/ops/download/' -TEST_ORDERS_URL = f'{TEST_URL}{ORDERS_PATH}' -TEST_STATS_URL = f'{TEST_URL}{STATS_PATH}' +TEST_URL = 'http://www.MockNotRealURL.com/api/path' +TEST_BULK_CANCEL_URL = f'{TEST_URL}/bulk/orders/v2/cancel' +TEST_DOWNLOAD_URL = f'{TEST_URL}/download' +TEST_ORDERS_URL = f'{TEST_URL}/orders/v2' +TEST_STATS_URL = f'{TEST_URL}/stats/orders/v2' LOGGER = logging.getLogger(__name__) @@ -59,7 +59,7 @@ def f(): {'location': dl_url}, ] - get_url = TEST_ORDERS_URL + oid + get_url = f'{TEST_ORDERS_URL}/{oid}' mock_resp = httpx.Response(HTTPStatus.OK, json=order_description) respx.get(get_url).return_value = mock_resp @@ -262,7 +262,7 @@ async def test_create_order_item_id_does_not_exist( @respx.mock @pytest.mark.asyncio async def test_get_order(oid, order_description, session): - get_url = TEST_ORDERS_URL + oid + get_url = f'{TEST_ORDERS_URL}/{oid}' mock_resp = httpx.Response(HTTPStatus.OK, json=order_description) respx.get(get_url).return_value = mock_resp @@ -283,7 +283,7 @@ async def test_get_order_invalid_id(session): @pytest.mark.asyncio async def test_get_order_id_doesnt_exist( oid, session, match_pytest_raises): - get_url = TEST_ORDERS_URL + oid + get_url = f'{TEST_ORDERS_URL}/{oid}' msg = f'Could not load order ID: {oid}.' resp = { @@ -301,7 +301,7 @@ async def test_get_order_id_doesnt_exist( @respx.mock @pytest.mark.asyncio async def test_cancel_order(oid, order_description, session): - cancel_url = TEST_ORDERS_URL + oid + cancel_url = f'{TEST_ORDERS_URL}/{oid}' order_description['state'] = 'cancelled' mock_resp = httpx.Response(HTTPStatus.OK, json=order_description) respx.put(cancel_url).return_value = mock_resp @@ -323,7 +323,7 @@ async def test_cancel_order_invalid_id(session): @pytest.mark.asyncio async def test_cancel_order_id_doesnt_exist( oid, session, match_pytest_raises): - cancel_url = TEST_ORDERS_URL + oid + cancel_url = f'{TEST_ORDERS_URL}/{oid}' msg = f'No such order ID: {oid}.' resp = { @@ -342,7 +342,7 @@ async def test_cancel_order_id_doesnt_exist( @pytest.mark.asyncio async def test_cancel_order_id_cannot_be_cancelled( oid, session, match_pytest_raises): - cancel_url = TEST_ORDERS_URL + oid + cancel_url = f'{TEST_ORDERS_URL}/{oid}' msg = 'Order not in a cancellable state' resp = { @@ -360,7 +360,6 @@ async def test_cancel_order_id_cannot_be_cancelled( @respx.mock @pytest.mark.asyncio async def test_cancel_orders_by_ids(session, oid): - bulk_cancel_url = TEST_BULK_URL + 'cancel' oid2 = '5ece1dc0-ea81-11eb-837c-acde48001122' test_ids = [oid, oid2] example_result = { @@ -378,7 +377,7 @@ async def test_cancel_orders_by_ids(session, oid): } } mock_resp = httpx.Response(HTTPStatus.OK, json=example_result) - respx.post(bulk_cancel_url).return_value = mock_resp + respx.post(TEST_BULK_CANCEL_URL).return_value = mock_resp cl = OrdersClient(session, base_url=TEST_URL) res = await cl.cancel_orders(test_ids) @@ -402,8 +401,6 @@ async def test_cancel_orders_by_ids_invalid_id(session, oid): @respx.mock @pytest.mark.asyncio async def test_cancel_orders_all(session): - bulk_cancel_url = TEST_BULK_URL + 'cancel' - example_result = { "result": { "succeeded": {"count": 2}, @@ -414,7 +411,7 @@ async def test_cancel_orders_all(session): } } mock_resp = httpx.Response(HTTPStatus.OK, json=example_result) - respx.post(bulk_cancel_url).return_value = mock_resp + respx.post(TEST_BULK_CANCEL_URL).return_value = mock_resp cl = OrdersClient(session, base_url=TEST_URL) res = await cl.cancel_orders() @@ -428,7 +425,7 @@ async def test_cancel_orders_all(session): @respx.mock @pytest.mark.asyncio async def test_poll(oid, order_description, session): - get_url = TEST_ORDERS_URL + oid + get_url = f'{TEST_ORDERS_URL}/{oid}' order_description2 = copy.deepcopy(order_description) order_description2['state'] = 'running' @@ -503,7 +500,7 @@ async def test_aggegated_order_stats(session): @respx.mock @pytest.mark.asyncio async def test_download_asset_md(tmpdir, session): - dl_url = TEST_DOWNLOAD_URL + '1?token=IAmAToken' + dl_url = TEST_DOWNLOAD_URL + '/1?token=IAmAToken' md_json = {'key': 'value'} md_headers = { @@ -523,7 +520,7 @@ async def test_download_asset_md(tmpdir, session): @respx.mock @pytest.mark.asyncio async def test_download_asset_img(tmpdir, open_test_img, session): - dl_url = TEST_DOWNLOAD_URL + '1?token=IAmAToken' + dl_url = TEST_DOWNLOAD_URL + '/1?token=IAmAToken' img_headers = { 'Content-Type': 'image/tiff', @@ -563,14 +560,14 @@ async def test_download_order_success(tmpdir, order_description, oid, session): ''' # Mock an HTTP response for download - dl_url1 = TEST_DOWNLOAD_URL + '1?token=IAmAToken' - dl_url2 = TEST_DOWNLOAD_URL + '2?token=IAmAnotherToken' + dl_url1 = TEST_DOWNLOAD_URL + '/1?token=IAmAToken' + dl_url2 = TEST_DOWNLOAD_URL + '/2?token=IAmAnotherToken' order_description['_links']['results'] = [ {'location': dl_url1}, {'location': dl_url2} ] - get_url = TEST_ORDERS_URL + oid + get_url = f'{TEST_ORDERS_URL}/{oid}' mock_resp = httpx.Response(HTTPStatus.OK, json=order_description) respx.get(get_url).return_value = mock_resp diff --git a/tests/integration/test_orders_cli.py b/tests/integration/test_orders_cli.py index 1e1fd3872..32d140dfb 100644 --- a/tests/integration/test_orders_cli.py +++ b/tests/integration/test_orders_cli.py @@ -23,11 +23,11 @@ import respx from planet.cli import cli -from planet.clients.orders import ORDERS_PATH -TEST_URL = 'http://MockNotRealURL/' -TEST_ORDERS_URL = f'{TEST_URL}{ORDERS_PATH}' -TEST_DOWNLOAD_URL = f'{TEST_URL}comp/ops/download/' +TEST_URL = 'http://MockNotRealURL/api/path' +TEST_DOWNLOAD_URL = f'{TEST_URL}/download' +TEST_ORDERS_URL = f'{TEST_URL}/orders/v2' + # NOTE: These tests use a lot of the same mocked responses as test_orders_api. @@ -36,14 +36,14 @@ def invoke(): def _invoke(extra_args, runner=None): runner = runner or CliRunner() - args = ['--base-url', TEST_URL] + extra_args + args = ['orders', '--base-url', TEST_URL] + extra_args return runner.invoke(cli.main, args=args) return _invoke @respx.mock def test_cli_orders_list_basic(invoke, order_descriptions): - next_page_url = TEST_ORDERS_URL + 'blob/?page_marker=IAmATest' + next_page_url = TEST_ORDERS_URL + '/blob/?page_marker=IAmATest' order1, order2, order3 = order_descriptions page1_response = { @@ -63,7 +63,7 @@ def test_cli_orders_list_basic(invoke, order_descriptions): mock_resp2 = httpx.Response(HTTPStatus.OK, json=page2_response) respx.get(next_page_url).return_value = mock_resp2 - result = invoke(['orders', 'list']) + result = invoke(['list']) assert not result.exception assert [order1, order2, order3] == json.loads(result.output) @@ -79,7 +79,7 @@ def test_cli_orders_list_empty(invoke): mock_resp = httpx.Response(HTTPStatus.OK, json=page1_response) respx.get(TEST_ORDERS_URL).return_value = mock_resp - result = invoke(['orders', 'list']) + result = invoke(['list']) assert not result.exception assert [] == json.loads(result.output) @@ -101,7 +101,7 @@ def test_cli_orders_list_state(invoke, order_descriptions): # if the value of state doesn't get sent as a url parameter, # the mock will fail and this test will fail - result = invoke(['orders', 'list', '--state', 'failed']) + result = invoke(['list', '--state', 'failed']) assert not result.exception assert [order1, order2] == json.loads(result.output) @@ -121,7 +121,7 @@ def test_cli_orders_list_limit(invoke, order_descriptions): # limiting is done within the client, no change to api call respx.get(TEST_ORDERS_URL).return_value = mock_resp - result = invoke(['orders', 'list', '--limit', '1']) + result = invoke(['list', '--limit', '1']) assert not result.exception assert [order1] == json.loads(result.output) @@ -140,54 +140,54 @@ def test_cli_orders_list_pretty(invoke, monkeypatch, order_description): mock_resp = httpx.Response(HTTPStatus.OK, json=page1_response) respx.get(TEST_ORDERS_URL).return_value = mock_resp - result = invoke(['orders', 'list', '--pretty']) + result = invoke(['list', '--pretty']) assert not result.exception mock_echo_json.assert_called_once_with([order_description], True) @respx.mock def test_cli_orders_get(invoke, oid, order_description): - get_url = TEST_ORDERS_URL + oid + get_url = f'{TEST_ORDERS_URL}/{oid}' mock_resp = httpx.Response(HTTPStatus.OK, json=order_description) respx.get(get_url).return_value = mock_resp - result = invoke(['orders', 'get', oid]) + result = invoke(['get', oid]) assert not result.exception assert order_description == json.loads(result.output) @respx.mock def test_cli_orders_get_id_not_found(invoke, oid): - get_url = TEST_ORDERS_URL + oid + get_url = f'{TEST_ORDERS_URL}/{oid}' error_json = {'message': 'A descriptive error message'} mock_resp = httpx.Response(404, json=error_json) respx.get(get_url).return_value = mock_resp - result = invoke(['orders', 'get', oid]) + result = invoke(['get', oid]) assert result.exception assert 'Error: A descriptive error message\n' == result.output @respx.mock def test_cli_orders_cancel(invoke, oid, order_description): - cancel_url = TEST_ORDERS_URL + oid + cancel_url = f'{TEST_ORDERS_URL}/{oid}' order_description['state'] = 'cancelled' mock_resp = httpx.Response(HTTPStatus.OK, json=order_description) respx.put(cancel_url).return_value = mock_resp - result = invoke(['orders', 'cancel', oid]) + result = invoke(['cancel', oid]) assert not result.exception assert 'Cancelled\n' == result.output @respx.mock def test_cli_orders_cancel_id_not_found(invoke, oid): - cancel_url = TEST_ORDERS_URL + oid + cancel_url = f'{TEST_ORDERS_URL}/{oid}' error_json = {'message': 'A descriptive error message'} mock_resp = httpx.Response(404, json=error_json) respx.put(cancel_url).return_value = mock_resp - result = invoke(['orders', 'cancel', oid]) + result = invoke(['cancel', oid]) assert result.exception assert 'Error: A descriptive error message\n' == result.output @@ -197,14 +197,14 @@ def mock_download_response(oid, order_description): def _func(): # Mock an HTTP response for polling and download order_description['state'] = 'success' - dl_url1 = TEST_DOWNLOAD_URL + '1?token=IAmAToken' - dl_url2 = TEST_DOWNLOAD_URL + '2?token=IAmAnotherToken' + dl_url1 = TEST_DOWNLOAD_URL + '/1?token=IAmAToken' + dl_url2 = TEST_DOWNLOAD_URL + '/2?token=IAmAnotherToken' order_description['_links']['results'] = [ {'location': dl_url1}, {'location': dl_url2} ] - get_url = TEST_ORDERS_URL + oid + get_url = f'{TEST_ORDERS_URL}/{oid}' mock_resp = httpx.Response(HTTPStatus.OK, json=order_description) respx.get(get_url).return_value = mock_resp @@ -234,7 +234,7 @@ def test_cli_orders_download(invoke, mock_download_response, oid): runner = CliRunner() with runner.isolated_filesystem() as folder: - result = invoke(['orders', 'download', oid], runner=runner) + result = invoke(['download', oid], runner=runner) assert not result.exception # output is progress reporting plus the message @@ -257,8 +257,7 @@ def test_cli_orders_download_dest(invoke, mock_download_response, oid): with runner.isolated_filesystem() as folder: dest_dir = Path(folder) / 'foobar' dest_dir.mkdir() - result = invoke(['orders', 'download', '--dest', 'foobar', oid], - runner=runner) + result = invoke(['download', '--dest', 'foobar', oid], runner=runner) assert not result.exception # Check that the files were downloaded to the custom directory @@ -279,14 +278,14 @@ def test_cli_orders_download_overwrite( write_to_tmp_json_file({'foo': 'bar'}, filepath) # check the file doesn't get overwritten by default - result = invoke(['orders', 'download', oid], - runner=runner) + result = invoke(['download', oid], runner=runner) assert not result.exception assert json.load(open(filepath)) == {'foo': 'bar'} # check the file gets overwritten - result = invoke(['orders', 'download', '--overwrite', oid], + result = invoke(['download', '--overwrite', oid], runner=runner) + assert not result.exception assert json.load(open(filepath)) == {'key': 'value'} @@ -297,7 +296,7 @@ def test_cli_orders_download_quiet(invoke, mock_download_response, oid): runner = CliRunner() with runner.isolated_filesystem(): - result = invoke(['orders', 'download', '-q', oid], runner=runner) + result = invoke(['download', '-q', oid], runner=runner) assert not result.exception # no progress reporting, just the message @@ -318,7 +317,7 @@ def test_cli_orders_create_basic_success( respx.post(TEST_ORDERS_URL).return_value = mock_resp result = invoke([ - 'orders', 'create', + 'create', '--name', 'test', '--id', id_string, '--bundle', 'analytic', @@ -341,7 +340,7 @@ def test_cli_orders_create_basic_success( def test_cli_orders_create_basic_item_type_invalid(invoke): result = invoke([ - 'orders', 'create', + 'create', '--name', 'test', '--id', '4500474_2133707_2021-05-20_2419', '--bundle', 'analytic', @@ -353,7 +352,7 @@ def test_cli_orders_create_basic_item_type_invalid(invoke): def test_cli_orders_create_id_empty(invoke): result = invoke([ - 'orders', 'create', + 'create', '--name', 'test', '--id', '', '--bundle', 'analytic', @@ -372,7 +371,7 @@ def test_cli_orders_create_clip( aoi_file = write_to_tmp_json_file(geom_geojson, 'aoi.geojson') result = invoke([ - 'orders', 'create', + 'create', '--name', 'test', '--id', '4500474_2133707_2021-05-20_2419', '--bundle', 'analytic', @@ -405,7 +404,7 @@ def test_cli_orders_create_clip_featureclass( fc_file = write_to_tmp_json_file(featureclass_geojson, 'fc.geojson') result = invoke([ - 'orders', 'create', + 'create', '--name', 'test', '--id', '4500474_2133707_2021-05-20_2419', '--bundle', 'analytic', @@ -432,7 +431,7 @@ def test_cli_orders_create_clip_invalid_geometry( aoi_file = write_to_tmp_json_file(point_geom_geojson, 'aoi.geojson') result = invoke([ - 'orders', 'create', + 'create', '--name', 'test', '--id', '4500474_2133707_2021-05-20_2419', '--bundle', 'analytic', @@ -452,7 +451,7 @@ def test_cli_orders_create_clip_and_tools( aoi_file = write_to_tmp_json_file(geom_geojson, 'aoi.geojson') result = invoke([ - 'orders', 'create', + 'create', '--name', 'test', '--id', '4500474_2133707_2021-05-20_2419', '--bundle', 'analytic', @@ -482,7 +481,7 @@ def test_cli_orders_create_cloudconfig( config_file = write_to_tmp_json_file(config_json, 'config.json') result = invoke([ - 'orders', 'create', + 'create', '--name', 'test', '--id', '4500474_2133707_2021-05-20_2419', '--bundle', 'analytic', @@ -511,7 +510,7 @@ def test_cli_orders_create_email( respx.post(TEST_ORDERS_URL).return_value = mock_resp result = invoke([ - 'orders', 'create', + 'create', '--name', 'test', '--id', '4500474_2133707_2021-05-20_2419', '--bundle', 'analytic', @@ -543,7 +542,7 @@ def test_cli_orders_create_tools( tools_file = write_to_tmp_json_file(tools_json, 'tools.json') result = invoke([ - 'orders', 'create', + 'create', '--name', 'test', '--id', '4500474_2133707_2021-05-20_2419', '--bundle', 'analytic', @@ -567,7 +566,7 @@ def test_cli_orders_create_tools( def test_cli_orders_read_file_json_doesnotexist(invoke): result = invoke([ - 'orders', 'create', + 'create', '--name', 'test', '--id', '4500474_2133707_2021-05-20_2419', '--bundle', 'analytic', @@ -586,7 +585,7 @@ def test_cli_orders_read_file_json_invalidjson(invoke, tmp_path): fp.write('[Invali]d j*son') result = invoke([ - 'orders', 'create', + 'create', '--name', 'test', '--id', '4500474_2133707_2021-05-20_2419', '--bundle', 'analytic', From 50ba5f2dff1b43c0f2de69a755746d6e28a29980 Mon Sep 17 00:00:00 2001 From: Jennifer Reiber Kyle Date: Tue, 1 Feb 2022 17:03:38 -0800 Subject: [PATCH 09/13] add unit tests for cli io module --- tests/unit/test_cli_io.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 tests/unit/test_cli_io.py diff --git a/tests/unit/test_cli_io.py b/tests/unit/test_cli_io.py new file mode 100644 index 000000000..80377136a --- /dev/null +++ b/tests/unit/test_cli_io.py @@ -0,0 +1,30 @@ +# Copyright 2022 Planet Labs, PBC. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. +from unittest.mock import Mock + +import pytest + +from planet.cli import io + + +@pytest.mark.parametrize( + "pretty,expected", + [(False, '{"key": "val"}'), (True, '{\n "key": "val"\n}')]) +def test_cli_echo_json(pretty, expected, monkeypatch): + mock_echo = Mock() + monkeypatch.setattr(io.click, 'echo', mock_echo) + + obj = {'key': 'val'} + io.echo_json(obj, pretty) + mock_echo.assert_called_once_with(expected) From 223d0051722011222b8ef769b30605d8b3d123b9 Mon Sep 17 00:00:00 2001 From: Jennifer Reiber Kyle Date: Tue, 1 Feb 2022 17:04:50 -0800 Subject: [PATCH 10/13] remove unreachable exception handling code --- planet/cli/orders.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/planet/cli/orders.py b/planet/cli/orders.py index 2f3134ccb..05030648d 100644 --- a/planet/cli/orders.py +++ b/planet/cli/orders.py @@ -157,8 +157,6 @@ def read_file_json(ctx, param, value): json_value = json.load(value) except json.decoder.JSONDecodeError: raise click.ClickException('File does not contain valid json.') - except click.FileError as e: - raise click.ClickException(e) return json_value From 8d562924d2d9a2820177e4bae1fb6c81f05d9c4c Mon Sep 17 00:00:00 2001 From: Jennifer Reiber Kyle Date: Wed, 2 Feb 2022 15:52:59 -0800 Subject: [PATCH 11/13] fixup quote changes and base_url docs --- planet/auth.py | 8 ++++---- planet/cli/auth.py | 2 +- planet/clients/orders.py | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/planet/auth.py b/planet/auth.py index 797925616..274522e29 100644 --- a/planet/auth.py +++ b/planet/auth.py @@ -109,8 +109,8 @@ def from_login( Parameters: email: Planet account email address. password: Planet account password. - base_url: The base URL to use. Defaults to Planet production - services base url. + base_url: The base URL to use. Defaults to production + authentication API base url. ''' cl = AuthClient(base_url=base_url) auth_data = cl.login(email, password) @@ -154,8 +154,8 @@ def __init__( ): """ Parameters: - base_url: The base URL to use. Defaults to Planet production - services base url. + base_url: The base URL to use. Defaults to production + authentication API base url. """ self._base_url = base_url or BASE_URL if self._base_url.endswith('/'): diff --git a/planet/cli/auth.py b/planet/cli/auth.py index c8ba813b5..74352e6f8 100644 --- a/planet/cli/auth.py +++ b/planet/cli/auth.py @@ -29,7 +29,7 @@ help='Assign custom base Auth API URL.') def auth(ctx, base_url): '''Commands for working with Planet authentication''' - ctx.obj['BASE_URL'] = base_url + ctx.obj["BASE_URL"] = base_url @auth.command() diff --git a/planet/clients/orders.py b/planet/clients/orders.py index 5a5e755b4..094055975 100644 --- a/planet/clients/orders.py +++ b/planet/clients/orders.py @@ -69,8 +69,8 @@ def __init__( """ Parameters: session: Open session connected to server. - base_url: The base URL to use. Defaults to Planet production - services base url. + base_url: The base URL to use. Defaults to production orders API + base url. """ self._session = session From 2731387dc75dfd93707bb906511b631c9e79181b Mon Sep 17 00:00:00 2001 From: Jennifer Reiber Kyle Date: Wed, 2 Feb 2022 15:54:49 -0800 Subject: [PATCH 12/13] minor change for string quote matching --- planet/cli/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/planet/cli/auth.py b/planet/cli/auth.py index 74352e6f8..c8ba813b5 100644 --- a/planet/cli/auth.py +++ b/planet/cli/auth.py @@ -29,7 +29,7 @@ help='Assign custom base Auth API URL.') def auth(ctx, base_url): '''Commands for working with Planet authentication''' - ctx.obj["BASE_URL"] = base_url + ctx.obj['BASE_URL'] = base_url @auth.command() From 45ab4247fe04875f4f2f69537b880afb624cb45d Mon Sep 17 00:00:00 2001 From: Jennifer Reiber Kyle Date: Thu, 3 Feb 2022 17:36:40 -0800 Subject: [PATCH 13/13] use unittest.patch instead of pytest monkeypatch --- tests/unit/test_cli_io.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/unit/test_cli_io.py b/tests/unit/test_cli_io.py index 80377136a..b3db23813 100644 --- a/tests/unit/test_cli_io.py +++ b/tests/unit/test_cli_io.py @@ -11,7 +11,7 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations under # the License. -from unittest.mock import Mock +from unittest.mock import patch import pytest @@ -21,10 +21,8 @@ @pytest.mark.parametrize( "pretty,expected", [(False, '{"key": "val"}'), (True, '{\n "key": "val"\n}')]) -def test_cli_echo_json(pretty, expected, monkeypatch): - mock_echo = Mock() - monkeypatch.setattr(io.click, 'echo', mock_echo) - +@patch('planet.cli.io.click.echo') +def test_cli_echo_json(mock_echo, pretty, expected): obj = {'key': 'val'} io.echo_json(obj, pretty) mock_echo.assert_called_once_with(expected)