Skip to content

Commit 69cc5e1

Browse files
authored
Merge pull request #1020 from planetlabs/subs-patch-1019
Support Subscriptions API `PATCH` requests
2 parents 6f95df6 + bb463df commit 69cc5e1

File tree

6 files changed

+115
-18
lines changed

6 files changed

+115
-18
lines changed

CONTRIBUTING.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,15 @@ git checkout -b new-branch-name
3333

3434
#### Branch Naming
3535

36-
Please use the following naming convention for development branchs:
36+
Please use the following naming convention for development branches:
3737

3838
`{up to 3-word summary of topic, separated by a dash)-{ticket number}`
3939

4040
For example: `release-contributing-691` for [ticket 691](https://github.com/planetlabs/planet-client-python/issues/691).
4141

4242
### Pull Requests
4343

44-
NOTE: Make sure to set the appropriate base branch for PRs. See Development Branch above for appriopriate branch.
44+
NOTE: Make sure to set the appropriate base branch for PRs. See Development Branch above for appropriate branch.
4545

4646
The Pull Request requirements are included in the pull request template as a list of checkboxes.
4747

planet/cli/subscriptions.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ async def cancel_subscription_cmd(ctx, subscription_id, pretty):
127127
@translate_exceptions
128128
@coro
129129
async def update_subscription_cmd(ctx, subscription_id, request, pretty):
130-
"""Update a subscription.
130+
"""Update a subscription via PUT.
131131
132132
Updates a subscription and prints the updated subscription description,
133133
optionally pretty-printed.
@@ -140,6 +140,27 @@ async def update_subscription_cmd(ctx, subscription_id, request, pretty):
140140
echo_json(sub, pretty)
141141

142142

143+
@subscriptions.command(name='patch') # type: ignore
144+
@click.argument('subscription_id')
145+
@click.argument('request', type=types.JSON())
146+
@pretty
147+
@click.pass_context
148+
@translate_exceptions
149+
@coro
150+
async def patch_subscription_cmd(ctx, subscription_id, request, pretty):
151+
"""Update a subscription via PATCH.
152+
153+
Updates a subscription and prints the updated subscription description,
154+
optionally pretty-printed.
155+
156+
REQUEST only requires the attributes to be changed. It must be
157+
JSON and can be specified a json string, filename, or '-' for stdin.
158+
"""
159+
async with subscriptions_client(ctx) as client:
160+
sub = await client.patch_subscription(subscription_id, request)
161+
echo_json(sub, pretty)
162+
163+
143164
@subscriptions.command(name='get') # type: ignore
144165
@click.argument('subscription_id')
145166
@pretty

planet/clients/subscriptions.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,11 +160,12 @@ async def cancel_subscription(self, subscription_id: str) -> None:
160160

161161
async def update_subscription(self, subscription_id: str,
162162
request: dict) -> dict:
163-
"""Update (edit) a Subscription.
163+
"""Update (edit) a Subscription via PUT.
164164
165165
Args
166166
subscription_id (str): id of the subscription to update.
167-
request (dict): subscription content for update.
167+
request (dict): subscription content for update, full
168+
payload is required.
168169
169170
Returns:
170171
dict: description of the updated subscription.
@@ -189,6 +190,38 @@ async def update_subscription(self, subscription_id: str,
189190
sub = resp.json()
190191
return sub
191192

193+
async def patch_subscription(self, subscription_id: str,
194+
request: dict) -> dict:
195+
"""Update (edit) a Subscription via PATCH.
196+
197+
Args
198+
subscription_id (str): id of the subscription to update.
199+
request (dict): subscription content for update, only
200+
attributes to update are required.
201+
202+
Returns:
203+
dict: description of the updated subscription.
204+
205+
Raises:
206+
APIError: on an API server error.
207+
ClientError: on a client error.
208+
"""
209+
url = f'{self._base_url}/{subscription_id}'
210+
211+
try:
212+
resp = await self._session.request(method='PATCH',
213+
url=url,
214+
json=request)
215+
# Forward APIError. We don't strictly need this clause, but it
216+
# makes our intent clear.
217+
except APIError:
218+
raise
219+
except ClientError: # pragma: no cover
220+
raise
221+
else:
222+
sub = resp.json()
223+
return sub
224+
192225
async def get_subscription(self, subscription_id: str) -> dict:
193226
"""Get a description of a Subscription.
194227

tests/integration/conftest.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ def test_disable_limiter(monkeypatch):
2828

2929

3030
@pytest.fixture
31-
@pytest.mark.anyio
3231
async def session():
3332
async with planet.Session() as ps:
3433
yield ps

tests/integration/test_subscriptions_api.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@ def result_pages(status=None, size=40):
7272

7373

7474
# The "creation", "update", and "cancel" mock APIs return submitted
75-
# data to the caller. They are used to test methods that rely on POST
76-
# or PUT.
75+
# data to the caller. They are used to test methods that rely on POST,
76+
# PATCH, or PUT.
7777
def modify_response(request):
7878
if request.content:
7979
return Response(200, json=json.loads(request.content))
@@ -89,6 +89,10 @@ def modify_response(request):
8989
update_mock.route(M(url=f'{TEST_URL}/test'),
9090
method='PUT').mock(side_effect=modify_response)
9191

92+
patch_mock = respx.mock()
93+
patch_mock.route(M(url=f'{TEST_URL}/test'),
94+
method='PATCH').mock(side_effect=modify_response)
95+
9296
cancel_mock = respx.mock()
9397
cancel_mock.route(M(url=f'{TEST_URL}/test/cancel'),
9498
method='POST').mock(side_effect=modify_response)
@@ -232,14 +236,24 @@ async def test_update_subscription_failure():
232236
@pytest.mark.anyio
233237
@update_mock
234238
async def test_update_subscription_success():
235-
"""Subscription is created, description has the expected items."""
239+
"""Subscription is updated, description has the expected items."""
236240
async with Session() as session:
237241
client = SubscriptionsClient(session, base_url=TEST_URL)
238242
sub = await client.update_subscription(
239243
"test", {
240-
'name': 'test', 'delivery': "no, thanks", 'source': 'test'
244+
"name": "test", "delivery": "no, thanks", "source": "test"
241245
})
242-
assert sub['delivery'] == "no, thanks"
246+
assert sub["delivery"] == "no, thanks"
247+
248+
249+
@pytest.mark.anyio
250+
@patch_mock
251+
async def test_patch_subscription_success():
252+
"""Subscription is patched, description has the expected items."""
253+
async with Session() as session:
254+
client = SubscriptionsClient(session, base_url=TEST_URL)
255+
sub = await client.patch_subscription("test", {"name": "test patch"})
256+
assert sub["name"] == "test patch"
243257

244258

245259
@pytest.mark.anyio

tests/integration/test_subscriptions_cli.py

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
"""Tests of the Subscriptions CLI (aka planet-subscriptions)
22
3-
There are 6 subscriptions commands:
3+
There are 7 subscriptions commands:
44
5-
[x] planet subscriptions list
5+
[x] planet subscriptions cancel
6+
[x] planet subscriptions create
67
[x] planet subscriptions get
8+
[x] planet subscriptions list
9+
[x] planet subscriptions patch
710
[x] planet subscriptions results
8-
[x] planet subscriptions create
911
[x] planet subscriptions update
10-
[x] planet subscriptions cancel
1112
1213
TODO: tests for 3 options of the planet-subscriptions-results command.
1314
@@ -21,12 +22,13 @@
2122
from planet.cli import cli
2223

2324
from test_subscriptions_api import (api_mock,
24-
failing_api_mock,
25-
create_mock,
26-
update_mock,
2725
cancel_mock,
26+
create_mock,
27+
failing_api_mock,
2828
get_mock,
29+
patch_mock,
2930
res_api_mock,
31+
update_mock,
3032
TEST_URL)
3133

3234
# CliRunner doesn't agree with empty options, so a list of option
@@ -192,6 +194,34 @@ def test_subscriptions_update_success(invoke):
192194
assert json.loads(result.output)['name'] == 'new_name'
193195

194196

197+
@failing_api_mock
198+
def test_subscriptions_patch_failure(invoke):
199+
"""Patch command exits gracefully from an API error."""
200+
result = invoke(
201+
['patch', 'test', json.dumps(GOOD_SUB_REQUEST)],
202+
# Note: catch_exceptions=True (the default) is required if we want
203+
# to exercise the "translate_exceptions" decorator and test for
204+
# failure.
205+
catch_exceptions=True)
206+
207+
assert result.exit_code == 1 # failure.
208+
209+
210+
@patch_mock
211+
def test_subscriptions_patch_success(invoke):
212+
"""Patch command succeeds."""
213+
request = {'name': 'test patch'}
214+
result = invoke(
215+
['patch', 'test', json.dumps(request)],
216+
# Note: catch_exceptions=True (the default) is required if we want
217+
# to exercise the "translate_exceptions" decorator and test for
218+
# failure.
219+
catch_exceptions=True)
220+
221+
assert result.exit_code == 0 # success.
222+
assert json.loads(result.output)['name'] == request['name']
223+
224+
195225
@failing_api_mock
196226
def test_subscriptions_get_failure(invoke):
197227
"""Describe command exits gracefully from an API error."""

0 commit comments

Comments
 (0)