From fb8030f43f3291fd8abfcbc3919820b65217fb46 Mon Sep 17 00:00:00 2001 From: tdstein Date: Wed, 24 Jul 2024 14:54:17 -0400 Subject: [PATCH] refactor: resource parameter object --- src/posit/connect/bundles.py | 35 +++++------ src/posit/connect/client.py | 33 +++++----- src/posit/connect/content.py | 53 ++++++++-------- src/posit/connect/env.py | 21 +++---- src/posit/connect/groups.py | 28 ++++----- src/posit/connect/me.py | 11 ++-- src/posit/connect/metrics/__init__.py | 2 +- src/posit/connect/metrics/shiny_usage.py | 10 ++-- src/posit/connect/metrics/usage.py | 13 ++-- src/posit/connect/metrics/visits.py | 10 ++-- src/posit/connect/permissions.py | 28 ++++----- src/posit/connect/resources.py | 35 ++++++++--- src/posit/connect/tasks.py | 6 +- src/posit/connect/users.py | 43 ++++++------- src/posit/connect/variants.py | 21 +++---- .../posit/connect/metrics/test_shiny_usage.py | 21 ++++--- tests/posit/connect/metrics/test_usage.py | 8 +-- tests/posit/connect/metrics/test_visits.py | 21 ++++--- tests/posit/connect/test_bundles.py | 7 +-- tests/posit/connect/test_content.py | 25 ++++---- tests/posit/connect/test_groups.py | 7 +-- tests/posit/connect/test_permissions.py | 60 +++++++++++-------- tests/posit/connect/test_resources.py | 15 ++--- tests/posit/connect/test_tasks.py | 5 +- tests/posit/connect/test_users.py | 45 +++++++------- 25 files changed, 269 insertions(+), 294 deletions(-) diff --git a/src/posit/connect/bundles.py b/src/posit/connect/bundles.py index 11fa8958..fa6eb194 100644 --- a/src/posit/connect/bundles.py +++ b/src/posit/connect/bundles.py @@ -5,9 +5,7 @@ import io from typing import List -import requests - -from . import config, resources, tasks +from . import resources, tasks class BundleMetadata(resources.Resource): @@ -137,14 +135,12 @@ def size(self) -> int | None: @property def metadata(self) -> BundleMetadata: - return BundleMetadata( - self.config, self.session, **self.get("metadata", {}) - ) + return BundleMetadata(self.params, **self.get("metadata", {})) def delete(self) -> None: """Delete the bundle.""" path = f"v1/content/{self.content_guid}/bundles/{self.id}" - url = self.config.url + path + url = self.url + path self.session.delete(url) def deploy(self) -> tasks.Task: @@ -164,10 +160,10 @@ def deploy(self) -> tasks.Task: None """ path = f"v1/content/{self.content_guid}/deploy" - url = self.config.url + path + url = self.url + path response = self.session.post(url, json={"bundle_id": self.id}) result = response.json() - ts = tasks.Tasks(self.config, self.session) + ts = tasks.Tasks(self.params) return ts.get(result["task_id"]) def download(self, output: io.BufferedWriter | str) -> None: @@ -202,7 +198,7 @@ def download(self, output: io.BufferedWriter | str) -> None: ) path = f"v1/content/{self.content_guid}/bundles/{self.id}/download" - url = self.config.url + path + url = self.url + path response = self.session.get(url, stream=True) if isinstance(output, io.BufferedWriter): for chunk in response.iter_content(): @@ -233,11 +229,10 @@ class Bundles(resources.Resources): def __init__( self, - config: config.Config, - session: requests.Session, + params: resources.ResourceParameters, content_guid: str, ) -> None: - super().__init__(config, session) + super().__init__(params) self.content_guid = content_guid def create(self, input: io.BufferedReader | bytes | str) -> Bundle: @@ -289,10 +284,10 @@ def create(self, input: io.BufferedReader | bytes | str) -> Bundle: ) path = f"v1/content/{self.content_guid}/bundles" - url = self.config.url + path + url = self.url + path response = self.session.post(url, data=data) result = response.json() - return Bundle(self.config, self.session, **result) + return Bundle(self.params, **result) def find(self) -> List[Bundle]: """Find all bundles. @@ -303,12 +298,10 @@ def find(self) -> List[Bundle]: List of all found bundles. """ path = f"v1/content/{self.content_guid}/bundles" - url = self.config.url + path + url = self.url + path response = self.session.get(url) results = response.json() - return [ - Bundle(self.config, self.session, **result) for result in results - ] + return [Bundle(self.params, **result) for result in results] def find_one(self) -> Bundle | None: """Find a bundle. @@ -335,7 +328,7 @@ def get(self, id: str) -> Bundle: The bundle with the specified ID. """ path = f"v1/content/{self.content_guid}/bundles/{id}" - url = self.config.url + path + url = self.url + path response = self.session.get(url) result = response.json() - return Bundle(self.config, self.session, **result) + return Bundle(self.params, **result) diff --git a/src/posit/connect/client.py b/src/posit/connect/client.py index b36cc823..77001891 100644 --- a/src/posit/connect/client.py +++ b/src/posit/connect/client.py @@ -6,6 +6,8 @@ from requests import Response, Session +from posit.connect.resources import ResourceParameters + from . import hooks, me from .auth import Auth from .config import Config @@ -155,12 +157,13 @@ def __init__(self, *args, **kwargs) -> None: if "url" in kwargs and isinstance(kwargs["url"], str): url = kwargs["url"] - self.config = Config(api_key=api_key, url=url) + self.cfg = Config(api_key=api_key, url=url) session = Session() - session.auth = Auth(config=self.config) + session.auth = Auth(config=self.cfg) session.hooks["response"].append(hooks.check_for_deprecation_header) session.hooks["response"].append(hooks.handle_errors) self.session = session + self.resource_params = ResourceParameters(session, self.cfg.url) @property def version(self) -> str: @@ -184,7 +187,7 @@ def me(self) -> User: User The currently authenticated user. """ - return me.get(self.config, self.session) + return me.get(self.resource_params) @property def oauth(self) -> OAuthIntegration: @@ -196,7 +199,7 @@ def oauth(self) -> OAuthIntegration: OAuthIntegration The OAuth integration instance. """ - return OAuthIntegration(config=self.config, session=self.session) + return OAuthIntegration(self.cfg, self.session) @property def groups(self) -> Groups: @@ -207,7 +210,7 @@ def groups(self) -> Groups: Groups The groups resource interface. """ - return Groups(self.config, self.session) + return Groups(self.resource_params) @property def tasks(self) -> Tasks: @@ -219,7 +222,7 @@ def tasks(self) -> Tasks: tasks.Tasks The tasks resource instance. """ - return Tasks(self.config, self.session) + return Tasks(self.resource_params) @property def users(self) -> Users: @@ -231,7 +234,7 @@ def users(self) -> Users: Users The users resource instance. """ - return Users(config=self.config, session=self.session) + return Users(self.resource_params) @property def content(self) -> Content: @@ -243,7 +246,7 @@ def content(self) -> Content: Content The content resource instance. """ - return Content(config=self.config, session=self.session) + return Content(self.resource_params) @property def metrics(self) -> Metrics: @@ -271,7 +274,7 @@ def metrics(self) -> Metrics: >>> len(events) 24 """ - return Metrics(self.config, self.session) + return Metrics(self.resource_params) def __del__(self): """Close the session when the Client instance is deleted.""" @@ -318,7 +321,7 @@ def request(self, method: str, path: str, **kwargs) -> Response: Response A [](`requests.Response`) object. """ - url = self.config.url + path + url = self.cfg.url + path return self.session.request(method, url, **kwargs) def get(self, path: str, **kwargs) -> Response: @@ -339,7 +342,7 @@ def get(self, path: str, **kwargs) -> Response: Response A [](`requests.Response`) object. """ - url = self.config.url + path + url = self.cfg.url + path return self.session.get(url, **kwargs) def post(self, path: str, **kwargs) -> Response: @@ -360,7 +363,7 @@ def post(self, path: str, **kwargs) -> Response: Response A [](`requests.Response`) object. """ - url = self.config.url + path + url = self.cfg.url + path return self.session.post(url, **kwargs) def put(self, path: str, **kwargs) -> Response: @@ -381,7 +384,7 @@ def put(self, path: str, **kwargs) -> Response: Response A [](`requests.Response`) object. """ - url = self.config.url + path + url = self.cfg.url + path return self.session.put(url, **kwargs) def patch(self, path: str, **kwargs) -> Response: @@ -402,7 +405,7 @@ def patch(self, path: str, **kwargs) -> Response: Response A [](`requests.Response`) object. """ - url = self.config.url + path + url = self.cfg.url + path return self.session.patch(url, **kwargs) def delete(self, path: str, **kwargs) -> Response: @@ -423,5 +426,5 @@ def delete(self, path: str, **kwargs) -> Response: Response A [](`requests.Response`) object. """ - url = self.config.url + path + url = self.cfg.url + path return self.session.delete(url, **kwargs) diff --git a/src/posit/connect/content.py b/src/posit/connect/content.py index 42b704fe..bb780fc0 100644 --- a/src/posit/connect/content.py +++ b/src/posit/connect/content.py @@ -2,18 +2,16 @@ from __future__ import annotations +import posixpath import secrets from posixpath import dirname from typing import List, Optional, overload -from requests import Session - from . import tasks from .bundles import Bundles -from .config import Config from .env import EnvVars from .permissions import Permissions -from .resources import Resource, Resources +from .resources import Resource, ResourceParameters, Resources from .tasks import Task from .variants import Variants @@ -144,7 +142,7 @@ class ContentItem(Resource): def delete(self) -> None: """Delete the content item.""" path = f"v1/content/{self.guid}" - url = self.config.url + path + url = self.url + path self.session.delete(url) def deploy(self) -> tasks.Task: @@ -164,10 +162,10 @@ def deploy(self) -> tasks.Task: None """ path = f"v1/content/{self.guid}/deploy" - url = self.config.url + path + url = self.url + path response = self.session.post(url, json={"bundle_id": None}) result = response.json() - ts = tasks.Tasks(self.config, self.session) + ts = tasks.Tasks(self.params) return ts.get(result["task_id"]) def render(self) -> Task: @@ -237,7 +235,7 @@ def restart(self) -> None: self.environment_variables.create(key, random_hash) self.environment_variables.delete(key) # GET via the base Connect URL to force create a new worker thread. - url = dirname(self.config.url) + f"/content/{self.guid}" + url = posixpath.join(dirname(self.url), f"content/{self.guid}") self.session.get(url) return None else: @@ -314,7 +312,7 @@ def update(self, *args, **kwargs) -> None: def update(self, *args, **kwargs) -> None: """Update the content item.""" body = dict(*args, **kwargs) - url = self.config.url + f"v1/content/{self.guid}" + url = self.url + f"v1/content/{self.guid}" response = self.session.patch(url, json=body) super().update(**response.json()) @@ -322,15 +320,15 @@ def update(self, *args, **kwargs) -> None: @property def bundles(self) -> Bundles: - return Bundles(self.config, self.session, self.guid) + return Bundles(self.params, self.guid) @property def environment_variables(self) -> EnvVars: - return EnvVars(self.config, self.session, self.guid) + return EnvVars(self.params, self.guid) @property def permissions(self) -> Permissions: - return Permissions(self.config, self.session, self.guid) + return Permissions(self.params, self.guid) @property def owner(self) -> ContentItemOwner: @@ -340,14 +338,12 @@ def owner(self) -> ContentItemOwner: # 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 - ) - return ContentItemOwner(self.config, self.session, **self["owner"]) + self["owner"] = Users(self.params).get(self.owner_guid) + return ContentItemOwner(self.params, **self["owner"]) @property def _variants(self) -> Variants: - return Variants(self.config, self.session, self.guid) + return Variants(self.params, self.guid) # Properties @@ -543,14 +539,11 @@ class Content(Resources): def __init__( self, - config: Config, - session: Session, + params: ResourceParameters, *, owner_guid: str | None = None, ) -> None: - self.url = config.url + "v1/content" - self.config = config - self.session = session + super().__init__(params) self.owner_guid = owner_guid def _get_default_params(self) -> dict: @@ -656,9 +649,9 @@ def create(self, *args, **kwargs) -> ContentItem: """ body = dict(*args, **kwargs) path = "v1/content" - url = self.config.url + path + url = self.url + path response = self.session.post(url, json=body) - return ContentItem(self.config, self.session, **response.json()) + return ContentItem(self.params, **response.json()) @overload def find( @@ -719,11 +712,12 @@ def find( params.update(args) params.update(kwargs) params["include"] = include - response = self.session.get(self.url, params=params) + path = "v1/content" + url = self.url + path + response = self.session.get(url, params=params) return [ ContentItem( - config=self.config, - session=self.session, + self.params, **result, ) for result in response.json() @@ -798,6 +792,7 @@ def get(self, guid: str) -> ContentItem: ------- ContentItem """ - url = self.url + guid + path = f"v1/content/{guid}" + url = self.url + path response = self.session.get(url) - return ContentItem(self.config, self.session, **response.json()) + return ContentItem(self.params, **response.json()) diff --git a/src/posit/connect/env.py b/src/posit/connect/env.py index c6eedd6b..073915e3 100644 --- a/src/posit/connect/env.py +++ b/src/posit/connect/env.py @@ -2,17 +2,12 @@ from typing import Any, Iterator, List, Mapping, MutableMapping, Optional -from requests import Session - -from .config import Config -from .resources import Resources +from .resources import ResourceParameters, Resources class EnvVars(Resources, MutableMapping[str, Optional[str]]): - def __init__( - self, config: Config, session: Session, content_guid: str - ) -> None: - super().__init__(config, session) + def __init__(self, params: ResourceParameters, content_guid: str) -> None: + super().__init__(params) self.content_guid = content_guid def __delitem__(self, key: str, /) -> None: @@ -25,7 +20,7 @@ def __delitem__(self, key: str, /) -> None: Examples -------- - >>> vars = EnvVars(config, session, content_guid) + >>> vars = EnvVars(params, content_guid) >>> del vars["DATABASE_URL"] """ self.update({key: None}) @@ -55,7 +50,7 @@ def __setitem__(self, key: str, value: Optional[str], /) -> None: Examples -------- - >>> vars = EnvVars(config, session, content_guid) + >>> vars = EnvVars(params, content_guid) >>> vars["DATABASE_URL"] = ( ... "postgres://user:password@localhost:5432/database" ... ) @@ -70,7 +65,7 @@ def clear(self) -> None: >>> clear() """ path = f"v1/content/{self.content_guid}/environment" - url = self.config.url + path + url = self.url + path self.session.put(url, json=[]) def create(self, key: str, value: str, /) -> None: @@ -128,7 +123,7 @@ def find(self) -> List[str]: ['DATABASE_URL'] """ path = f"v1/content/{self.content_guid}/environment" - url = self.config.url + path + url = self.url + path response = self.session.get(url) return response.json() @@ -203,5 +198,5 @@ def update(self, other=(), /, **kwargs: Optional[str]): body = [{"name": key, "value": value} for key, value in d.items()] path = f"v1/content/{self.content_guid}/environment" - url = self.config.url + path + url = self.url + path self.session.patch(url, json=body) diff --git a/src/posit/connect/groups.py b/src/posit/connect/groups.py index 4feeeaee..631d8bdc 100644 --- a/src/posit/connect/groups.py +++ b/src/posit/connect/groups.py @@ -6,7 +6,6 @@ import requests -from .config import Config from .paginator import Paginator from .resources import Resource, Resources @@ -38,17 +37,13 @@ def owner_guid(self) -> str: def delete(self) -> None: """Delete the group.""" path = f"v1/groups/{self.guid}" - url = self.config.url + path + url = self.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. @@ -89,9 +84,9 @@ def create(self, *args, **kwargs) -> Group: ... body = dict(*args, **kwargs) path = "v1/groups" - url = self.config.url + path + url = self.url + path response = self.session.post(url, json=body) - return Group(self.config, self.session, **response.json()) + return Group(self.params, **response.json()) @overload def find( @@ -116,13 +111,12 @@ def find(self, *args, **kwargs): """ params = dict(*args, **kwargs) path = "v1/groups" - url = self.config.url + path + url = self.url + path paginator = Paginator(self.session, url, params=params) results = paginator.fetch_results() return [ Group( - config=self.config, - session=self.session, + self.params, **result, ) for result in results @@ -151,14 +145,13 @@ def find_one(self, *args, **kwargs) -> Group | None: """ params = dict(*args, **kwargs) path = "v1/groups" - url = self.config.url + path + url = self.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, + self.params, **result, ) for result in results @@ -176,11 +169,10 @@ def get(self, guid: str) -> Group: ------- Group """ - url = self.config.url + f"v1/groups/{guid}" + url = self.url + f"v1/groups/{guid}" response = self.session.get(url) return Group( - config=self.config, - session=self.session, + self.params, **response.json(), ) @@ -192,7 +184,7 @@ def count(self) -> int: int """ path = "v1/groups" - url = self.config.url + path + url = self.url + path response: requests.Response = self.session.get( url, params={"page_size": 1} ) diff --git a/src/posit/connect/me.py b/src/posit/connect/me.py index 0349c115..a32d7c63 100644 --- a/src/posit/connect/me.py +++ b/src/posit/connect/me.py @@ -1,10 +1,9 @@ -import requests +from posit.connect.resources import ResourceParameters -from .config import Config from .users import User -def get(config: Config, session: requests.Session) -> User: +def get(params: ResourceParameters) -> User: """ Gets the current user. @@ -16,6 +15,6 @@ def get(config: Config, session: requests.Session) -> User: ------- User: The current user. """ - url = config.url + "v1/user" - response = session.get(url) - return User(config, session, **response.json()) + url = params.url + "v1/user" + response = params.session.get(url) + return User(params, **response.json()) diff --git a/src/posit/connect/metrics/__init__.py b/src/posit/connect/metrics/__init__.py index 0518826a..a205c517 100644 --- a/src/posit/connect/metrics/__init__.py +++ b/src/posit/connect/metrics/__init__.py @@ -15,4 +15,4 @@ class Metrics(resources.Resources): @property def usage(self) -> Usage: - return Usage(self.config, self.session) + return Usage(self.params) diff --git a/src/posit/connect/metrics/shiny_usage.py b/src/posit/connect/metrics/shiny_usage.py index 12a71079..296250b2 100644 --- a/src/posit/connect/metrics/shiny_usage.py +++ b/src/posit/connect/metrics/shiny_usage.py @@ -107,13 +107,12 @@ def find(self, *args, **kwargs) -> List[ShinyUsageEvent]: params = rename_params(params) path = "/v1/instrumentation/shiny/usage" - url = self.config.url + path + url = self.url + path paginator = CursorPaginator(self.session, url, params=params) results = paginator.fetch_results() return [ ShinyUsageEvent( - config=self.config, - session=self.session, + self.params, **result, ) for result in results @@ -166,14 +165,13 @@ def find_one(self, *args, **kwargs) -> ShinyUsageEvent | None: params = dict(*args, **kwargs) params = rename_params(params) path = "/v1/instrumentation/shiny/usage" - url = self.config.url + path + url = self.url + path paginator = CursorPaginator(self.session, url, params=params) pages = paginator.fetch_pages() results = (result for page in pages for result in page.results) visits = ( ShinyUsageEvent( - config=self.config, - session=self.session, + self.params, **result, ) for result in results diff --git a/src/posit/connect/metrics/usage.py b/src/posit/connect/metrics/usage.py index a59a6245..7442e4f4 100644 --- a/src/posit/connect/metrics/usage.py +++ b/src/posit/connect/metrics/usage.py @@ -26,8 +26,7 @@ def from_event( @staticmethod def from_visit_event(event: visits.VisitEvent) -> UsageEvent: return UsageEvent( - event.config, - event.session, + event.params, content_guid=event.content_guid, user_guid=event.user_guid, variant_key=event.variant_key, @@ -44,8 +43,7 @@ def from_shiny_usage_event( event: shiny_usage.ShinyUsageEvent, ) -> UsageEvent: return UsageEvent( - event.config, - event.session, + event.params, content_guid=event.content_guid, user_guid=event.user_guid, variant_key=None, @@ -57,9 +55,6 @@ def from_shiny_usage_event( path=None, ) - def __init__(self, config: resources.Config, session: Session, **kwargs): - super().__init__(config, session, **kwargs) - @property def content_guid(self) -> str: """The associated unique content identifier. @@ -203,7 +198,7 @@ def find(self, *args, **kwargs) -> List[UsageEvent]: events = [] finders = (visits.Visits, shiny_usage.ShinyUsage) for finder in finders: - instance = finder(self.config, self.session) + instance = finder(self.params) events.extend( [ UsageEvent.from_event(event) @@ -258,7 +253,7 @@ def find_one(self, *args, **kwargs) -> UsageEvent | None: """ finders = (visits.Visits, shiny_usage.ShinyUsage) for finder in finders: - instance = finder(self.config, self.session) + instance = finder(self.params) event = instance.find_one(*args, **kwargs) # type: ignore[attr-defined] if event: return UsageEvent.from_event(event) diff --git a/src/posit/connect/metrics/visits.py b/src/posit/connect/metrics/visits.py index fb249280..87878782 100644 --- a/src/posit/connect/metrics/visits.py +++ b/src/posit/connect/metrics/visits.py @@ -139,13 +139,12 @@ def find(self, *args, **kwargs) -> List[VisitEvent]: params = rename_params(params) path = "/v1/instrumentation/content/visits" - url = self.config.url + path + url = self.url + path paginator = CursorPaginator(self.session, url, params=params) results = paginator.fetch_results() return [ VisitEvent( - config=self.config, - session=self.session, + self.params, **result, ) for result in results @@ -198,14 +197,13 @@ def find_one(self, *args, **kwargs) -> VisitEvent | None: params = dict(*args, **kwargs) params = rename_params(params) path = "/v1/instrumentation/content/visits" - url = self.config.url + path + url = self.url + path paginator = CursorPaginator(self.session, url, params=params) pages = paginator.fetch_pages() results = (result for page in pages for result in page.results) visits = ( VisitEvent( - config=self.config, - session=self.session, + self.params, **result, ) for result in results diff --git a/src/posit/connect/permissions.py b/src/posit/connect/permissions.py index 16abcdc5..118d7ba4 100644 --- a/src/posit/connect/permissions.py +++ b/src/posit/connect/permissions.py @@ -6,8 +6,7 @@ from requests.sessions import Session as Session -from .config import Config -from .resources import Resource, Resources +from .resources import Resource, ResourceParameters, Resources class Permission(Resource): @@ -34,7 +33,7 @@ def role(self) -> str: def delete(self) -> None: """Delete the permission.""" path = f"v1/content/{self.content_guid}/permissions/{self.id}" - url = self.config.url + path + url = self.url + path self.session.delete(url) @overload @@ -62,7 +61,7 @@ def update(self, *args, **kwargs) -> None: } body.update(*args, **kwargs) path = f"v1/content/{self.content_guid}/permissions/{self.id}" - url = self.config.url + path + url = self.url + path response = self.session.put( url, json=body, @@ -71,10 +70,8 @@ def update(self, *args, **kwargs) -> None: class Permissions(Resources): - def __init__( - self, config: Config, session: Session, content_guid: str - ) -> None: - super().__init__(config, session) + def __init__(self, params: ResourceParameters, content_guid: str) -> None: + super().__init__(params) self.content_guid = content_guid def count(self) -> int: @@ -124,9 +121,9 @@ def create(self, *args, **kwargs) -> Permission: ... body = dict(*args, **kwargs) path = f"v1/content/{self.content_guid}/permissions" - url = self.config.url + path + url = self.url + path response = self.session.post(url, json=body) - return Permission(self.config, self.session, **response.json()) + return Permission(self.params, **response.json()) def find(self, *args, **kwargs) -> List[Permission]: """Find permissions. @@ -137,13 +134,10 @@ def find(self, *args, **kwargs) -> List[Permission]: """ body = dict(*args, **kwargs) path = f"v1/content/{self.content_guid}/permissions" - url = self.config.url + path + url = self.url + path response = self.session.get(url, json=body) results = response.json() - return [ - Permission(self.config, self.session, **result) - for result in results - ] + return [Permission(self.params, **result) for result in results] def find_one(self, *args, **kwargs) -> Permission | None: """Find a permission. @@ -168,6 +162,6 @@ def get(self, id: str) -> Permission: Permission """ path = f"v1/content/{self.content_guid}/permissions/{id}" - url = self.config.url + path + url = self.url + path response = self.session.get(url) - return Permission(self.config, self.session, **response.json()) + return Permission(self.params, **response.json()) diff --git a/src/posit/connect/resources.py b/src/posit/connect/resources.py index b3daf85b..98328379 100644 --- a/src/posit/connect/resources.py +++ b/src/posit/connect/resources.py @@ -1,24 +1,43 @@ from abc import ABC +from dataclasses import dataclass from typing import Any import requests -from .config import Config +from .urls import Url + + +@dataclass(frozen=True) +class ResourceParameters: + """Shared parameter object for resources. + + Attributes + ---------- + session: requests.Session + url: str + The Connect API base URL (e.g., https://connect.example.com/__api__) + """ + + session: requests.Session + url: Url class Resource(ABC, dict): - def __init__(self, config: Config, session: requests.Session, **kwargs): + def __init__(self, params: ResourceParameters, **kwargs): super().__init__(**kwargs) - self.config: Config - super().__setattr__("config", config) + self.params: ResourceParameters + super().__setattr__("params", params) self.session: requests.Session - super().__setattr__("session", session) + super().__setattr__("session", params.session) + self.url: Url + super().__setattr__("url", params.url) def __setattr__(self, name: str, value: Any) -> None: raise AttributeError("cannot set attributes: use update() instead") class Resources(ABC): - def __init__(self, config: Config, session: requests.Session) -> None: - self.config = config - self.session = session + def __init__(self, params: ResourceParameters) -> None: + self.params = params + self.session = params.session + self.url = params.url diff --git a/src/posit/connect/tasks.py b/src/posit/connect/tasks.py index b9f077f9..83d17b3b 100644 --- a/src/posit/connect/tasks.py +++ b/src/posit/connect/tasks.py @@ -125,7 +125,7 @@ def update(self, *args, **kwargs) -> None: """ params = dict(*args, **kwargs) path = f"v1/tasks/{self.id}" - url = self.config.url + path + url = self.url + path response = self.session.get(url, params=params) result = response.json() super().update(**result) @@ -191,7 +191,7 @@ def get(self, id: str, *args, **kwargs) -> Task: """ params = dict(*args, **kwargs) path = f"v1/tasks/{id}" - url = self.config.url + path + url = self.url + path response = self.session.get(url, params=params) result = response.json() - return Task(self.config, self.session, **result) + return Task(self.params, **result) diff --git a/src/posit/connect/users.py b/src/posit/connect/users.py index 18bf8769..cccb72ad 100644 --- a/src/posit/connect/users.py +++ b/src/posit/connect/users.py @@ -4,13 +4,10 @@ from typing import List, overload -import requests - from . import me -from .config import Config from .content import Content from .paginator import Paginator -from .resources import Resource, Resources +from .resources import Resource, ResourceParameters, Resources class User(Resource): @@ -37,7 +34,7 @@ class User(Resource): @property def content(self) -> Content: - return Content(self.config, self.session, owner_guid=self.guid) + return Content(self.params, owner_guid=self.guid) @property def guid(self) -> str: @@ -96,12 +93,12 @@ def lock(self, *, force: bool = False): ------- None """ - _me = me.get(self.config, self.session) + _me = me.get(self.params) if _me.guid == self.guid and not force: raise RuntimeError( "You cannot lock your own account. Set force=True to override this behavior." ) - url = self.config.url + f"v1/users/{self.guid}/lock" + url = self.url + f"v1/users/{self.guid}/lock" body = {"locked": True} self.session.post(url, json=body) super().update(locked=True) @@ -114,7 +111,7 @@ def unlock(self): ------- None """ - url = self.config.url + f"v1/users/{self.guid}/lock" + url = self.url + f"v1/users/{self.guid}/lock" body = {"locked": False} self.session.post(url, json=body) super().update(locked=False) @@ -172,7 +169,7 @@ def update(self, *args, **kwargs) -> None: None """ body = dict(*args, **kwargs) - url = self.config.url + f"v1/users/{self.guid}" + url = self.url + f"v1/users/{self.guid}" response = self.session.put(url, json=body) super().update(**response.json()) @@ -180,10 +177,8 @@ def update(self, *args, **kwargs) -> None: class Users(Resources): """Users resource.""" - def __init__(self, config: Config, session: requests.Session) -> None: - self.url = config.url + "v1/users" - self.config = config - self.session = session + def __init__(self, params: ResourceParameters) -> None: + super().__init__(params) @overload def find( @@ -197,13 +192,13 @@ def find( def find(self, *args, **kwargs) -> List[User]: ... def find(self, *args, **kwargs): + url = self.params.url + "v1/users" params = dict(*args, **kwargs) - paginator = Paginator(self.session, self.url, params=params) + paginator = Paginator(self.session, url, params=params) results = paginator.fetch_results() return [ User( - config=self.config, - session=self.session, + self.params, **user, ) for user in results @@ -221,14 +216,14 @@ def find_one( def find_one(self, *args, **kwargs) -> User | None: ... def find_one(self, *args, **kwargs) -> User | None: + url = self.params.url + "v1/users" params = dict(*args, **kwargs) - paginator = Paginator(self.session, self.url, params=params) + paginator = Paginator(self.session, url, params=params) pages = paginator.fetch_pages() results = (result for page in pages for result in page.results) users = ( User( - config=self.config, - session=self.session, + self.params, **result, ) for result in results @@ -236,17 +231,15 @@ def find_one(self, *args, **kwargs) -> User | None: return next(users, None) def get(self, id: str) -> User: - url = self.config.url + f"v1/users/{id}" + url = self.url + f"v1/users/{id}" response = self.session.get(url) return User( - config=self.config, - session=self.session, + self.params, **response.json(), ) def count(self) -> int: - response: requests.Response = self.session.get( - self.url, params={"page_size": 1} - ) + url = self.params.url + "v1/users" + response = self.session.get(url, params={"page_size": 1}) result: dict = response.json() return result["total"] diff --git a/src/posit/connect/variants.py b/src/posit/connect/variants.py index 8ce896da..fcc78d3f 100644 --- a/src/posit/connect/variants.py +++ b/src/posit/connect/variants.py @@ -1,9 +1,6 @@ from typing import List -from requests import Session - -from .config import Config -from .resources import Resource, Resources +from .resources import Resource, ResourceParameters, Resources from .tasks import Task @@ -18,23 +15,19 @@ def is_default(self) -> bool: def render(self) -> Task: path = f"variants/{self.id}/render" - url = self.config.url + path + url = self.url + path response = self.session.post(url) - return Task(self.config, self.session, **response.json()) + return Task(self.params, **response.json()) class Variants(Resources): - def __init__( - self, config: Config, session: Session, content_guid: str - ) -> None: - super().__init__(config, session) + def __init__(self, params: ResourceParameters, content_guid: str) -> None: + super().__init__(params) self.content_guid = content_guid def find(self) -> List[Variant]: path = f"applications/{self.content_guid}/variants" - url = self.config.url + path + url = self.url + path response = self.session.get(url) results = response.json() or [] - return [ - Variant(self.config, self.session, **result) for result in results - ] + return [Variant(self.params, **result) for result in results] diff --git a/tests/posit/connect/metrics/test_shiny_usage.py b/tests/posit/connect/metrics/test_shiny_usage.py index 7d832acd..29fad126 100644 --- a/tests/posit/connect/metrics/test_shiny_usage.py +++ b/tests/posit/connect/metrics/test_shiny_usage.py @@ -1,7 +1,9 @@ +from unittest import mock + import requests import responses -from posit.connect import config from posit.connect.metrics import shiny_usage +from posit.connect.resources import ResourceParameters from responses import matchers from ..api import load_mock # type: ignore @@ -10,8 +12,7 @@ class TestShinyUsageEventAttributes: def setup_class(cls): cls.event = shiny_usage.ShinyUsageEvent( - None, - None, + mock.Mock(), **load_mock("v1/instrumentation/shiny/usage?limit=500.json")[ "results" ][0], @@ -68,11 +69,12 @@ def test(self): ) # setup - c = config.Config("12345", "https://connect.example") - session = requests.Session() + params = ResourceParameters( + requests.Session(), "https://connect.example/__api__" + ) # invoke - events = shiny_usage.ShinyUsage(c, session).find() + events = shiny_usage.ShinyUsage(params).find() # assert assert mock_get[0].call_count == 1 @@ -113,11 +115,12 @@ def test(self): ) # setup - c = config.Config("12345", "https://connect.example") - session = requests.Session() + params = ResourceParameters( + requests.Session(), "https://connect.example/__api__" + ) # invoke - event = shiny_usage.ShinyUsage(c, session).find_one() + event = shiny_usage.ShinyUsage(params).find_one() # assert assert mock_get[0].call_count == 1 diff --git a/tests/posit/connect/metrics/test_usage.py b/tests/posit/connect/metrics/test_usage.py index ff557b29..a1257a5e 100644 --- a/tests/posit/connect/metrics/test_usage.py +++ b/tests/posit/connect/metrics/test_usage.py @@ -1,3 +1,5 @@ +from unittest import mock + import pytest import responses from posit import connect @@ -16,8 +18,7 @@ def test(self): class TestUsageEventFromVisitEvent: def setup_class(cls): visit_event = visits.VisitEvent( - None, - None, + mock.Mock(), **load_mock("v1/instrumentation/content/visits?limit=500.json")[ "results" ][0], @@ -60,8 +61,7 @@ def test_path(self): class TestUsageEventFromShinyUsageEvent: def setup_class(cls): visit_event = shiny_usage.ShinyUsageEvent( - None, - None, + mock.Mock(), **load_mock("v1/instrumentation/shiny/usage?limit=500.json")[ "results" ][0], diff --git a/tests/posit/connect/metrics/test_visits.py b/tests/posit/connect/metrics/test_visits.py index 2a1e7132..cce28f7d 100644 --- a/tests/posit/connect/metrics/test_visits.py +++ b/tests/posit/connect/metrics/test_visits.py @@ -1,7 +1,9 @@ +from unittest import mock + import requests import responses -from posit.connect import config from posit.connect.metrics import visits +from posit.connect.resources import ResourceParameters from responses import matchers from ..api import load_mock # type: ignore @@ -10,8 +12,7 @@ class TestVisitAttributes: def setup_class(cls): cls.visit = visits.VisitEvent( - None, - None, + mock.Mock(), **load_mock("v1/instrumentation/content/visits?limit=500.json")[ "results" ][0], @@ -77,11 +78,12 @@ def test(self): ) # setup - c = config.Config("12345", "https://connect.example") - session = requests.Session() + params = ResourceParameters( + requests.Session(), "https://connect.example/__api__" + ) # invoke - events = visits.Visits(c, session).find() + events = visits.Visits(params).find() # assert assert mock_get[0].call_count == 1 @@ -122,11 +124,12 @@ def test(self): ) # setup - c = config.Config("12345", "https://connect.example") - session = requests.Session() + params = ResourceParameters( + requests.Session(), "https://connect.example/__api__" + ) # invoke - event = visits.Visits(c, session).find_one() + event = visits.Visits(params).find_one() # assert assert mock_get[0].call_count == 1 diff --git a/tests/posit/connect/test_bundles.py b/tests/posit/connect/test_bundles.py index 4ba544fb..16adaa40 100644 --- a/tests/posit/connect/test_bundles.py +++ b/tests/posit/connect/test_bundles.py @@ -2,11 +2,9 @@ from unittest import mock import pytest -import requests import responses from posit.connect import Client from posit.connect.bundles import Bundle -from posit.connect.config import Config from responses import matchers from .api import get_path, load_mock # type: ignore @@ -14,11 +12,8 @@ class TestBundleProperties: def setup_class(cls): - config = Config(api_key="12345", url="https://connect.example/") - session = requests.Session() cls.bundle = Bundle( - config, - session, + mock.Mock(), **load_mock( f"v1/content/f2f37341-e21d-3d80-c698-a935ad614066/bundles/101.json" ), diff --git a/tests/posit/connect/test_content.py b/tests/posit/connect/test_content.py index 60ac4222..b846ad98 100644 --- a/tests/posit/connect/test_content.py +++ b/tests/posit/connect/test_content.py @@ -1,8 +1,8 @@ +from unittest import mock + import pytest -import requests import responses from posit.connect.client import Client -from posit.connect.config import Config from posit.connect.content import ContentItem, ContentItemOwner from posit.connect.permissions import Permissions from responses import matchers @@ -14,10 +14,8 @@ class TestContentOwnerAttributes: @classmethod def setup_class(cls): guid = "20a79ce3-6e87-4522-9faf-be24228800a4" - config = Config(api_key="12345", url="https://connect.example/") - session = requests.Session() fake_item = load_mock(f"v1/users/{guid}.json") - cls.item = ContentItemOwner(config, session, **fake_item) + cls.item = ContentItemOwner(mock.Mock(), **fake_item) def test_guid(self): assert self.item.guid == "20a79ce3-6e87-4522-9faf-be24228800a4" @@ -36,10 +34,8 @@ class TestContentItemAttributes: @classmethod def setup_class(cls): guid = "f2f37341-e21d-3d80-c698-a935ad614066" - config = Config(api_key="12345", url="https://connect.example/") - session = requests.Session() fake_item = load_mock(f"v1/content/{guid}.json") - cls.item = ContentItem(config, session, **fake_item) + cls.item = ContentItem(mock.Mock(), **fake_item) def test_id(self): assert self.item.id == "8274" @@ -221,18 +217,21 @@ def test(self): guid = "f2f37341-e21d-3d80-c698-a935ad614066" # behavior + mock_get = responses.get( + f"https://connect.example/__api__/v1/content/{guid}", + json=load_mock(f"v1/content/{guid}.json"), + ) + mock_delete = responses.delete( f"https://connect.example/__api__/v1/content/{guid}" ) # setup - config = Config(api_key="12345", url="https://connect.example/") - session = requests.Session() - fake_item = load_mock(f"v1/content/{guid}.json") - item = ContentItem(config, session, **fake_item) + c = Client("https://connect.example", "12345") + content = c.content.get(guid) # invoke - item.delete() + content.delete() # assert assert mock_delete.call_count == 1 diff --git a/tests/posit/connect/test_groups.py b/tests/posit/connect/test_groups.py index 0c6d4751..d63beed8 100644 --- a/tests/posit/connect/test_groups.py +++ b/tests/posit/connect/test_groups.py @@ -1,7 +1,6 @@ +from unittest import mock from unittest.mock import Mock -import requests -from posit.connect.config import Config from posit.connect.groups import Group from .api import load_mock # type: ignore @@ -14,10 +13,8 @@ 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) + cls.item = Group(mock.Mock(), **fake_item) def test_guid(self): assert self.item.guid == "6f300623-1e0c-48e6-a473-ddf630c0c0c3" diff --git a/tests/posit/connect/test_permissions.py b/tests/posit/connect/test_permissions.py index 346501af..03099af9 100644 --- a/tests/posit/connect/test_permissions.py +++ b/tests/posit/connect/test_permissions.py @@ -3,8 +3,9 @@ import requests import responses -from posit.connect.config import Config from posit.connect.permissions import Permission, Permissions +from posit.connect.resources import ResourceParameters +from posit.connect.urls import Url from responses import matchers from .api import load_mock # type: ignore @@ -23,12 +24,13 @@ def test(self): ) # setup - config = Config(api_key="12345", url="https://connect.example/") - session = requests.Session() + params = ResourceParameters( + requests.Session(), Url("https://connect.example/__api__") + ) fake_permission = load_mock( f"v1/content/{content_guid}/permissions/{id}.json" ) - permission = Permission(config, session, **fake_permission) + permission = Permission(params, **fake_permission) # invoke permission.delete() @@ -70,11 +72,11 @@ def test_request_shape(self): ) # setup - config = Config(api_key="12345", url="https://connect.example/") - session = requests.Session() + params = ResourceParameters( + requests.Session(), Url("https://connect.example/__api__") + ) permission = Permission( - config, - session, + params, id=id, content_guid=content_guid, principal_guid=principal_guid, @@ -117,10 +119,11 @@ def test_role_update(self): ) # setup - config = Config(api_key="12345", url="https://connect.example/") - session = requests.Session() + params = ResourceParameters( + requests.Session(), Url("https://connect.example/__api__") + ) permission = Permission( - config, session, id=id, content_guid=content_guid, role=old_role + params, id=id, content_guid=content_guid, role=old_role ) # assert role change with respect to api response @@ -145,9 +148,10 @@ def test(self): ) # setup - config = Config(api_key="12345", url="https://connect.example/") - session = requests.Session() - permissions = Permissions(config, session, content_guid=content_guid) + params = ResourceParameters( + requests.Session(), Url("https://connect.example/__api__") + ) + permissions = Permissions(params, content_guid=content_guid) # invoke count = permissions.count() @@ -188,9 +192,10 @@ def test(self): ) # setup - config = Config(api_key="12345", url="https://connect.example/") - session = requests.Session() - permissions = Permissions(config, session, content_guid=content_guid) + params = ResourceParameters( + requests.Session(), Url("https://connect.example/__api__") + ) + permissions = Permissions(params, content_guid=content_guid) # invoke permission = permissions.create( @@ -219,9 +224,10 @@ def test(self): ) # setup - config = Config(api_key="12345", url="https://connect.example/") - session = requests.Session() - permissions = Permissions(config, session, content_guid=content_guid) + params = ResourceParameters( + requests.Session(), Url("https://connect.example/__api__") + ) + permissions = Permissions(params, content_guid=content_guid) # invoke permissions = permissions.find() @@ -246,9 +252,10 @@ def test(self): ) # setup - config = Config(api_key="12345", url="https://connect.example/") - session = requests.Session() - permissions = Permissions(config, session, content_guid=content_guid) + params = ResourceParameters( + requests.Session(), Url("https://connect.example/__api__") + ) + permissions = Permissions(params, content_guid=content_guid) # invoke permission = permissions.find_one() @@ -274,9 +281,10 @@ def test(self): ) # setup - config = Config(api_key="12345", url="https://connect.example/") - session = requests.Session() - permissions = Permissions(config, session, content_guid=content_guid) + params = ResourceParameters( + requests.Session(), Url("https://connect.example/__api__") + ) + permissions = Permissions(params, content_guid=content_guid) # invoke permission = permissions.get(id) diff --git a/tests/posit/connect/test_resources.py b/tests/posit/connect/test_resources.py index 7a7e94fd..e6273f8a 100644 --- a/tests/posit/connect/test_resources.py +++ b/tests/posit/connect/test_resources.py @@ -1,5 +1,6 @@ import warnings from typing import Optional +from unittest import mock from unittest.mock import Mock from posit.connect.resources import Resource @@ -16,19 +17,19 @@ def foo(self) -> Optional[str]: class TestResource: def test_init(self): + p = mock.Mock() k = "foo" v = "bar" d = dict({k: v}) - r = FakeResource(config, session, **d) - assert r.session == session - assert r.config == config + r = FakeResource(p, **d) + assert r.params == p def test__getitem__(self): warnings.filterwarnings("ignore", category=FutureWarning) k = "foo" v = "bar" d = dict({k: v}) - r = FakeResource(config, session, **d) + r = FakeResource(mock.Mock(), **d) assert r.__getitem__(k) == d.__getitem__(k) assert r[k] == d[k] @@ -38,7 +39,7 @@ def test__setitem__(self): v1 = "bar" v2 = "baz" d = dict({k: v1}) - r = FakeResource(config, session, **d) + r = FakeResource(mock.Mock(), **d) assert r[k] == v1 r[k] = v2 assert r[k] == v2 @@ -48,7 +49,7 @@ def test__delitem__(self): k = "foo" v = "bar" d = dict({k: v}) - r = FakeResource(config, session, **d) + r = FakeResource(mock.Mock(), **d) assert k in r assert r[k] == v del r[k] @@ -58,5 +59,5 @@ def test_foo(self): k = "foo" v = "bar" d = dict({k: v}) - r = FakeResource(config, session, **d) + r = FakeResource(mock.Mock(), **d) assert r.foo == v diff --git a/tests/posit/connect/test_tasks.py b/tests/posit/connect/test_tasks.py index 4b8162df..134d8017 100644 --- a/tests/posit/connect/test_tasks.py +++ b/tests/posit/connect/test_tasks.py @@ -1,3 +1,5 @@ +from unittest import mock + import responses from posit import connect from posit.connect import tasks @@ -9,8 +11,7 @@ class TestTaskAttributes: def setup_class(cls): cls.task = tasks.Task( - None, - None, + mock.Mock(), **load_mock("v1/tasks/jXhOhdm5OOSkGhJw.json"), ) diff --git a/tests/posit/connect/test_users.py b/tests/posit/connect/test_users.py index 14f97c3c..7b5463da 100644 --- a/tests/posit/connect/test_users.py +++ b/tests/posit/connect/test_users.py @@ -1,3 +1,4 @@ +from unittest import mock from unittest.mock import Mock import pytest @@ -15,80 +16,80 @@ class TestUserAttributes: def test_guid(self): - user = User(session, url) + user = User(mock.Mock()) assert hasattr(user, "guid") assert user.guid is None - user = User(session, url, guid="test_guid") + user = User(mock.Mock(), guid="test_guid") assert user.guid == "test_guid" def test_email(self): - user = User(session, url) + user = User(mock.Mock()) assert hasattr(user, "email") assert user.email is None - user = User(session, url, email="test@example.com") + user = User(mock.Mock(), email="test@example.com") assert user.email == "test@example.com" def test_username(self): - user = User(session, url) + user = User(mock.Mock()) assert hasattr(user, "username") assert user.username is None - user = User(session, url, username="test_user") + user = User(mock.Mock(), username="test_user") assert user.username == "test_user" def test_first_name(self): - user = User(session, url) + user = User(mock.Mock()) assert hasattr(user, "first_name") assert user.first_name is None - user = User(session, url, first_name="John") + user = User(mock.Mock(), first_name="John") assert user.first_name == "John" def test_last_name(self): - user = User(session, url) + user = User(mock.Mock()) assert hasattr(user, "last_name") assert user.last_name is None - user = User(session, url, last_name="Doe") + user = User(mock.Mock(), last_name="Doe") assert user.last_name == "Doe" def test_user_role(self): - user = User(session, url) + user = User(mock.Mock()) assert hasattr(user, "user_role") assert user.user_role is None - user = User(session, url, user_role="admin") + user = User(mock.Mock(), user_role="admin") assert user.user_role == "admin" def test_created_time(self): - user = User(session, url) + user = User(mock.Mock()) assert hasattr(user, "created_time") assert user.created_time is None - user = User(session, url, created_time="2022-01-01T00:00:00") + user = User(mock.Mock(), created_time="2022-01-01T00:00:00") assert user.created_time == "2022-01-01T00:00:00" def test_updated_time(self): - user = User(session, url) + user = User(mock.Mock()) assert hasattr(user, "updated_time") assert user.updated_time is None - user = User(session, url, updated_time="2022-01-01T00:00:00") + user = User(mock.Mock(), updated_time="2022-01-01T00:00:00") assert user.updated_time == "2022-01-01T00:00:00" def test_active_time(self): - user = User(session, url) + user = User(mock.Mock()) assert hasattr(user, "active_time") assert user.active_time is None - user = User(session, url, active_time="2022-01-01T00:00:00") + user = User(mock.Mock(), active_time="2022-01-01T00:00:00") assert user.active_time == "2022-01-01T00:00:00" def test_confirmed(self): - user = User(session, url) + user = User(mock.Mock()) assert hasattr(user, "confirmed") assert user.confirmed is None - user = User(session, url, confirmed=True) + user = User(mock.Mock(), confirmed=True) assert user.confirmed is True def test_locked(self): - user = User(session, url) + user = User(mock.Mock()) assert hasattr(user, "locked") assert user.locked is None - user = User(session, url, locked=False) + user = User(mock.Mock(), locked=False) assert user.locked is False