Skip to content

Commit 1641926

Browse files
committed
feat: add packages attribute to content
1 parent d69dac1 commit 1641926

File tree

4 files changed

+220
-2
lines changed

4 files changed

+220
-2
lines changed

src/posit/connect/content.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from typing import Any, List, Literal, Optional, overload
99

1010
from posit.connect.oauth.associations import ContentItemAssociations
11+
from posit.connect.packages import PackagesMixin
1112

1213
from . import tasks
1314
from .bundles import Bundles
@@ -32,7 +33,7 @@ class ContentItemOwner(Resource):
3233
pass
3334

3435

35-
class ContentItem(Resource):
36+
class ContentItem(PackagesMixin, Resource):
3637
def __getitem__(self, key: Any) -> Any:
3738
v = super().__getitem__(key)
3839
if key == "owner" and isinstance(v, dict):

src/posit/connect/packages.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
from typing import Literal, Optional, Sequence, TypedDict
2+
3+
from typing_extensions import NotRequired, Required, Unpack
4+
5+
from .resources import Resource, ResourceParameters, Resources
6+
7+
8+
class Package(Resource):
9+
"""A package resource."""
10+
11+
class PackageAttributes(TypedDict):
12+
"""Package attributes."""
13+
14+
language: Required[Literal["r", "python"]]
15+
name: Required[str]
16+
version: Required[str]
17+
hash: NotRequired[str]
18+
19+
def __init__(self, params: ResourceParameters, **kwargs: Unpack[PackageAttributes]):
20+
super().__init__(params, **kwargs)
21+
22+
23+
class Packages(Resources, Sequence[Package]):
24+
"""A collection of packages."""
25+
26+
def __init__(self, params, endpoint):
27+
super().__init__(params)
28+
self._endpoint = endpoint
29+
self._packages = []
30+
self.reload()
31+
32+
def __getitem__(self, index):
33+
"""Retrieve an item or slice from the sequence."""
34+
return self._packages[index]
35+
36+
def __len__(self):
37+
"""Return the length of the sequence."""
38+
return len(self._packages)
39+
40+
def __repr__(self):
41+
"""Return the string representation of the sequence."""
42+
return f"Packages({', '.join(map(str, self._packages))})"
43+
44+
def count(self, value):
45+
"""Return the number of occurrences of a value in the sequence."""
46+
return self._packages.count(value)
47+
48+
def index(self, value, start=0, stop=None):
49+
"""Return the index of the first occurrence of a value in the sequence."""
50+
if stop is None:
51+
stop = len(self._packages)
52+
return self._packages.index(value, start, stop)
53+
54+
def reload(self) -> "Packages":
55+
"""Reload packages from the Connect server.
56+
57+
Returns
58+
-------
59+
List[Package]
60+
"""
61+
response = self.params.session.get(self._endpoint)
62+
results = response.json()
63+
packages = [Package(self.params, **result) for result in results]
64+
self._packages = packages
65+
return self
66+
67+
68+
class PackagesMixin(Resource):
69+
"""Mixin class to add a packages to a resource."""
70+
71+
class HasGuid(TypedDict):
72+
"""Has a guid."""
73+
74+
guid: Required[str]
75+
76+
def __init__(self, params: ResourceParameters, **kwargs: Unpack[HasGuid]):
77+
super().__init__(params, **kwargs)
78+
self._guid = kwargs["guid"]
79+
self._packages: Optional[Packages] = None
80+
81+
@property
82+
def packages(self) -> Packages:
83+
"""Get the packages."""
84+
if self._packages:
85+
return self._packages
86+
87+
endpoint = self.params.url + f"v1/content/{self._guid}/packages"
88+
self._packages = Packages(self.params, endpoint)
89+
return self._packages

src/posit/connect/paginator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ class Paginator:
3838
url (str): The URL of the paginated API endpoint.
3939
"""
4040

41-
def __init__(self, session: requests.Session, url: str, params = {}) -> None:
41+
def __init__(self, session: requests.Session, url: str, params={}) -> None:
4242
self.session = session
4343
self.url = url
4444
self.params = params
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import requests
2+
import responses
3+
4+
from posit.connect.packages import PackagesMixin
5+
from posit.connect.resources import ResourceParameters
6+
from posit.connect.urls import Url
7+
8+
9+
class TestPackagesMixin:
10+
def setup_method(self):
11+
self.url = Url("http://connect.example/__api__")
12+
self.endpoint = self.url + "v1/content/1/packages"
13+
self.session = requests.Session()
14+
self.params = ResourceParameters(self.session, self.url)
15+
self.mixin = PackagesMixin(self.params, guid="1")
16+
17+
@responses.activate
18+
def test_packages(self):
19+
# mock
20+
mock_get = responses.get(
21+
self.endpoint,
22+
json=[
23+
{
24+
"language": "python",
25+
"name": "posit-sdk",
26+
"version": "0.5.1.dev3+gd4bba40.d20241016",
27+
}
28+
],
29+
)
30+
31+
# call
32+
packages = self.mixin.packages
33+
34+
# assert
35+
assert mock_get.call_count == 1
36+
assert packages[0] == {
37+
"language": "python",
38+
"name": "posit-sdk",
39+
"version": "0.5.1.dev3+gd4bba40.d20241016",
40+
}
41+
42+
@responses.activate
43+
def test_packages_are_cached(self):
44+
# mock
45+
mock_get = responses.get(
46+
self.endpoint,
47+
json=[
48+
{
49+
"language": "python",
50+
"name": "posit-sdk",
51+
"version": "0.5.1.dev3+gd4bba40.d20241016",
52+
}
53+
],
54+
)
55+
56+
# call attribute twice, the second call should be cached
57+
self.mixin.packages
58+
self.mixin.packages
59+
60+
# assert called once
61+
assert mock_get.call_count == 1
62+
63+
@responses.activate
64+
def test_packages_count(self):
65+
responses.get(
66+
self.endpoint,
67+
json=[
68+
{
69+
"language": "python",
70+
"name": "posit-sdk",
71+
"version": "0.5.1.dev3+gd4bba40.d20241016",
72+
}
73+
],
74+
)
75+
76+
packages = self.mixin.packages
77+
count = packages.count(
78+
{
79+
"language": "python",
80+
"name": "posit-sdk",
81+
"version": "0.5.1.dev3+gd4bba40.d20241016",
82+
}
83+
)
84+
85+
assert count == 1
86+
87+
@responses.activate
88+
def test_packages_index(self):
89+
responses.get(
90+
self.endpoint,
91+
json=[
92+
{
93+
"language": "python",
94+
"name": "posit-sdk",
95+
"version": "0.5.1.dev3+gd4bba40.d20241016",
96+
}
97+
],
98+
)
99+
100+
packages = self.mixin.packages
101+
index = packages.index(
102+
{
103+
"language": "python",
104+
"name": "posit-sdk",
105+
"version": "0.5.1.dev3+gd4bba40.d20241016",
106+
}
107+
)
108+
109+
assert index == 0
110+
111+
@responses.activate
112+
def test_packages_repr(self):
113+
responses.get(
114+
self.endpoint,
115+
json=[
116+
{
117+
"language": "python",
118+
"name": "posit-sdk",
119+
"version": "0.5.1.dev3+gd4bba40.d20241016",
120+
}
121+
],
122+
)
123+
124+
packages = self.mixin.packages
125+
assert (
126+
repr(packages)
127+
== "Packages({'language': 'python', 'name': 'posit-sdk', 'version': '0.5.1.dev3+gd4bba40.d20241016'})"
128+
)

0 commit comments

Comments
 (0)