From 6ce40663526e815f0a8d14ce3dda6aaf753f93e6 Mon Sep 17 00:00:00 2001 From: Taylor Steinberg Date: Tue, 25 Jun 2024 10:43:56 -0400 Subject: [PATCH] feat: adds groups (#227) --- docs/_quarto.yml | 1 + .../tests/posit/connect/test_groups.py | 23 ++ src/posit/connect/client.py | 12 ++ src/posit/connect/groups.py | 201 ++++++++++++++++++ .../6f300623-1e0c-48e6-a473-ddf630c0c0c3.json | 5 + tests/posit/connect/test_groups.py | 34 +++ 6 files changed, 276 insertions(+) create mode 100644 integration/tests/posit/connect/test_groups.py create mode 100644 src/posit/connect/groups.py create mode 100644 tests/posit/connect/__api__/v1/groups/6f300623-1e0c-48e6-a473-ddf630c0c0c3.json create mode 100644 tests/posit/connect/test_groups.py diff --git a/docs/_quarto.yml b/docs/_quarto.yml index 48d41707..4983a130 100644 --- a/docs/_quarto.yml +++ b/docs/_quarto.yml @@ -100,6 +100,7 @@ quartodoc: contents: - connect.bundles - connect.content + - connect.groups - connect.permissions - connect.tasks - connect.users diff --git a/integration/tests/posit/connect/test_groups.py b/integration/tests/posit/connect/test_groups.py new file mode 100644 index 00000000..ba2c0e94 --- /dev/null +++ b/integration/tests/posit/connect/test_groups.py @@ -0,0 +1,23 @@ +from posit import connect + + +class TestGroups: + def setup_class(cls): + cls.client = connect.Client() + cls.item = cls.client.groups.create(name="Friends") + + def teardown_class(cls): + cls.item.delete() + assert cls.client.groups.count() == 0 + + def test_count(self): + assert self.client.groups.count() == 1 + + def test_get(self): + assert self.client.groups.get(self.item.guid) + + def test_find(self): + assert self.client.groups.find() == [self.item] + + def test_find_one(self): + assert self.client.groups.find_one() == self.item diff --git a/src/posit/connect/client.py b/src/posit/connect/client.py index 16fd4e88..df0c1f15 100644 --- a/src/posit/connect/client.py +++ b/src/posit/connect/client.py @@ -14,6 +14,7 @@ from .metrics import Metrics from .tasks import Tasks from .users import User, Users +from .groups import Groups class Client: @@ -94,6 +95,17 @@ def oauth(self) -> OAuthIntegration: """ return OAuthIntegration(config=self.config, session=self.session) + @property + def groups(self) -> Groups: + """The groups resource interface. + + Returns + ------- + Groups + The groups resource interface. + """ + return Groups(self.config, self.session) + @property def tasks(self) -> Tasks: """ diff --git a/src/posit/connect/groups.py b/src/posit/connect/groups.py new file mode 100644 index 00000000..dc47b552 --- /dev/null +++ b/src/posit/connect/groups.py @@ -0,0 +1,201 @@ +"""Group resources.""" + +from __future__ import annotations +from typing import List, overload + +import requests + +from . import me, urls + +from .config import Config +from .paginator import Paginator +from .resources import Resource, Resources + + +class Group(Resource): + """Group resource. + + Attributes + ---------- + guid : str + name: str + owner_guid: str + """ + + @property + def guid(self) -> str: + return self.get("guid") # type: ignore + + @property + def name(self) -> str: + return self.get("name") # type: ignore + + @property + def owner_guid(self) -> str: + return self.get("owner_guid") # type: ignore + + # CRUD Methods + + def delete(self) -> None: + """Delete the group.""" + path = f"v1/groups/{self.guid}" + url = urls.append(self.config.url, path) + self.session.delete(url) + + +class Groups(Resources): + """Groups resource.""" + + def __init__(self, config: Config, session: requests.Session) -> None: + self.config = config + self.session = session + + @overload + def create(self, name: str, unique_id: str | None) -> Group: + """Create a group. + + Parameters + ---------- + name: str + unique_id: str | None + + Returns + ------- + Group + """ + ... + + @overload + def create(self, *args, **kwargs) -> Group: + """Create a group. + + Returns + ------- + Group + """ + ... + + def create(self, *args, **kwargs) -> Group: + """Create a group. + + Parameters + ---------- + name: str + unique_id: str | None + + Returns + ------- + Group + """ + ... + body = dict(*args, **kwargs) + path = "v1/groups" + url = urls.append(self.config.url, path) + response = self.session.post(url, json=body) + return Group(self.config, self.session, **response.json()) + + @overload + def find( + self, + prefix: str = ..., + ) -> List[Group]: ... + + @overload + def find(self, *args, **kwargs) -> List[Group]: ... + + def find(self, *args, **kwargs): + """Find groups. + + Parameters + ---------- + prefix: str + Filter by group name prefix. Casing is ignored. + + Returns + ------- + List[Group] + """ + params = dict(*args, **kwargs) + path = "v1/groups" + url = urls.append(self.config.url, path) + paginator = Paginator(self.session, url, params=params) + results = paginator.fetch_results() + return [ + Group( + config=self.config, + session=self.session, + **result, + ) + for result in results + ] + + @overload + def find_one( + self, + prefix: str = ..., + ) -> Group | None: ... + + @overload + def find_one(self, *args, **kwargs) -> Group | None: ... + + def find_one(self, *args, **kwargs) -> Group | None: + """Find one group. + + Parameters + ---------- + prefix: str + Filter by group name prefix. Casing is ignored. + + Returns + ------- + Group | None + """ + params = dict(*args, **kwargs) + path = "v1/groups" + url = urls.append(self.config.url, path) + paginator = Paginator(self.session, url, params=params) + pages = paginator.fetch_pages() + results = (result for page in pages for result in page.results) + groups = ( + Group( + config=self.config, + session=self.session, + **result, + ) + for result in results + ) + return next(groups, None) + + def get(self, guid: str) -> Group: + """Get group. + + Parameters + ---------- + guid : str + + Returns + ------- + Group + """ + url = urls.append(self.config.url, f"v1/groups/{guid}") + response = self.session.get(url) + return Group( + config=self.config, + session=self.session, + **response.json(), + ) + + def count(self) -> int: + """Count the number of groups. + + Returns + ------- + int + """ + path = "v1/groups" + url = urls.append(self.config.url, path) + response: requests.Response = self.session.get( + url, params={"page_size": 1} + ) + result: dict = response.json() + return result["total"] diff --git a/tests/posit/connect/__api__/v1/groups/6f300623-1e0c-48e6-a473-ddf630c0c0c3.json b/tests/posit/connect/__api__/v1/groups/6f300623-1e0c-48e6-a473-ddf630c0c0c3.json new file mode 100644 index 00000000..bcd0c7a2 --- /dev/null +++ b/tests/posit/connect/__api__/v1/groups/6f300623-1e0c-48e6-a473-ddf630c0c0c3.json @@ -0,0 +1,5 @@ +{ + "guid": "6f300623-1e0c-48e6-a473-ddf630c0c0c3", + "name": "Friends", + "owner_guid": "20a79ce3-6e87-4522-9faf-be24228800a4" + } diff --git a/tests/posit/connect/test_groups.py b/tests/posit/connect/test_groups.py new file mode 100644 index 00000000..550f8798 --- /dev/null +++ b/tests/posit/connect/test_groups.py @@ -0,0 +1,34 @@ +from unittest.mock import Mock + +import pytest +import requests +import responses + + +from posit.connect.client import Client +from posit.connect.config import Config +from posit.connect.groups import Group + +from .api import load_mock # type: ignore + +session = Mock() +url = Mock() + + +class TestGroupAttributes: + @classmethod + def setup_class(cls): + guid = "6f300623-1e0c-48e6-a473-ddf630c0c0c3" + config = Config(api_key="12345", url="https://connect.example.com/") + session = requests.Session() + fake_item = load_mock(f"v1/groups/{guid}.json") + cls.item = Group(config, session, **fake_item) + + def test_guid(self): + assert self.item.guid == "6f300623-1e0c-48e6-a473-ddf630c0c0c3" + + def test_name(self): + assert self.item.name == "Friends" + + def test_owner_guid(self): + assert self.item.owner_guid == "20a79ce3-6e87-4522-9faf-be24228800a4"