diff --git a/src/posit/connect/bundles.py b/src/posit/connect/bundles.py new file mode 100644 index 00000000..3d876283 --- /dev/null +++ b/src/posit/connect/bundles.py @@ -0,0 +1,129 @@ +from __future__ import annotations + +from typing import List + +from requests.sessions import Session as Session + +from posit.connect.config import Config + +from . import urls + +from .resources import Resources, Resource + + +class BundleMetadata(Resource): + @property + def source(self) -> str | None: + return self.get("source") + + @property + def source_repo(self) -> str | None: + return self.get("source_repo") + + @property + def source_branch(self) -> str | None: + return self.get("source_branch") + + @property + def source_commit(self) -> str | None: + return self.get("source_commit") + + @property + def archive_md5(self) -> str | None: + return self.get("archive_md5") + + @property + def archive_sha1(self) -> str | None: + return self.get("archive_sha1") + + +class Bundle(Resource): + @property + def id(self) -> str: + return self["id"] + + @property + def content_guid(self) -> str: + return self["content_guid"] + + @property + def created_time(self) -> str: + return self["created_time"] + + @property + def cluster_name(self) -> str | None: + return self.get("cluster_name") + + @property + def image_name(self) -> str | None: + return self.get("image_name") + + @property + def r_version(self) -> str | None: + return self.get("r_version") + + @property + def r_environment_management(self) -> bool | None: + return self.get("r_environment_management") + + @property + def py_version(self) -> str | None: + return self.get("py_version") + + @property + def py_environment_management(self) -> bool | None: + return self.get("py_environment_management") + + @property + def quarto_version(self) -> str | None: + return self.get("quarto_version") + + @property + def active(self) -> bool | None: + return self["active"] + + @property + def size(self) -> int | None: + return self["size"] + + @property + def metadata(self) -> BundleMetadata: + return BundleMetadata(self.config, self.session, **self.get("metadata", {})) + + # CRUD Methods + + def delete(self) -> None: + path = f"v1/content/{self.content_guid}/bundles/{self.id}" + url = urls.append_path(self.config.url, path) + self.session.delete(url) + + +class Bundles(Resources): + def __init__(self, config: Config, session: Session, content_guid: str) -> None: + super().__init__(config, session) + self.content_guid = content_guid + + def find(self) -> List[Bundle]: + path = f"v1/content/{self.content_guid}/bundles" + url = urls.append_path(self.config.url, path) + response = self.session.get(url) + results = response.json() + return [ + Bundle( + self.config, + self.session, + **result, + ) + for result in results + ] + + def find_one(self) -> Bundle | None: + bundles = self.find() + return next(iter(bundles), None) + + def get(self, id: str) -> Bundle: + path = f"v1/content/{self.content_guid}/bundles/{id}" + url = urls.append_path(self.config.url, path) + response = self.session.get(url) + result = response.json() + return Bundle(self.config, self.session, **result) diff --git a/src/posit/connect/content.py b/src/posit/connect/content.py index 82d52025..da59f8e7 100644 --- a/src/posit/connect/content.py +++ b/src/posit/connect/content.py @@ -10,6 +10,7 @@ from . import urls from .config import Config +from .bundles import Bundles from .permissions import Permissions from .resources import Resources, Resource @@ -17,6 +18,18 @@ class ContentItem(Resource): """A piece of content.""" + # Relationships + + @property + def bundles(self) -> Bundles: + return Bundles(self.config, self.session, self.guid) + + @property + def permissions(self) -> Permissions: + return Permissions(self.config, self.session, self.guid) + + # Properties + @property def id(self) -> str: return self.get("id") # type: ignore @@ -189,9 +202,7 @@ def dashboard_url(self) -> str: def app_role(self) -> str: return self.get("app_role") # type: ignore - @property - def permissions(self) -> Permissions: - return Permissions(self.config, self.session, content_guid=self.guid) + # CRUD Methods def delete(self) -> None: """Delete the content item.""" diff --git a/src/posit/connect/permissions.py b/src/posit/connect/permissions.py index 48b06dbc..4d02fd1d 100644 --- a/src/posit/connect/permissions.py +++ b/src/posit/connect/permissions.py @@ -71,7 +71,7 @@ def update(self, *args, **kwargs) -> None: class Permissions(Resources): - def __init__(self, config: Config, session: Session, *, content_guid: str) -> None: + def __init__(self, config: Config, session: Session, content_guid: str) -> None: super().__init__(config, session) self.content_guid = content_guid diff --git a/tests/posit/connect/__api__/v1/content/f2f37341-e21d-3d80-c698-a935ad614066/bundles.json b/tests/posit/connect/__api__/v1/content/f2f37341-e21d-3d80-c698-a935ad614066/bundles.json new file mode 100644 index 00000000..5ebb65bc --- /dev/null +++ b/tests/posit/connect/__api__/v1/content/f2f37341-e21d-3d80-c698-a935ad614066/bundles.json @@ -0,0 +1,24 @@ +[ + { + "id": "101", + "content_guid": "f2f37341-e21d-3d80-c698-a935ad614066", + "created_time": "2006-01-02T15:04:05Z07:00", + "cluster_name": "Local", + "image_name": "Local", + "r_version": "3.5.1", + "r_environment_management": true, + "py_version": "3.8.2", + "py_environment_management": true, + "quarto_version": "0.2.22", + "active": false, + "size": 1000000, + "metadata": { + "source": "string", + "source_repo": "string", + "source_branch": "string", + "source_commit": "string", + "archive_md5": "37324238a80595c453c706b22adb83d3", + "archive_sha1": "a2f7d13d87657df599aeeabdb70194d508cfa92f" + } + } +] diff --git a/tests/posit/connect/__api__/v1/content/f2f37341-e21d-3d80-c698-a935ad614066/bundles/101.json b/tests/posit/connect/__api__/v1/content/f2f37341-e21d-3d80-c698-a935ad614066/bundles/101.json new file mode 100644 index 00000000..8b074d82 --- /dev/null +++ b/tests/posit/connect/__api__/v1/content/f2f37341-e21d-3d80-c698-a935ad614066/bundles/101.json @@ -0,0 +1,22 @@ +{ + "id": "101", + "content_guid": "f2f37341-e21d-3d80-c698-a935ad614066", + "created_time": "2006-01-02T15:04:05Z07:00", + "cluster_name": "Local", + "image_name": "Local", + "r_version": "3.5.1", + "r_environment_management": true, + "py_version": "3.8.2", + "py_environment_management": true, + "quarto_version": "0.2.22", + "active": false, + "size": 1000000, + "metadata": { + "source": "string", + "source_repo": "string", + "source_branch": "string", + "source_commit": "string", + "archive_md5": "37324238a80595c453c706b22adb83d3", + "archive_sha1": "a2f7d13d87657df599aeeabdb70194d508cfa92f" + } +} diff --git a/tests/posit/connect/test_bundles.py b/tests/posit/connect/test_bundles.py new file mode 100644 index 00000000..e702487b --- /dev/null +++ b/tests/posit/connect/test_bundles.py @@ -0,0 +1,198 @@ +import requests +import responses + +from posit.connect import Client +from posit.connect.config import Config +from posit.connect.bundles import Bundle + +from .api import load_mock # type: ignore + + +class TestBundleProperties: + def setup_class(cls): + config = Config(api_key="12345", url="https://connect.example/") + session = requests.Session() + cls.bundle = Bundle( + config, + session, + **load_mock( + f"v1/content/f2f37341-e21d-3d80-c698-a935ad614066/bundles/101.json" + ), + ) + + def test_id(self): + assert self.bundle.id == "101" + + def test_content_guid(self): + assert self.bundle.content_guid == "f2f37341-e21d-3d80-c698-a935ad614066" + + def test_created_time(self): + assert self.bundle.created_time == "2006-01-02T15:04:05Z07:00" + + def test_cluster_name(self): + assert self.bundle.cluster_name == "Local" + + def test_image_name(self): + assert self.bundle.image_name == "Local" + + def test_r_version(self): + assert self.bundle.r_version == "3.5.1" + + def test_r_environment_management(self): + assert self.bundle.r_environment_management == True + + def test_py_version(self): + assert self.bundle.py_version == "3.8.2" + + def test_py_environment_management(self): + assert self.bundle.py_environment_management == True + + def test_quarto_version(self): + assert self.bundle.quarto_version == "0.2.22" + + def test_active(self): + assert self.bundle.active == False + + def test_size(self): + assert self.bundle.size == 1000000 + + def test_metadata_source(self): + assert self.bundle.metadata.source == "string" + + def test_metadata_source_repo(self): + assert self.bundle.metadata.source_repo == "string" + + def test_metadata_source_branch(self): + assert self.bundle.metadata.source_branch == "string" + + def test_metadata_source_commit(self): + assert self.bundle.metadata.source_commit == "string" + + def test_metadata_archive_md5(self): + assert self.bundle.metadata.archive_md5 == "37324238a80595c453c706b22adb83d3" + + def test_metadata_archive_sha1(self): + assert ( + self.bundle.metadata.archive_sha1 + == "a2f7d13d87657df599aeeabdb70194d508cfa92f" + ) + + +class TestBundleDelete: + @responses.activate + def test(self): + content_guid = "f2f37341-e21d-3d80-c698-a935ad614066" + bundle_id = "101" + + # behavior + mock_content_get = responses.get( + f"https://connect.example/__api__/v1/content/{content_guid}", + json=load_mock(f"v1/content/{content_guid}.json"), + ) + + mock_bundle_get = responses.get( + f"https://connect.example/__api__/v1/content/{content_guid}/bundles/{bundle_id}", + json=load_mock(f"v1/content/{content_guid}/bundles/{bundle_id}.json"), + ) + + mock_bundle_delete = responses.delete( + f"https://connect.example/__api__/v1/content/{content_guid}/bundles/{bundle_id}", + ) + + # setup + c = Client("12345", "https://connect.example") + bundle = c.content.get(content_guid).bundles.get(bundle_id) + + # invoke + bundle.delete() + + # assert + assert mock_content_get.call_count == 1 + assert mock_bundle_get.call_count == 1 + assert mock_bundle_delete.call_count == 1 + + +class TestBundlesFind: + @responses.activate + def test(self): + content_guid = "f2f37341-e21d-3d80-c698-a935ad614066" + + # behavior + mock_content_get = responses.get( + f"https://connect.example/__api__/v1/content/{content_guid}", + json=load_mock(f"v1/content/{content_guid}.json"), + ) + + mock_bundles_get = responses.get( + f"https://connect.example/__api__/v1/content/{content_guid}/bundles", + json=load_mock(f"v1/content/{content_guid}/bundles.json"), + ) + + # setup + c = Client("12345", "https://connect.example") + + # invoke + bundles = c.content.get(content_guid).bundles.find() + + # assert + assert mock_content_get.call_count == 1 + assert mock_bundles_get.call_count == 1 + assert len(bundles) == 1 + assert bundles[0].id == "101" + + +class TestBundlesFindOne: + @responses.activate + def test(self): + content_guid = "f2f37341-e21d-3d80-c698-a935ad614066" + + # behavior + mock_content_get = responses.get( + f"https://connect.example/__api__/v1/content/{content_guid}", + json=load_mock(f"v1/content/{content_guid}.json"), + ) + + mock_bundles_get = responses.get( + f"https://connect.example/__api__/v1/content/{content_guid}/bundles", + json=load_mock(f"v1/content/{content_guid}/bundles.json"), + ) + + # setup + c = Client("12345", "https://connect.example") + + # invoke + bundle = c.content.get(content_guid).bundles.find_one() + + # assert + assert mock_content_get.call_count == 1 + assert mock_bundles_get.call_count == 1 + assert bundle.id == "101" + + +class TestBundlesGet: + @responses.activate + def test(self): + content_guid = "f2f37341-e21d-3d80-c698-a935ad614066" + bundle_id = "101" + + # behavior + mock_content_get = responses.get( + f"https://connect.example/__api__/v1/content/{content_guid}", + json=load_mock(f"v1/content/{content_guid}.json"), + ) + + mock_bundle_get = responses.get( + f"https://connect.example/__api__/v1/content/{content_guid}/bundles/{bundle_id}", + json=load_mock(f"v1/content/{content_guid}/bundles/{bundle_id}.json"), + ) + + # setup + c = Client("12345", "https://connect.example") + + # invoke + bundle = c.content.get(content_guid).bundles.get(bundle_id) + + # assert + assert mock_content_get.call_count == 1 + assert mock_bundle_get.call_count == 1 + assert bundle.id == bundle_id