Skip to content

Commit 4629b23

Browse files
authored
Add update command to modify a registered API (#17)
1 parent 379e838 commit 4629b23

File tree

6 files changed

+642
-2
lines changed

6 files changed

+642
-2
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
2+
Added
3+
-----
4+
5+
* Add ``update`` CLI command to modify a registered API by ID.

src/globus_registered_api/cli.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@
66

77
import json
88
import os
9+
import pathlib
910
import sys
1011
from collections.abc import Iterable
12+
from typing import Any
13+
from uuid import UUID
1114

1215
import click
1316
from globus_sdk import AuthClient
@@ -236,6 +239,116 @@ def get_registered_api(ctx: click.Context, registered_api_id: str, format: str)
236239
click.echo(f"Updated: {res['updated_timestamp']}")
237240

238241

242+
@cli.command("update")
243+
@click.argument("registered_api_id", type=click.UUID)
244+
@click.option("--name", help="Update the name of the registered API")
245+
@click.option("--description", help="Update the description of the registered API")
246+
@click.option(
247+
"--owner",
248+
"owners",
249+
multiple=True,
250+
help="Set owner URN (can specify multiple, can only be set by owners)",
251+
)
252+
@click.option(
253+
"--administrator",
254+
"administrators",
255+
multiple=True,
256+
help="Set administrator URN (can specify multiple, can only be set by owners)",
257+
)
258+
@click.option(
259+
"--viewer",
260+
"viewers",
261+
multiple=True,
262+
help="Set viewer URN (can specify multiple, can only be set by owners and administrators)",
263+
)
264+
@click.option(
265+
"--no-administrators",
266+
is_flag=True,
267+
help="Clear all administrators (can only be set by owners)",
268+
)
269+
@click.option(
270+
"--no-viewers",
271+
is_flag=True,
272+
help="Clear all viewers (can only be set by owners and administrators)",
273+
)
274+
@click.option(
275+
"--target-file",
276+
type=click.Path(exists=True, dir_okay=False, path_type=pathlib.Path),
277+
help="Path to JSON file containing target definition",
278+
)
279+
@click.option("--format", type=click.Choice(["json", "text"]), default="text")
280+
@click.pass_context
281+
def update_registered_api(
282+
ctx: click.Context,
283+
registered_api_id: UUID,
284+
name: str | None,
285+
description: str | None,
286+
owners: tuple[str, ...],
287+
administrators: tuple[str, ...],
288+
viewers: tuple[str, ...],
289+
no_administrators: bool,
290+
no_viewers: bool,
291+
target_file: pathlib.Path | None,
292+
format: str,
293+
) -> None:
294+
"""
295+
Update a registered API by ID.
296+
297+
Use this command to modify the name, description, roles, or target
298+
definition of an existing registered API.
299+
"""
300+
app: UserApp | ClientApp = ctx.obj
301+
flows_client = _create_flows_client(app)
302+
303+
# Validate mutually exclusive options
304+
if administrators and no_administrators:
305+
raise click.UsageError(
306+
"--administrator and --no-administrators cannot be used together"
307+
)
308+
if viewers and no_viewers:
309+
raise click.UsageError(
310+
"--viewer and --no-viewers cannot be used together"
311+
)
312+
313+
request: dict[str, Any] = {}
314+
if name is not None:
315+
request["name"] = name
316+
if description is not None:
317+
request["description"] = description
318+
if owners:
319+
request["owners"] = list(set(owners))
320+
if no_administrators:
321+
request["administrators"] = []
322+
elif administrators:
323+
request["administrators"] = list(set(administrators))
324+
if no_viewers:
325+
request["viewers"] = []
326+
elif viewers:
327+
request["viewers"] = list(set(viewers))
328+
if target_file is not None:
329+
try:
330+
request["target"] = json.loads(target_file.read_text())
331+
except json.JSONDecodeError as e:
332+
raise click.UsageError(f"Invalid JSON in target file: {e}")
333+
except UnicodeDecodeError as e:
334+
raise click.UsageError(f"Unable to read target file: {e}")
335+
336+
res = flows_client.update_registered_api(registered_api_id, **request)
337+
338+
if format == "json":
339+
click.echo(json.dumps(res.data, indent=2))
340+
else:
341+
click.echo(f"ID: {res['id']}")
342+
click.echo(f"Name: {res['name']}")
343+
click.echo(f"Description: {res['description']}")
344+
click.echo(f"Owners: {res['roles']['owners']}")
345+
click.echo(f"Administrators: {res['roles']['administrators']}")
346+
click.echo(f"Viewers: {res['roles']['viewers']}")
347+
click.echo(f"Created: {res['created_timestamp']}")
348+
if res.get("edited_timestamp"):
349+
click.echo(f"Edited: {res['edited_timestamp']}")
350+
351+
239352
# --- willdelete command group ---
240353

241354

src/globus_registered_api/extended_flows_client.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,46 @@ def get_registered_api(
7979
:return: Response containing the registered API details
8080
"""
8181
return self.get(f"/registered_apis/{registered_api_id}")
82+
83+
def update_registered_api(
84+
self,
85+
registered_api_id: str | uuid.UUID,
86+
*,
87+
name: str | None = None,
88+
description: str | None = None,
89+
owners: list[str] | None = None,
90+
administrators: list[str] | None = None,
91+
viewers: list[str] | None = None,
92+
target: dict[str, t.Any] | None = None,
93+
) -> GlobusHTTPResponse:
94+
"""
95+
Update a registered API by ID.
96+
97+
:param registered_api_id: The ID of the registered API to update
98+
:param name: New name for the registered API
99+
:param description: New description for the registered API
100+
:param owners: List of owner URNs (replaces existing owners)
101+
:param administrators: List of administrator URNs (replaces existing)
102+
:param viewers: List of viewer URNs (replaces existing)
103+
:param target: Target definition dict
104+
:return: Response containing the updated registered API
105+
"""
106+
body: dict[str, t.Any] = _filter_nones(
107+
{
108+
"name": name,
109+
"description": description,
110+
"target": target,
111+
}
112+
)
113+
114+
roles = _filter_nones(
115+
{
116+
"owners": owners,
117+
"administrators": administrators,
118+
"viewers": viewers,
119+
}
120+
)
121+
if roles:
122+
body["roles"] = roles
123+
124+
return self.patch(f"/registered_apis/{registered_api_id}", data=body)

tests/conftest.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
GET_REGISTERED_API_URL = re.compile(
1717
r"https://.*flows.*\.globus\.org/registered_apis/[a-f0-9-]+"
1818
)
19+
UPDATE_REGISTERED_API_URL = re.compile(
20+
r"https://.*flows.*\.globus\.org/registered_apis/[a-f0-9-]+"
21+
)
1922

2023

2124
@pytest.fixture(autouse=True)

tests/test_extended_flows_client.py

Lines changed: 143 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,22 @@
33
# Copyright 2025 Globus <support@globus.org>
44
# SPDX-License-Identifier: Apache-2.0
55

6+
import json
67
import uuid
78

89
import pytest
910
import responses
10-
from conftest import GET_REGISTERED_API_URL
11-
from conftest import LIST_REGISTERED_APIS_URL
11+
from conftest import (
12+
GET_REGISTERED_API_URL,
13+
LIST_REGISTERED_APIS_URL,
14+
UPDATE_REGISTERED_API_URL,
15+
)
1216
from globus_sdk import GlobusHTTPResponse
1317

1418
from globus_registered_api.extended_flows_client import ExtendedFlowsClient
1519

1620

21+
1722
@pytest.fixture
1823
def client():
1924
return ExtendedFlowsClient()
@@ -179,3 +184,139 @@ def test_get_registered_api(client):
179184
assert response["name"] == "Test API"
180185
assert response["description"] == "A test API"
181186
assert f"/registered_apis/{api_id}" in responses.calls[0].request.url
187+
188+
189+
def test_update_registered_api_basic(client):
190+
api_id = uuid.uuid4()
191+
responses.add(
192+
responses.PATCH,
193+
UPDATE_REGISTERED_API_URL,
194+
json={
195+
"id": str(api_id),
196+
"name": "Updated API",
197+
"description": "Updated description",
198+
"roles": {
199+
"owners": ["urn:globus:auth:identity:user1"],
200+
"administrators": [],
201+
"viewers": [],
202+
},
203+
"created_timestamp": "2025-01-01T00:00:00+00:00",
204+
"edited_timestamp": None,
205+
},
206+
)
207+
208+
response = client.update_registered_api(api_id, name="Updated API")
209+
210+
assert isinstance(response, GlobusHTTPResponse)
211+
assert response["name"] == "Updated API"
212+
assert f"/registered_apis/{api_id}" in responses.calls[0].request.url
213+
assert responses.calls[0].request.method == "PATCH"
214+
215+
216+
def test_update_registered_api_with_description(client):
217+
api_id = uuid.uuid4()
218+
responses.add(
219+
responses.PATCH,
220+
UPDATE_REGISTERED_API_URL,
221+
json={
222+
"id": str(api_id),
223+
"name": "Test API",
224+
"description": "New description",
225+
"roles": {
226+
"owners": ["urn:globus:auth:identity:user1"],
227+
"administrators": [],
228+
"viewers": [],
229+
},
230+
"created_timestamp": "2025-01-01T00:00:00+00:00",
231+
"edited_timestamp": None,
232+
},
233+
)
234+
235+
response = client.update_registered_api(api_id, description="New description")
236+
237+
assert response["description"] == "New description"
238+
239+
240+
def test_update_registered_api_with_roles(client):
241+
api_id = uuid.uuid4()
242+
new_owners = ["urn:globus:auth:identity:user1", "urn:globus:auth:identity:user2"]
243+
responses.add(
244+
responses.PATCH,
245+
UPDATE_REGISTERED_API_URL,
246+
json={
247+
"id": str(api_id),
248+
"name": "Test API",
249+
"description": "Test",
250+
"roles": {
251+
"owners": new_owners,
252+
"administrators": [],
253+
"viewers": [],
254+
},
255+
"created_timestamp": "2025-01-01T00:00:00+00:00",
256+
"edited_timestamp": None,
257+
},
258+
)
259+
260+
response = client.update_registered_api(api_id, owners=new_owners)
261+
262+
assert response["roles"]["owners"] == new_owners
263+
264+
265+
def test_update_registered_api_with_target(client):
266+
api_id = uuid.uuid4()
267+
target = {
268+
"type": "openapi",
269+
"openapi_version": "3.1",
270+
"destination": {"method": "get", "url": "https://example.com/api"},
271+
"specification": {"operationId": "test-op", "responses": {}},
272+
}
273+
responses.add(
274+
responses.PATCH,
275+
UPDATE_REGISTERED_API_URL,
276+
json={
277+
"id": str(api_id),
278+
"name": "Test API",
279+
"description": "Test",
280+
"roles": {
281+
"owners": ["urn:globus:auth:identity:user1"],
282+
"administrators": [],
283+
"viewers": [],
284+
},
285+
"target": target,
286+
"created_timestamp": "2025-01-01T00:00:00+00:00",
287+
"edited_timestamp": "2025-01-02T00:00:00+00:00",
288+
},
289+
)
290+
291+
response = client.update_registered_api(api_id, target=target)
292+
293+
assert response["target"] == target
294+
295+
296+
def test_update_registered_api_omitted_params_not_in_request(client):
297+
api_id = uuid.uuid4()
298+
responses.add(
299+
responses.PATCH,
300+
UPDATE_REGISTERED_API_URL,
301+
json={
302+
"id": str(api_id),
303+
"name": "Updated Name",
304+
"description": "Original description",
305+
"roles": {
306+
"owners": ["urn:globus:auth:identity:user1"],
307+
"administrators": [],
308+
"viewers": [],
309+
},
310+
"created_timestamp": "2025-01-01T00:00:00+00:00",
311+
"edited_timestamp": None,
312+
},
313+
)
314+
315+
client.update_registered_api(api_id, name="Updated Name")
316+
317+
request_body = json.loads(responses.calls[0].request.body)
318+
# Only 'name' should be in the request, not None values for omitted params
319+
assert "name" in request_body
320+
assert "description" not in request_body
321+
assert "target" not in request_body
322+
assert "roles" not in request_body

0 commit comments

Comments
 (0)