Skip to content

Commit c927af0

Browse files
authored
feat: get content for user (#226)
1 parent 6ce4066 commit c927af0

File tree

7 files changed

+182
-14
lines changed

7 files changed

+182
-14
lines changed

integration/tests/posit/connect/test_content.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33

44
class TestContent:
5+
@classmethod
56
def setup_class(cls):
67
cls.client = connect.Client()
78
cls.item = cls.client.content.create(
@@ -10,6 +11,11 @@ def setup_class(cls):
1011
access_type="acl",
1112
)
1213

14+
@classmethod
15+
def teardown_class(cls):
16+
cls.item.delete()
17+
assert cls.client.content.count() == 0
18+
1319
def test_count(self):
1420
assert self.client.content.count() == 1
1521

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from posit import connect
2+
3+
4+
class TestAttributeContent:
5+
"""Checks behavior of the content attribute."""
6+
7+
@classmethod
8+
def setup_class(cls):
9+
cls.client = connect.Client()
10+
cls.user = cls.client.me
11+
cls.user.content.create(
12+
name="Sample",
13+
description="Simple sample content for testing",
14+
access_type="acl",
15+
)
16+
17+
@classmethod
18+
def teardown_class(cls):
19+
assert cls.user.content.find_one().delete() is None
20+
assert cls.user.content.count() == 0
21+
22+
def test_count(self):
23+
assert self.user.content.count() == 1
24+
25+
def test_find(self):
26+
assert self.user.content.find()
27+
28+
def test_find_one(self):
29+
assert self.user.content.find_one()

src/posit/connect/content.py

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
from __future__ import annotations
44

5-
from typing import List, Optional, overload
5+
from collections import defaultdict
6+
from typing import TYPE_CHECKING, List, Optional, overload
67

78

89
from requests import Session
@@ -13,11 +14,10 @@
1314
from .bundles import Bundles
1415
from .permissions import Permissions
1516
from .resources import Resources, Resource
16-
from .users import Users
1717

1818

1919
class ContentItemOwner(Resource):
20-
"""Owner information."""
20+
"""Content item owner resource."""
2121

2222
@property
2323
def guid(self) -> str:
@@ -153,6 +153,8 @@ def owner(self) -> ContentItemOwner:
153153
# It is possible to get a content item that does not contain owner.
154154
# "owner" is an optional additional request param.
155155
# If it's not included, we can retrieve the information by `owner_guid`
156+
from .users import Users
157+
156158
self["owner"] = Users(self.config, self.session).get(
157159
self.owner_guid
158160
)
@@ -442,12 +444,41 @@ def update(self, *args, **kwargs) -> None:
442444

443445

444446
class Content(Resources):
445-
"""Content resource."""
447+
"""Content resource.
448+
449+
Parameters
450+
----------
451+
config : Config
452+
Configuration object.
453+
session : Session
454+
Requests session object.
455+
owner_guid : str, optional
456+
Content item owner identifier. Filters results to those owned by a specific user (the default is None, which implies not filtering results on owner identifier).
457+
"""
446458

447-
def __init__(self, config: Config, session: Session) -> None:
459+
def __init__(
460+
self,
461+
config: Config,
462+
session: Session,
463+
*,
464+
owner_guid: str | None = None,
465+
) -> None:
448466
self.url = urls.append(config.url, "v1/content")
449467
self.config = config
450468
self.session = session
469+
self.owner_guid = owner_guid
470+
471+
def _get_default_params(self) -> dict:
472+
"""Build default parameters for GET requests.
473+
474+
Returns
475+
-------
476+
dict
477+
"""
478+
params = {}
479+
if self.owner_guid:
480+
params["owner_guid"] = self.owner_guid
481+
return params
451482

452483
def count(self) -> int:
453484
"""Count the number of content items.
@@ -456,8 +487,7 @@ def count(self) -> int:
456487
-------
457488
int
458489
"""
459-
results = self.session.get(self.url).json()
460-
return len(results)
490+
return len(self.find())
461491

462492
@overload
463493
def create(
@@ -600,18 +630,19 @@ def find(
600630
-------
601631
List[ContentItem]
602632
"""
603-
params = dict(*args, include=include, **kwargs)
633+
params = self._get_default_params()
634+
params.update(args)
635+
params.update(kwargs)
636+
params["include"] = include
604637
response = self.session.get(self.url, params=params)
605-
results = response.json()
606-
items = (
638+
return [
607639
ContentItem(
608640
config=self.config,
609641
session=self.session,
610642
**result,
611643
)
612-
for result in results
613-
)
614-
return [item for item in items]
644+
for result in response.json()
645+
]
615646

616647
@overload
617648
def find_one(

src/posit/connect/users.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from . import me, urls
99

1010
from .config import Config
11+
from .content import Content
1112
from .paginator import Paginator
1213
from .resources import Resource, Resources
1314

@@ -17,6 +18,8 @@ class User(Resource):
1718
1819
Attributes
1920
----------
21+
content: Content
22+
A content resource scoped to this user.
2023
guid : str
2124
email : str
2225
username : str
@@ -32,6 +35,10 @@ class User(Resource):
3235
Whether the user is locked.
3336
"""
3437

38+
@property
39+
def content(self) -> Content:
40+
return Content(self.config, self.session, owner_guid=self.guid)
41+
3542
@property
3643
def guid(self) -> str:
3744
return self.get("guid") # type: ignore
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
[
2+
{
3+
"guid": "93a3cd6d-5a1b-236c-9808-6045f2a73fb5",
4+
"name": "My-Streamlit-app",
5+
"title": "My Streamlit app",
6+
"description": "",
7+
"access_type": "logged_in",
8+
"connection_timeout": null,
9+
"read_timeout": null,
10+
"init_timeout": null,
11+
"idle_timeout": null,
12+
"max_processes": null,
13+
"min_processes": null,
14+
"max_conns_per_process": null,
15+
"load_factor": null,
16+
"memory_request": null,
17+
"memory_limit": null,
18+
"cpu_request": null,
19+
"cpu_limit": null,
20+
"amd_gpu_limit": null,
21+
"nvidia_gpu_limit": null,
22+
"service_account_name": null,
23+
"default_image_name": null,
24+
"created_time": "2023-02-28T14:00:17Z",
25+
"last_deployed_time": "2023-03-01T14:12:21Z",
26+
"bundle_id": "217640",
27+
"app_mode": "python-streamlit",
28+
"content_category": "",
29+
"parameterized": false,
30+
"cluster_name": "Local",
31+
"image_name": null,
32+
"r_version": null,
33+
"py_version": "3.9.17",
34+
"quarto_version": null,
35+
"r_environment_management": null,
36+
"default_r_environment_management": null,
37+
"py_environment_management": true,
38+
"default_py_environment_management": null,
39+
"run_as": null,
40+
"run_as_current_user": false,
41+
"owner_guid": "20a79ce3-6e87-4522-9faf-be24228800a4",
42+
"content_url": "https://connect.example/content/93a3cd6d-5a1b-236c-9808-6045f2a73fb5/",
43+
"dashboard_url": "https://connect.example/connect/#/apps/93a3cd6d-5a1b-236c-9808-6045f2a73fb5",
44+
"app_role": "viewer",
45+
"id": "8462",
46+
"owner": {
47+
"guid": "20a79ce3-6e87-4522-9faf-be24228800a4",
48+
"username": "carlos12",
49+
"first_name": "Carlos",
50+
"last_name": "User"
51+
}
52+
}
53+
]

tests/posit/connect/test_content.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,7 @@ def test(self):
345345
# invoke
346346
content = client.content.find()
347347

348-
# assert
348+
# assert
349349
assert mock_get.call_count == 1
350350
assert len(content) == 3
351351
assert content[0].name == "team-admin-dashboard"

tests/posit/connect/test_users.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import requests
55
import responses
66

7+
from responses import matchers
8+
79
from posit.connect.client import Client
810
from posit.connect.users import User
911

@@ -92,6 +94,46 @@ def test_locked(self):
9294
assert user.locked is False
9395

9496

97+
class TestUserContent:
98+
"""Check behavior of content attribute."""
99+
100+
@responses.activate
101+
def test_find(self):
102+
"""Check GET /v1/content call includes owner_guid query parameter."""
103+
# behavior
104+
mock_get_user = responses.get(
105+
"https://connect.example/__api__/v1/users/20a79ce3-6e87-4522-9faf-be24228800a4",
106+
json=load_mock(
107+
"v1/users/20a79ce3-6e87-4522-9faf-be24228800a4.json"
108+
),
109+
)
110+
111+
mock_get_content = responses.get(
112+
"https://connect.example/__api__/v1/content",
113+
json=load_mock(
114+
"v1/content?owner_guid=20a79ce3-6e87-4522-9faf-be24228800a4.json"
115+
),
116+
match=[
117+
matchers.query_param_matcher(
118+
{"owner_guid": "20a79ce3-6e87-4522-9faf-be24228800a4"},
119+
strict_match=False,
120+
)
121+
],
122+
)
123+
124+
# setup
125+
c = Client(api_key="12345", url="https://connect.example/")
126+
user = c.users.get("20a79ce3-6e87-4522-9faf-be24228800a4")
127+
128+
# invoke
129+
content = user.content.find()
130+
131+
# assert
132+
assert mock_get_user.call_count == 1
133+
assert mock_get_content.call_count == 1
134+
assert len(content) == 1
135+
136+
95137
class TestUserLock:
96138
@responses.activate
97139
def test_lock(self):

0 commit comments

Comments
 (0)