diff --git a/integration/tests/posit/connect/test_content.py b/integration/tests/posit/connect/test_content.py index d7fa2fbd..8ed83693 100644 --- a/integration/tests/posit/connect/test_content.py +++ b/integration/tests/posit/connect/test_content.py @@ -2,6 +2,7 @@ class TestContent: + @classmethod def setup_class(cls): cls.client = connect.Client() cls.item = cls.client.content.create( @@ -10,6 +11,11 @@ def setup_class(cls): access_type="acl", ) + @classmethod + def teardown_class(cls): + cls.item.delete() + assert cls.client.content.count() == 0 + def test_count(self): assert self.client.content.count() == 1 diff --git a/integration/tests/posit/connect/test_users.py b/integration/tests/posit/connect/test_users.py new file mode 100644 index 00000000..666f4baf --- /dev/null +++ b/integration/tests/posit/connect/test_users.py @@ -0,0 +1,29 @@ +from posit import connect + + +class TestAttributeContent: + """Checks behavior of the content attribute.""" + + @classmethod + def setup_class(cls): + cls.client = connect.Client() + cls.user = cls.client.me + cls.user.content.create( + name="Sample", + description="Simple sample content for testing", + access_type="acl", + ) + + @classmethod + def teardown_class(cls): + assert cls.user.content.find_one().delete() is None + assert cls.user.content.count() == 0 + + def test_count(self): + assert self.user.content.count() == 1 + + def test_find(self): + assert self.user.content.find() + + def test_find_one(self): + assert self.user.content.find_one() diff --git a/src/posit/connect/content.py b/src/posit/connect/content.py index 50c3963b..eed077c9 100644 --- a/src/posit/connect/content.py +++ b/src/posit/connect/content.py @@ -2,7 +2,8 @@ from __future__ import annotations -from typing import List, Optional, overload +from collections import defaultdict +from typing import TYPE_CHECKING, List, Optional, overload from requests import Session @@ -13,11 +14,10 @@ from .bundles import Bundles from .permissions import Permissions from .resources import Resources, Resource -from .users import Users class ContentItemOwner(Resource): - """Owner information.""" + """Content item owner resource.""" @property def guid(self) -> str: @@ -153,6 +153,8 @@ def owner(self) -> ContentItemOwner: # It is possible to get a content item that does not contain owner. # "owner" is an optional additional request param. # If it's not included, we can retrieve the information by `owner_guid` + from .users import Users + self["owner"] = Users(self.config, self.session).get( self.owner_guid ) @@ -442,12 +444,41 @@ def update(self, *args, **kwargs) -> None: class Content(Resources): - """Content resource.""" + """Content resource. + + Parameters + ---------- + config : Config + Configuration object. + session : Session + Requests session object. + owner_guid : str, optional + 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). + """ - def __init__(self, config: Config, session: Session) -> None: + def __init__( + self, + config: Config, + session: Session, + *, + owner_guid: str | None = None, + ) -> None: self.url = urls.append(config.url, "v1/content") self.config = config self.session = session + self.owner_guid = owner_guid + + def _get_default_params(self) -> dict: + """Build default parameters for GET requests. + + Returns + ------- + dict + """ + params = {} + if self.owner_guid: + params["owner_guid"] = self.owner_guid + return params def count(self) -> int: """Count the number of content items. @@ -456,8 +487,7 @@ def count(self) -> int: ------- int """ - results = self.session.get(self.url).json() - return len(results) + return len(self.find()) @overload def create( @@ -600,18 +630,19 @@ def find( ------- List[ContentItem] """ - params = dict(*args, include=include, **kwargs) + params = self._get_default_params() + params.update(args) + params.update(kwargs) + params["include"] = include response = self.session.get(self.url, params=params) - results = response.json() - items = ( + return [ ContentItem( config=self.config, session=self.session, **result, ) - for result in results - ) - return [item for item in items] + for result in response.json() + ] @overload def find_one( diff --git a/src/posit/connect/users.py b/src/posit/connect/users.py index f6c0ea24..6ef9f93d 100644 --- a/src/posit/connect/users.py +++ b/src/posit/connect/users.py @@ -8,6 +8,7 @@ from . import me, urls from .config import Config +from .content import Content from .paginator import Paginator from .resources import Resource, Resources @@ -32,6 +33,10 @@ class User(Resource): Whether the user is locked. """ + @property + def content(self) -> Content: + return Content(self.config, self.session, owner_guid=self.guid) + @property def guid(self) -> str: return self.get("guid") # type: ignore diff --git a/tests/posit/connect/__api__/v1/content?owner_guid=20a79ce3-6e87-4522-9faf-be24228800a4.json b/tests/posit/connect/__api__/v1/content?owner_guid=20a79ce3-6e87-4522-9faf-be24228800a4.json new file mode 100644 index 00000000..c83fdb1d --- /dev/null +++ b/tests/posit/connect/__api__/v1/content?owner_guid=20a79ce3-6e87-4522-9faf-be24228800a4.json @@ -0,0 +1,53 @@ +[ + { + "guid": "93a3cd6d-5a1b-236c-9808-6045f2a73fb5", + "name": "My-Streamlit-app", + "title": "My Streamlit app", + "description": "", + "access_type": "logged_in", + "connection_timeout": null, + "read_timeout": null, + "init_timeout": null, + "idle_timeout": null, + "max_processes": null, + "min_processes": null, + "max_conns_per_process": null, + "load_factor": null, + "memory_request": null, + "memory_limit": null, + "cpu_request": null, + "cpu_limit": null, + "amd_gpu_limit": null, + "nvidia_gpu_limit": null, + "service_account_name": null, + "default_image_name": null, + "created_time": "2023-02-28T14:00:17Z", + "last_deployed_time": "2023-03-01T14:12:21Z", + "bundle_id": "217640", + "app_mode": "python-streamlit", + "content_category": "", + "parameterized": false, + "cluster_name": "Local", + "image_name": null, + "r_version": null, + "py_version": "3.9.17", + "quarto_version": null, + "r_environment_management": null, + "default_r_environment_management": null, + "py_environment_management": true, + "default_py_environment_management": null, + "run_as": null, + "run_as_current_user": false, + "owner_guid": "20a79ce3-6e87-4522-9faf-be24228800a4", + "content_url": "https://connect.example/content/93a3cd6d-5a1b-236c-9808-6045f2a73fb5/", + "dashboard_url": "https://connect.example/connect/#/apps/93a3cd6d-5a1b-236c-9808-6045f2a73fb5", + "app_role": "viewer", + "id": "8462", + "owner": { + "guid": "20a79ce3-6e87-4522-9faf-be24228800a4", + "username": "carlos12", + "first_name": "Carlos", + "last_name": "User" + } + } +] diff --git a/tests/posit/connect/test_content.py b/tests/posit/connect/test_content.py index 4980029f..cd71ddba 100644 --- a/tests/posit/connect/test_content.py +++ b/tests/posit/connect/test_content.py @@ -345,7 +345,7 @@ def test(self): # invoke content = client.content.find() - # assert + # assert assert mock_get.call_count == 1 assert len(content) == 3 assert content[0].name == "team-admin-dashboard" diff --git a/tests/posit/connect/test_users.py b/tests/posit/connect/test_users.py index 65026569..b8a8cd42 100644 --- a/tests/posit/connect/test_users.py +++ b/tests/posit/connect/test_users.py @@ -4,6 +4,8 @@ import requests import responses +from responses import matchers + from posit.connect.client import Client from posit.connect.users import User @@ -92,6 +94,46 @@ def test_locked(self): assert user.locked is False +class TestUserContent: + """Check behavior of content attribute.""" + + @responses.activate + def test_find(self): + """Check GET /v1/content call includes owner_guid query parameter.""" + # behavior + mock_get_user = responses.get( + "https://connect.example/__api__/v1/users/20a79ce3-6e87-4522-9faf-be24228800a4", + json=load_mock( + "v1/users/20a79ce3-6e87-4522-9faf-be24228800a4.json" + ), + ) + + mock_get_content = responses.get( + "https://connect.example/__api__/v1/content", + json=load_mock( + "v1/content?owner_guid=20a79ce3-6e87-4522-9faf-be24228800a4.json" + ), + match=[ + matchers.query_param_matcher( + {"owner_guid": "20a79ce3-6e87-4522-9faf-be24228800a4"}, + strict_match=False, + ) + ], + ) + + # setup + c = Client(api_key="12345", url="https://connect.example/") + user = c.users.get("20a79ce3-6e87-4522-9faf-be24228800a4") + + # invoke + content = user.content.find() + + # assert + assert mock_get_user.call_count == 1 + assert mock_get_content.call_count == 1 + assert len(content) == 1 + + class TestUserLock: @responses.activate def test_lock(self):