Skip to content

Commit 9f8443c

Browse files
committed
--wip-- [skip ci]
1 parent 5d3f2b2 commit 9f8443c

File tree

5 files changed

+261
-65
lines changed

5 files changed

+261
-65
lines changed

src/posit/connect/client.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66

77
from requests import Response, Session
88

9-
from posit.connect.vanity import Vanities
10-
119
from . import hooks, me
1210
from .auth import Auth
1311
from .config import Config
@@ -19,6 +17,7 @@
1917
from .resources import ResourceParameters
2018
from .tasks import Tasks
2119
from .users import User, Users
20+
from .vanities import Vanities
2221

2322

2423
class Client(ContextManager):
@@ -275,7 +274,7 @@ def oauth(self) -> OAuth:
275274

276275
@property
277276
def vanities(self) -> Vanities:
278-
return Vanities(self.ctx)
277+
return Vanities(self.resource_params)
279278

280279
def __del__(self):
281280
"""Close the session when the Client instance is deleted."""

src/posit/connect/content.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from .permissions import Permissions
1515
from .resources import Resource, ResourceParameters, Resources
1616
from .tasks import Task
17-
from .vanity import VanityContentMixin
17+
from .vanities import VanityMixin
1818
from .variants import Variants
1919

2020

@@ -32,7 +32,7 @@ class ContentItemOwner(Resource):
3232
pass
3333

3434

35-
class ContentItem(Resource):
35+
class ContentItem(VanityMixin, Resource):
3636
def __getitem__(self, key: Any) -> Any:
3737
v = super().__getitem__(key)
3838
if key == "owner" and isinstance(v, dict):

src/posit/connect/vanities.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
from typing import Callable, Optional, Union, overload
2+
3+
from .resources import Resource, ResourceParameters, Resources
4+
5+
AfterDestroyCallback = Callable[[], None]
6+
7+
8+
class Vanity(Resource):
9+
"""Represents a Vanity resource with the ability to destroy itself."""
10+
11+
def __init__(
12+
self,
13+
/,
14+
params: ResourceParameters,
15+
*,
16+
after_destroy: AfterDestroyCallback = lambda: None,
17+
**kwargs,
18+
):
19+
super().__init__(params, **kwargs)
20+
self._after_destroy = after_destroy
21+
22+
def destroy(self) -> None:
23+
"""Destroy the vanity resource."""
24+
content_guid = self.get("content_guid")
25+
if content_guid is None:
26+
raise ValueError(
27+
"The 'content_guid' is missing. Unable to perform the destroy operation."
28+
)
29+
endpoint = self.params.url + f"v1/content/{content_guid}/vanity"
30+
self.params.session.delete(endpoint)
31+
self._after_destroy()
32+
33+
34+
class Vanities(Resources):
35+
"""Manages a collection of Vanity resources."""
36+
37+
def all(self) -> list[Vanity]:
38+
"""Retrieve all vanity resources."""
39+
endpoint = self.params.url + "v1/vanities"
40+
response = self.params.session.get(endpoint)
41+
results = response.json()
42+
return [Vanity(self.params, **result) for result in results]
43+
44+
45+
class VanityMixin(Resource):
46+
"""Mixin class to add vanity management capabilities to a resource."""
47+
48+
def __init__(self, /, params: ResourceParameters, **kwargs):
49+
super().__init__(params, **kwargs)
50+
self._vanity: Optional[Vanity] = None
51+
52+
@property
53+
def vanity(self) -> Vanity:
54+
"""Retrieve or lazily load the associated vanity resource."""
55+
if self._vanity is None:
56+
uid = self.get("guid")
57+
if uid is None:
58+
raise ValueError(
59+
"The 'guid' is missing. Unable to perform the get vanity operation."
60+
)
61+
endpoint = self.params.url + f"v1/content/{uid}/vanity"
62+
response = self.params.session.get(endpoint)
63+
result = response.json()
64+
self._vanity = Vanity(
65+
self.params, after_destroy=lambda: setattr(self, "_vanity", None), **result
66+
)
67+
return self._vanity
68+
69+
@vanity.setter
70+
def vanity(self, value: Union[str, dict]) -> None:
71+
"""Set the vanity using a path or dictionary of attributes."""
72+
if isinstance(value, str):
73+
self.set_vanity(path=value)
74+
elif isinstance(value, dict):
75+
self.set_vanity(**value)
76+
self.reset()
77+
78+
@vanity.deleter
79+
def vanity(self) -> None:
80+
"""Delete the vanity resource."""
81+
if self._vanity:
82+
self._vanity.destroy()
83+
self.reset()
84+
85+
def reset(self) -> None:
86+
"""Reset the cached vanity resource."""
87+
self._vanity = None
88+
89+
@overload
90+
def set_vanity(self, *, path: str) -> None: ...
91+
92+
@overload
93+
def set_vanity(self, *, path: str, force: bool) -> None: ...
94+
95+
@overload
96+
def set_vanity(self, **attributes) -> None: ...
97+
98+
def set_vanity(self, **attributes) -> None:
99+
"""Set or update the vanity resource with given attributes."""
100+
uid = self.get("guid")
101+
if uid is None:
102+
raise ValueError("The 'guid' is missing. Unable to perform the set vanity operation.")
103+
endpoint = self.params.url + f"v1/content/{uid}/vanity"
104+
self.params.session.put(endpoint, json=attributes)

src/posit/connect/vanity.py

Lines changed: 0 additions & 60 deletions
This file was deleted.
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
from unittest.mock import Mock
2+
3+
import pytest
4+
import requests
5+
import responses
6+
from responses.matchers import json_params_matcher
7+
8+
from posit.connect.resources import ResourceParameters
9+
from posit.connect.urls import Url
10+
from posit.connect.vanities import Vanities, Vanity, VanityMixin
11+
12+
13+
class TestVanityDestroy:
14+
@responses.activate
15+
def test_destroy_sends_delete_request(self):
16+
content_guid = "8ce6eaca-60af-4c2f-93a0-f5f3cddf5ee5"
17+
base_url = "http://connect.example/__api__"
18+
endpoint = f"{base_url}/v1/content/{content_guid}/vanity"
19+
mock_delete = responses.delete(endpoint)
20+
21+
session = requests.Session()
22+
url = Url(base_url)
23+
params = ResourceParameters(session, url)
24+
vanity = Vanity(params, content_guid=content_guid)
25+
26+
vanity.destroy()
27+
28+
assert mock_delete.call_count == 1
29+
30+
def test_destroy_without_content_guid_raises_value_error(self):
31+
vanity = Vanity(params=Mock())
32+
with pytest.raises(ValueError):
33+
vanity.destroy()
34+
35+
def test_destroy_with_none_content_guid_raises_value_error(self):
36+
vanity = Vanity(params=Mock(), content_guid=None)
37+
with pytest.raises(ValueError):
38+
vanity.destroy()
39+
40+
@responses.activate
41+
def test_destroy_calls_after_destroy_callback(self):
42+
content_guid = "8ce6eaca-60af-4c2f-93a0-f5f3cddf5ee5"
43+
base_url = "http://connect.example/__api__"
44+
endpoint = f"{base_url}/v1/content/{content_guid}/vanity"
45+
responses.delete(endpoint)
46+
47+
session = requests.Session()
48+
url = Url(base_url)
49+
after_destroy = Mock()
50+
params = ResourceParameters(session, url)
51+
vanity = Vanity(params, after_destroy=after_destroy, content_guid=content_guid)
52+
53+
vanity.destroy()
54+
55+
assert after_destroy.call_count == 1
56+
57+
58+
class TestVanitiesAll:
59+
@responses.activate
60+
def test_all_sends_get_request(self):
61+
base_url = "http://connect.example/__api__"
62+
endpoint = f"{base_url}/v1/vanities"
63+
mock_get = responses.get(endpoint, json=[])
64+
65+
session = requests.Session()
66+
url = Url(base_url)
67+
params = ResourceParameters(session, url)
68+
vanities = Vanities(params)
69+
70+
vanities.all()
71+
72+
assert mock_get.call_count == 1
73+
74+
75+
class TestVanityMixin:
76+
@responses.activate
77+
def test_vanity_getter_returns_vanity(self):
78+
guid = "8ce6eaca-60af-4c2f-93a0-f5f3cddf5ee5"
79+
base_url = "http://connect.example/__api__"
80+
endpoint = f"{base_url}/v1/content/{guid}/vanity"
81+
vanity_data = {"content_guid": guid}
82+
mock_get = responses.get(endpoint, json=vanity_data)
83+
84+
session = requests.Session()
85+
url = Url(base_url)
86+
params = ResourceParameters(session, url)
87+
content = VanityMixin(params, guid=guid)
88+
89+
assert content.vanity == vanity_data
90+
assert mock_get.call_count == 1
91+
92+
@responses.activate
93+
def test_vanity_setter_with_string(self):
94+
guid = "8ce6eaca-60af-4c2f-93a0-f5f3cddf5ee5"
95+
base_url = "http://connect.example/__api__"
96+
endpoint = f"{base_url}/v1/content/{guid}/vanity"
97+
path = "example"
98+
mock_put = responses.put(endpoint, match=[json_params_matcher({"path": path})])
99+
100+
session = requests.Session()
101+
url = Url(base_url)
102+
params = ResourceParameters(session, url)
103+
content = VanityMixin(params, guid=guid)
104+
content.vanity = path
105+
106+
assert mock_put.call_count == 1
107+
108+
@responses.activate
109+
def test_vanity_setter_with_dict(self):
110+
guid = "8ce6eaca-60af-4c2f-93a0-f5f3cddf5ee5"
111+
base_url = "http://connect.example/__api__"
112+
endpoint = f"{base_url}/v1/content/{guid}/vanity"
113+
vanity_attrs = {"path": "example", "locked": True}
114+
mock_put = responses.put(endpoint, match=[json_params_matcher(vanity_attrs)])
115+
116+
session = requests.Session()
117+
url = Url(base_url)
118+
params = ResourceParameters(session, url)
119+
content = VanityMixin(params, guid=guid)
120+
content.vanity = vanity_attrs
121+
122+
assert mock_put.call_count == 1
123+
124+
@responses.activate
125+
def test_vanity_deleter_sends_delete_request(self):
126+
guid = "8ce6eaca-60af-4c2f-93a0-f5f3cddf5ee5"
127+
base_url = "http://connect.example/__api__"
128+
endpoint = f"{base_url}/v1/content/{guid}/vanity"
129+
mock_delete = responses.delete(endpoint)
130+
131+
session = requests.Session()
132+
url = Url(base_url)
133+
params = ResourceParameters(session, url)
134+
content = VanityMixin(params, guid=guid)
135+
content._vanity = Vanity(params, content_guid=guid)
136+
del content.vanity
137+
138+
assert mock_delete.call_count == 1
139+
140+
@responses.activate
141+
def test_set_vanity(self):
142+
guid = "8ce6eaca-60af-4c2f-93a0-f5f3cddf5ee5"
143+
base_url = "http://connect.example/__api__"
144+
endpoint = f"{base_url}/v1/content/{guid}/vanity"
145+
mock_put = responses.put(endpoint)
146+
147+
session = requests.Session()
148+
url = Url(base_url)
149+
params = ResourceParameters(session, url)
150+
content = VanityMixin(params, guid=guid)
151+
content.set_vanity(path="example")
152+
153+
assert mock_put.call_count == 1

0 commit comments

Comments
 (0)