Skip to content

Commit e8392e0

Browse files
authored
feat: Add Tag.update() -> Tag method (#351)
1 parent 6b82ba3 commit e8392e0

File tree

4 files changed

+136
-51
lines changed

4 files changed

+136
-51
lines changed

integration/tests/posit/connect/test_tags.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,16 @@ def test_tag_content_items(self):
150150
self.contentC["guid"],
151151
}
152152

153+
# Update tag
154+
tagDName = tagD.update(name="tagD_updated")
155+
assert tagDName["name"] == "tagD_updated"
156+
assert self.client.tags.get(tagD["id"])["name"] == "tagD_updated"
157+
158+
tagDParent = tagDName.update(parent=tagB)
159+
assert tagDParent["parent_id"] == tagB["id"]
160+
assert self.client.tags.get(tagD["id"])["parent_id"] == tagB["id"]
161+
162+
# Cleanup
153163
self.contentA.tags.delete(tagRoot)
154164
self.contentB.tags.delete(tagRoot)
155165
self.contentC.tags.delete(tagRoot)
@@ -159,6 +169,5 @@ def test_tag_content_items(self):
159169
assert len(tagC.content_items.find()) == 0
160170
assert len(tagD.content_items.find()) == 0
161171

162-
# cleanup
163172
tagRoot.destroy()
164173
assert len(self.client.tags.find()) == 0

src/posit/connect/tags.py

Lines changed: 85 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,32 @@ def find(self) -> list[Tag]:
1818
pass
1919

2020

21+
def _update_parent_kwargs(kwargs: dict) -> dict:
22+
"""
23+
Sets the `parent_id` key in the kwargs if `parent` is provided.
24+
25+
Asserts that the `parent=` and `parent_id=` keys are not both provided.
26+
"""
27+
parent = kwargs.get("parent", None)
28+
parent_id = kwargs.get("parent_id", None)
29+
if parent is None:
30+
# No parent to upgrade, return the kwargs as is
31+
return kwargs
32+
if parent and parent_id:
33+
raise ValueError("Cannot provide both `parent=` and `parent_id=`")
34+
if not isinstance(parent, Tag):
35+
raise TypeError(
36+
"`parent=` must be a Tag instance. If using a string, please use `parent_id=`"
37+
)
38+
39+
# Remove `parent` from a copy of `kwargs` and replace it with `parent_id`
40+
ret_kwargs = {**kwargs}
41+
del ret_kwargs["parent"]
42+
ret_kwargs["parent_id"] = parent["id"]
43+
44+
return ret_kwargs
45+
46+
2147
class Tag(Active):
2248
"""Tag resource."""
2349

@@ -135,6 +161,63 @@ def destroy(self) -> None:
135161
url = self._ctx.url + self._path
136162
self._ctx.session.delete(url)
137163

164+
# Allow for every combination of `name` and (`parent` or `parent_id`)
165+
@overload
166+
def update(self, /, *, name: str = ..., parent: Tag | None = ...) -> Tag: ...
167+
@overload
168+
def update(self, /, *, name: str = ..., parent_id: str | None = ...) -> Tag: ...
169+
170+
def update( # pyright: ignore[reportIncompatibleMethodOverride] ; This method returns `Tag`. Parent method returns `None`
171+
self,
172+
**kwargs,
173+
) -> Tag:
174+
"""
175+
Update the tag.
176+
177+
Parameters
178+
----------
179+
name : str
180+
The name of the tag.
181+
parent : Tag | None, optional
182+
The parent `Tag` object. If there is no parent, the tag is a top-level tag. To remove
183+
the parent tag, set the value to `None`. Only one of `parent` or `parent_id` can be
184+
provided.
185+
parent_id : str | None, optional
186+
The identifier for the parent tag. If there is no parent, the tag is a top-level tag.
187+
To remove the parent tag, set the value to `None`.
188+
189+
Returns
190+
-------
191+
Tag
192+
Updated tag object.
193+
194+
Examples
195+
--------
196+
```python
197+
import posit
198+
199+
client = posit.connect.Client()
200+
last_tag = client.tags.find()[-1]
201+
202+
# Update the tag's name
203+
updated_tag = last_tag.update(name="new_name")
204+
205+
# Remove the tag's parent
206+
updated_tag = last_tag.update(parent=None)
207+
updated_tag = last_tag.update(parent_id=None)
208+
209+
# Update the tag's parent
210+
parent_tag = client.tags.find()[0]
211+
updated_tag = last_tag.update(parent=parent_tag)
212+
updated_tag = last_tag.update(parent_id=parent_tag["id"])
213+
```
214+
"""
215+
updated_kwargs = _update_parent_kwargs(kwargs)
216+
url = self._ctx.url + self._path
217+
response = self._ctx.session.patch(url, json=updated_kwargs)
218+
result = response.json()
219+
return Tag(self._ctx, self._path, **result)
220+
138221

139222
class TagContentItems(ContextManager):
140223
def __init__(self, ctx: Context, path: str) -> None:
@@ -303,35 +386,6 @@ def get(self, tag_id: str) -> Tag:
303386
response = self._ctx.session.get(url)
304387
return Tag(self._ctx, path, **response.json())
305388

306-
def _update_parent_kwargs(self, kwargs: dict) -> dict:
307-
"""
308-
Sets the `parent_id` key in the kwargs if `parent` is provided.
309-
310-
Asserts that the `parent=` and `parent_id=` keys are not both provided.
311-
"""
312-
parent = kwargs.get("parent", None)
313-
if parent is None:
314-
# No parent to upgrade, return the kwargs as is
315-
return kwargs
316-
317-
if not isinstance(parent, Tag):
318-
raise TypeError(
319-
"`parent=` must be a Tag instance. If using a string, please use `parent_id=`"
320-
)
321-
322-
parent_id = kwargs.get("parent_id", None)
323-
if parent_id:
324-
raise ValueError("Cannot provide both `parent=` and `parent_id=`")
325-
326-
ret_kwargs = {**kwargs}
327-
328-
# Remove `parent` from ret_kwargs
329-
# and store the `parent_id` in the ret_kwargs below
330-
del ret_kwargs["parent"]
331-
332-
ret_kwargs["parent_id"] = parent["id"]
333-
return ret_kwargs
334-
335389
# Allow for every combination of `name` and (`parent` or `parent_id`)
336390
@overload
337391
def find(self, /, *, name: str = ..., parent: Tag = ...) -> list[Tag]: ...
@@ -379,7 +433,7 @@ def find(self, /, **kwargs) -> list[Tag]:
379433
subtags = client.tags.find(name="sub_name", parent=mytag["id"])
380434
```
381435
"""
382-
updated_kwargs = self._update_parent_kwargs(
436+
updated_kwargs = _update_parent_kwargs(
383437
kwargs, # pyright: ignore[reportArgumentType]
384438
)
385439
url = self._ctx.url + self._path
@@ -425,7 +479,7 @@ def create(self, /, **kwargs) -> Tag:
425479
tag = client.tags.create(name="tag_name", parent=category_tag)
426480
```
427481
"""
428-
updated_kwargs = self._update_parent_kwargs(
482+
updated_kwargs = _update_parent_kwargs(
429483
kwargs, # pyright: ignore[reportArgumentType]
430484
)
431485

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"id": "33",
3+
"name": "academy-updated",
4+
"parent_id": null,
5+
"created_time": "2021-10-18T18:37:56Z",
6+
"updated_time": "2021-10-18T18:37:56Z"
7+
}

tests/posit/connect/test_tags.py

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -252,10 +252,6 @@ def test_content_with_tag(self):
252252
@responses.activate
253253
def test_destroy(self):
254254
# behavior
255-
# mock_all_tags = responses.get(
256-
# "https://connect.example/__api__/v1/tags",
257-
# json=load_mock_list("v1/tags.json"),
258-
# )
259255
mock_29_tag = responses.get(
260256
"https://connect.example/__api__/v1/tags/29",
261257
json=load_mock_dict("v1/tags/29.json"),
@@ -264,32 +260,51 @@ def test_destroy(self):
264260
"https://connect.example/__api__/v1/tags/29",
265261
json={}, # empty response
266262
)
267-
# post_destroy_json = load_mock_list("v1/tags.json")
268-
# for tag in post_destroy_json:
269-
# if tag["id"] in {"29", "30"}:
270-
# post_destroy_json.remove(tag)
271-
# mock_all_tags_after_destroy = responses.get(
272-
# "https://connect.example/__api__/v1/tags",
273-
# json=post_destroy_json,
274-
# )
275263

276264
# setup
277265
client = Client(api_key="12345", url="https://connect.example")
278266

279267
# invoke
280-
# tags = client.tags.find()
281-
# assert len(tags) == 28
282268
tag29 = client.tags.get("29")
283269
tag29.destroy()
284-
# tags = client.tags.find()
285-
# # All children tags are removed
286-
# assert len(tags) == 26
287270

288271
# assert
289-
# assert mock_all_tags.call_count == 1
290272
assert mock_29_tag.call_count == 1
291273
assert mock_29_destroy.call_count == 1
292-
# assert mock_all_tags_after_destroy.call_count == 1
274+
275+
@responses.activate
276+
def test_update(self):
277+
# behavior
278+
mock_get_33_tag = responses.get(
279+
"https://connect.example/__api__/v1/tags/33",
280+
json=load_mock_dict("v1/tags/33.json"),
281+
)
282+
mock_update_33_tag = responses.patch(
283+
"https://connect.example/__api__/v1/tags/33",
284+
json=load_mock_dict("v1/tags/33-patched.json"),
285+
)
286+
287+
# setup
288+
client = Client(api_key="12345", url="https://connect.example")
289+
tag33 = client.tags.get("33")
290+
291+
# invoke
292+
updated_tag33_0 = tag33.update(name="academy-updated", parent_id=None)
293+
updated_tag33_1 = tag33.update(name="academy-updated", parent=None)
294+
295+
parent_tag = Tag(client._ctx, "/v1/tags/1", id="42", name="Parent")
296+
updated_tag33_2 = tag33.update(name="academy-updated", parent=parent_tag)
297+
updated_tag33_3 = tag33.update(name="academy-updated", parent_id=parent_tag["id"])
298+
299+
# assert
300+
assert mock_get_33_tag.call_count == 1
301+
assert mock_update_33_tag.call_count == 4
302+
303+
for tag in [updated_tag33_0, updated_tag33_1, updated_tag33_2, updated_tag33_3]:
304+
assert isinstance(tag, Tag)
305+
306+
# Asserting updated values are deferred to integration testing
307+
# to avoid agreening with the mocked data
293308

294309

295310
class TestContentItemTags:

0 commit comments

Comments
 (0)