diff --git a/integration/tests/posit/connect/test_content_item_repository.py b/integration/tests/posit/connect/test_content_item_repository.py index 6260b6e3..7911847d 100644 --- a/integration/tests/posit/connect/test_content_item_repository.py +++ b/integration/tests/posit/connect/test_content_item_repository.py @@ -2,7 +2,8 @@ from packaging import version from posit import connect -from posit.connect.content import ContentItem, ContentItemRepository +from posit.connect.content import ContentItem +from posit.connect.repository import ContentItemRepository from . import CONNECT_VERSION @@ -76,12 +77,12 @@ def assert_repo(r: ContentItemRepository): # Update ex_branch = "main" - updated_repo = content_repo.update(branch=ex_branch) - assert updated_repo["branch"] == ex_branch + content_repo.update(branch=ex_branch) + assert content_repo["branch"] == ex_branch - assert updated_repo["repository"] == self.repo_repository - assert updated_repo["directory"] == self.repo_directory - assert updated_repo["polling"] is self.repo_polling + assert content_repo["repository"] == self.repo_repository + assert content_repo["directory"] == self.repo_directory + assert content_repo["polling"] is self.repo_polling # Delete content_repo.destroy() diff --git a/integration/tests/posit/connect/test_tags.py b/integration/tests/posit/connect/test_tags.py index 1d58263c..99e9deb4 100644 --- a/integration/tests/posit/connect/test_tags.py +++ b/integration/tests/posit/connect/test_tags.py @@ -151,12 +151,12 @@ def test_tag_content_items(self): } # Update tag - tagDName = tagD.update(name="tagD_updated") - assert tagDName["name"] == "tagD_updated" + tagD.update(name="tagD_updated") + assert tagD["name"] == "tagD_updated" assert self.client.tags.get(tagD["id"])["name"] == "tagD_updated" - tagDParent = tagDName.update(parent=tagB) - assert tagDParent["parent_id"] == tagB["id"] + tagD.update(parent=tagB) + assert tagD["parent_id"] == tagB["id"] assert self.client.tags.get(tagD["id"])["parent_id"] == tagB["id"] # Cleanup diff --git a/src/posit/connect/_api.py b/src/posit/connect/_api.py deleted file mode 100644 index f45dc0ee..00000000 --- a/src/posit/connect/_api.py +++ /dev/null @@ -1,143 +0,0 @@ -# TODO-barret-future; Piecemeal migrate everything to leverage `ApiDictEndpoint` -# TODO-barret-future; Merge any trailing behavior of `Active` or `ActiveList` into the new classes. - -from __future__ import annotations - -from collections.abc import Mapping - -from typing_extensions import TYPE_CHECKING, Any, Optional, cast - -from ._api_call import ApiCallMixin, get_api -from ._json import Jsonifiable, JsonifiableDict, ResponseAttrs - -if TYPE_CHECKING: - from .context import Context - - -# Design Notes: -# * Perform API calls on property retrieval. e.g. `my_content.repository` -# * Dictionary endpoints: Retrieve all attributes during init unless provided -# * List endpoints: Do not retrieve until `.fetch()` is called directly. Avoids cache invalidation issues. -# * While slower, all ApiListEndpoint helper methods should `.fetch()` on demand. -# * Only expose methods needed for `ReadOnlyDict`. -# * Ex: When inheriting from `dict`, we'd need to shut down `update`, `pop`, etc. -# * Use `ApiContextProtocol` to ensure that the class has the necessary attributes for API calls. -# * Inherit from `ApiCallMixin` to add all helper methods for API calls. -# * Classes should write the `path` only once within its init method. -# * Through regular interactions, the path should only be written once. - -# When making a new class, -# * Use a class to define the parameters and their types -# * If attaching the type info class to the parent class, start with `_`. E.g.: `ContentItemRepository._Attrs` -# * Document all attributes like normal -# * When the time comes that there are multiple attribute types, we can use overloads with full documentation and unpacking of type info class for each overload method. -# * Inherit from `ApiDictEndpoint` or `ApiListEndpoint` as needed -# * Init signature should be `def __init__(self, ctx: Context, path: str, /, **attrs: Jsonifiable) -> None:` - - -# This class should not have typing about the class keys as that would fix the class's typing. If -# for some reason, we _know_ the keys are fixed (as we've moved on to a higher version), we can add -# `Generic[AttrsT]` to the class. -class ReadOnlyDict(Mapping): - _attrs: ResponseAttrs - """Resource attributes passed.""" - - def __init__(self, attrs: ResponseAttrs) -> None: - """ - A read-only dict abstraction for any HTTP endpoint that returns a singular resource. - - Parameters - ---------- - attrs : dict - Resource attributes passed - """ - super().__init__() - self._attrs = attrs - - def get(self, key: str, default: Any = None) -> Any: - return self._attrs.get(key, default) - - def __getitem__(self, key: str) -> Any: - return self._attrs[key] - - def __setitem__(self, key: str, value: Any) -> None: - raise NotImplementedError( - "Resource attributes are locked. " - "To retrieve updated values, please retrieve the parent object again." - ) - - def __len__(self) -> int: - return self._attrs.__len__() - - def __iter__(self): - return self._attrs.__iter__() - - def __contains__(self, key: object) -> bool: - return self._attrs.__contains__(key) - - def __repr__(self) -> str: - return repr(self._attrs) - - def __str__(self) -> str: - return str(self._attrs) - - def keys(self): - return self._attrs.keys() - - def values(self): - return self._attrs.values() - - def items(self): - return self._attrs.items() - - -class ApiDictEndpoint(ApiCallMixin, ReadOnlyDict): - _ctx: Context - """The context object containing the session and URL for API interactions.""" - _path: str - """The HTTP path component for the resource endpoint.""" - - def _get_api(self, *path) -> JsonifiableDict | None: - super()._get_api(*path) - - def __init__( - self, - ctx: Context, - path: str, - get_data: Optional[bool] = None, - /, - **attrs: Jsonifiable, - ) -> None: - """ - A dict abstraction for any HTTP endpoint that returns a singular resource. - - Adds helper methods to interact with the API with reduced boilerplate. - - Parameters - ---------- - ctx : Context - The context object containing the session and URL for API interactions. - path : str - The HTTP path component for the resource endpoint - get_data : Optional[bool] - If `True`, fetch the API and set the attributes from the response. If `False`, only set - the provided attributes. If `None` [default], fetch the API if no attributes are - provided. - attrs : dict - Resource attributes passed - """ - # If no attributes are provided, fetch the API and set the attributes from the response - if get_data is None: - get_data = len(attrs) == 0 - - # If we should get data, fetch the API and set the attributes from the response - if get_data: - init_attrs: Jsonifiable = get_api(ctx, path) - init_attrs_dict = cast(ResponseAttrs, init_attrs) - # Overwrite the initial attributes with `attrs`: e.g. {'key': value} | {'content_guid': '123'} - init_attrs_dict.update(attrs) - attrs = init_attrs_dict - - super().__init__(attrs) - self._ctx = ctx - self._path = path diff --git a/src/posit/connect/_api_call.py b/src/posit/connect/_api_call.py deleted file mode 100644 index 0de5f0ba..00000000 --- a/src/posit/connect/_api_call.py +++ /dev/null @@ -1,73 +0,0 @@ -from __future__ import annotations - -import posixpath - -from typing_extensions import TYPE_CHECKING, Protocol - -if TYPE_CHECKING: - from ._json import Jsonifiable - from .context import Context - - -class ApiCallProtocol(Protocol): - _ctx: Context - _path: str - - def _endpoint(self, *path) -> str: ... - def _get_api(self, *path) -> Jsonifiable: ... - def _delete_api(self, *path) -> Jsonifiable | None: ... - def _patch_api(self, *path, json: Jsonifiable | None) -> Jsonifiable: ... - def _put_api(self, *path, json: Jsonifiable | None) -> Jsonifiable: ... - - -def endpoint(*path) -> str: - return posixpath.join(*path) - - -# Helper methods for API interactions -def get_api(ctx: Context, *path) -> Jsonifiable: - response = ctx.client.get(*path) - return response.json() - - -def put_api( - ctx: Context, - *path, - json: Jsonifiable | None, -) -> Jsonifiable: - response = ctx.client.put(*path, json=json) - return response.json() - - -# Mixin class for API interactions - - -class ApiCallMixin: - def _endpoint(self: ApiCallProtocol, *path) -> str: - return endpoint(self._path, *path) - - def _get_api(self: ApiCallProtocol, *path) -> Jsonifiable: - response = self._ctx.client.get(self._endpoint(*path)) - return response.json() - - def _delete_api(self: ApiCallProtocol, *path) -> Jsonifiable | None: - response = self._ctx.client.delete(self._endpoint(*path)) - if len(response.content) == 0: - return None - return response.json() - - def _patch_api( - self: ApiCallProtocol, - *path, - json: Jsonifiable | None, - ) -> Jsonifiable: - response = self._ctx.client.patch(self._endpoint(*path), json=json) - return response.json() - - def _put_api( - self: ApiCallProtocol, - *path, - json: Jsonifiable | None, - ) -> Jsonifiable: - response = self._ctx.client.put(self._endpoint(*path), json=json) - return response.json() diff --git a/src/posit/connect/_json.py b/src/posit/connect/_json.py deleted file mode 100644 index 85f331fa..00000000 --- a/src/posit/connect/_json.py +++ /dev/null @@ -1,37 +0,0 @@ -from typing_extensions import Dict, List, Tuple, TypeVar, Union - -# Implemented in https://github.com/posit-dev/py-shiny/blob/415ced034e6c500adda524abb7579731c32088b5/shiny/types.py#L357-L386 -# Table from: https://github.com/python/cpython/blob/df1eec3dae3b1eddff819fd70f58b03b3fbd0eda/Lib/json/encoder.py#L77-L95 -# +-------------------+---------------+ -# | Python | JSON | -# +===================+===============+ -# | dict | object | -# +-------------------+---------------+ -# | list, tuple | array | -# +-------------------+---------------+ -# | str | string | -# +-------------------+---------------+ -# | int, float | number | -# +-------------------+---------------+ -# | True | true | -# +-------------------+---------------+ -# | False | false | -# +-------------------+---------------+ -# | None | null | -# +-------------------+---------------+ -Jsonifiable = Union[ - str, - int, - float, - bool, - None, - List["Jsonifiable"], - Tuple["Jsonifiable", ...], - "JsonifiableDict", -] - -JsonifiableT = TypeVar("JsonifiableT", bound="Jsonifiable") -JsonifiableDict = Dict[str, Jsonifiable] -JsonifiableList = List[JsonifiableT] - -ResponseAttrs = Dict[str, Jsonifiable] diff --git a/src/posit/connect/_utils.py b/src/posit/connect/_utils.py index e279f3b7..c35dabd9 100644 --- a/src/posit/connect/_utils.py +++ b/src/posit/connect/_utils.py @@ -3,5 +3,28 @@ from typing_extensions import Any -def drop_none(x: dict[str, Any]) -> dict[str, Any]: - return {k: v for k, v in x.items() if v is not None} +def update_dict_values(obj: dict[str, Any], /, **kwargs: Any) -> None: + """ + Update the values of a dictionary. + + This helper method exists as a workaround for the `dict.update` method. Sometimes, `super()` does not return the `dict` class and. If `super().update(**kwargs)` is called unintended behavior will occur. + + Therefore, this helper method exists to update the `dict`'s values. + + Parameters + ---------- + obj : dict[str, Any] + The object to update. + kwargs : Any + The key-value pairs to update the object with. + + See Also + -------- + * https://github.com/posit-dev/posit-sdk-py/pull/366#discussion_r1887845267 + """ + # Could also be performed with: + # for key, value in kwargs.items(): + # obj[key] = value + + # Use the `dict` class to explicity update the object in-place + dict.update(obj, **kwargs) diff --git a/src/posit/connect/content.py b/src/posit/connect/content.py index f7a3fdb8..5a8e7779 100644 --- a/src/posit/connect/content.py +++ b/src/posit/connect/content.py @@ -15,18 +15,16 @@ Required, TypedDict, Unpack, - cast, overload, ) from . import tasks -from ._api import ApiDictEndpoint, JsonifiableDict from .bundles import Bundles from .context import requires from .env import EnvVars -from .errors import ClientError from .oauth.associations import ContentItemAssociations from .permissions import Permissions +from .repository import ContentItemRepositoryMixin from .resources import Active, BaseResource, Resources, _ResourceSequence from .tags import ContentItemTags from .vanities import VanityMixin @@ -44,125 +42,6 @@ def _assert_guid(guid: str): assert len(guid) > 0, "Expected 'guid' to be non-empty" -def _assert_content_guid(content_guid: str): - assert isinstance(content_guid, str), "Expected 'content_guid' to be a string" - assert len(content_guid) > 0, "Expected 'content_guid' to be non-empty" - - -class ContentItemRepository(ApiDictEndpoint): - """ - Content items GitHub repository information. - - See Also - -------- - * Get info: https://docs.posit.co/connect/api/#get-/v1/content/-guid-/repository - * Delete info: https://docs.posit.co/connect/api/#delete-/v1/content/-guid-/repository - * Update info: https://docs.posit.co/connect/api/#patch-/v1/content/-guid-/repository - """ - - class _Attrs(TypedDict, total=False): - repository: str - """URL for the repository.""" - branch: NotRequired[str] - """The tracked Git branch.""" - directory: NotRequired[str] - """Directory containing the content.""" - polling: NotRequired[bool] - """Indicates that the Git repository is regularly polled.""" - - def __init__( - self, - ctx: Context, - /, - *, - content_guid: str, - # By default, the `attrs` will be retrieved from the API if no `attrs` are supplied. - **attrs: Unpack[ContentItemRepository._Attrs], - ) -> None: - """Content items GitHub repository information. - - Parameters - ---------- - ctx : Context - The context object containing the session and URL for API interactions. - content_guid : str - The unique identifier of the content item. - **attrs : ContentItemRepository._Attrs - Attributes for the content item repository. If not supplied, the attributes will be - retrieved from the API upon initialization - """ - _assert_content_guid(content_guid) - - path = self._api_path(content_guid) - # Only fetch data if `attrs` are not supplied - get_data = len(attrs) == 0 - super().__init__(ctx, path, get_data, **{"content_guid": content_guid, **attrs}) - - @classmethod - def _api_path(cls, content_guid: str) -> str: - return f"v1/content/{content_guid}/repository" - - @classmethod - def _create( - cls, - ctx: Context, - content_guid: str, - **attrs: Unpack[ContentItemRepository._Attrs], - ) -> ContentItemRepository: - from ._api_call import put_api - - result = put_api(ctx, cls._api_path(content_guid), json=cast(JsonifiableDict, attrs)) - - return ContentItemRepository( - ctx, - content_guid=content_guid, - **result, # pyright: ignore[reportCallIssue] - ) - - def destroy(self) -> None: - """ - Delete the content's git repository location. - - See Also - -------- - * https://docs.posit.co/connect/api/#delete-/v1/content/-guid-/repository - """ - self._delete_api() - - def update( - self, - # *, - **attrs: Unpack[ContentItemRepository._Attrs], - ) -> ContentItemRepository: - """Update the content's repository. - - Parameters - ---------- - repository: str, optional - URL for the repository. Default is None. - branch: str, optional - The tracked Git branch. Default is 'main'. - directory: str, optional - Directory containing the content. Default is '.' - polling: bool, optional - Indicates that the Git repository is regularly polled. Default is False. - - Returns - ------- - None - - See Also - -------- - * https://docs.posit.co/connect/api/#patch-/v1/content/-guid-/repository - """ - result = self._patch_api(json=cast(JsonifiableDict, dict(attrs))) - return ContentItemRepository( - self._ctx, - content_guid=self["content_guid"], - **result, # pyright: ignore[reportCallIssue] - ) - - class ContentItemOAuth(BaseResource): def __init__(self, ctx: Context, content_guid: str) -> None: super().__init__(ctx) @@ -177,7 +56,7 @@ class ContentItemOwner(BaseResource): pass -class ContentItem(Active, VanityMixin, BaseResource): +class ContentItem(Active, ContentItemRepositoryMixin, VanityMixin, BaseResource): class _AttrsBase(TypedDict, total=False): # # `name` will be set by other _Attrs classes # name: str @@ -264,36 +143,6 @@ def __getitem__(self, key: Any) -> Any: def oauth(self) -> ContentItemOAuth: return ContentItemOAuth(self._ctx, content_guid=self["guid"]) - @property - def repository(self) -> ContentItemRepository | None: - try: - return ContentItemRepository(self._ctx, content_guid=self["guid"]) - except ClientError: - return None - - def create_repository( - self, - **attrs: Unpack[ContentItemRepository._Attrs], - ) -> ContentItemRepository: - """Create repository. - - Parameters - ---------- - repository : str - URL for the respository. - branch : str, optional - The tracked Git branch. Default is 'main'. - directory : str, optional - Directory containing the content. Default is '.'. - polling : bool, optional - Indicates that the Git repository is regularly polled. Default is False. - - Returns - ------- - ContentItemRepository - """ - return ContentItemRepository._create(self._ctx, self["guid"], **attrs) - def delete(self) -> None: """Delete the content item.""" path = f"v1/content/{self['guid']}" @@ -379,7 +228,7 @@ def restart(self) -> None: f"Restart not supported for this application mode: {self['app_mode']}. Did you need to use the 'render()' method instead? Note that some application modes do not support 'render()' or 'restart()'.", ) - def update( # type: ignore[reportIncompatibleMethodOverride] + def update( self, **attrs: Unpack[ContentItem._Attrs], ) -> None: diff --git a/src/posit/connect/env.py b/src/posit/connect/env.py index f801dc00..a04a2ec9 100644 --- a/src/posit/connect/env.py +++ b/src/posit/connect/env.py @@ -131,7 +131,7 @@ def items(self): "Since environment variables may contain sensitive information, the values are not accessible outside of Connect.", ) - def update(self, other=(), /, **kwargs: Optional[str]): + def update(self, other=(), /, **kwargs: Optional[str]) -> None: """ Update environment variables. diff --git a/src/posit/connect/metrics/usage.py b/src/posit/connect/metrics/usage.py index b513a52f..2bb203f0 100644 --- a/src/posit/connect/metrics/usage.py +++ b/src/posit/connect/metrics/usage.py @@ -198,10 +198,7 @@ def find(self, **kwargs) -> List[UsageEvent]: for finder in finders: instance = finder(self._ctx) events.extend( - [ - UsageEvent.from_event(event) - for event in instance.find(**kwargs) # type: ignore[attr-defined] - ], + [UsageEvent.from_event(event) for event in instance.find(**kwargs)], ) return events @@ -251,7 +248,7 @@ def find_one(self, **kwargs) -> UsageEvent | None: finders = (visits.Visits, shiny_usage.ShinyUsage) for finder in finders: instance = finder(self._ctx) - event = instance.find_one(**kwargs) # type: ignore[attr-defined] + event = instance.find_one(**kwargs) if event: return UsageEvent.from_event(event) return None diff --git a/src/posit/connect/repository.py b/src/posit/connect/repository.py new file mode 100644 index 00000000..2b81e554 --- /dev/null +++ b/src/posit/connect/repository.py @@ -0,0 +1,135 @@ +"""Content item repository.""" + +from __future__ import annotations + +from typing_extensions import ( + Optional, + Protocol, + overload, + runtime_checkable, +) + +from ._utils import update_dict_values +from .errors import ClientError +from .resources import Resource, _Resource + + +# ContentItem Repository uses a PATCH method, not a PUT for updating. +class _ContentItemRepository(_Resource): + def update(self, **attributes) -> None: + response = self._ctx.client.patch(self._path, json=attributes) + result = response.json() + + update_dict_values(self, **result) + + +@runtime_checkable +class ContentItemRepository(Resource, Protocol): + """ + Content items GitHub repository information. + + See Also + -------- + * Get info: https://docs.posit.co/connect/api/#get-/v1/content/-guid-/repository + * Delete info: https://docs.posit.co/connect/api/#delete-/v1/content/-guid-/repository + * Update info: https://docs.posit.co/connect/api/#patch-/v1/content/-guid-/repository + """ + + def destroy(self) -> None: + """ + Delete the content's git repository location. + + See Also + -------- + * https://docs.posit.co/connect/api/#delete-/v1/content/-guid-/repository + """ + ... + + def update( + self, + *, + repository: Optional[str] = None, + branch: str = "main", + directory: str = ".", + polling: bool = False, + ) -> None: + """Update the content's repository. + + Parameters + ---------- + repository: str, optional + URL for the repository. Default is None. + branch: str, optional + The tracked Git branch. Default is 'main'. + directory: str, optional + Directory containing the content. Default is '.' + polling: bool, optional + Indicates that the Git repository is regularly polled. Default is False. + + Returns + ------- + None + + See Also + -------- + * https://docs.posit.co/connect/api/#patch-/v1/content/-guid-/repository + """ + ... + + +class ContentItemRepositoryMixin: + @property + def repository(self: Resource) -> ContentItemRepository | None: + try: + path = f"v1/content/{self['guid']}/repository" + response = self._ctx.client.get(path) + result = response.json() + return _ContentItemRepository( + self._ctx, + path, + **result, + ) + except ClientError: + return None + + @overload + def create_repository( + self: Resource, + /, + *, + repository: Optional[str] = None, + branch: str = "main", + directory: str = ".", + polling: bool = False, + ) -> ContentItemRepository: ... + + @overload + def create_repository(self: Resource, /, **attributes) -> ContentItemRepository: ... + + def create_repository(self: Resource, /, **attributes) -> ContentItemRepository: + """Create repository. + + Parameters + ---------- + repository : str + URL for the respository. + branch : str, optional + The tracked Git branch. Default is 'main'. + directory : str, optional + Directory containing the content. Default is '.'. + polling : bool, optional + Indicates that the Git repository is regularly polled. Default is False. + + Returns + ------- + ContentItemRepository + """ + path = f"v1/content/{self['guid']}/repository" + response = self._ctx.client.put(path, json=attributes) + result = response.json() + + return _ContentItemRepository( + self._ctx, + path, + **result, + ) diff --git a/src/posit/connect/resources.py b/src/posit/connect/resources.py index 2f75d14c..6617e4f0 100644 --- a/src/posit/connect/resources.py +++ b/src/posit/connect/resources.py @@ -71,6 +71,9 @@ def __init__(self, ctx: Context, path: str, /, **attributes): class Resource(Protocol): + _ctx: Context + _path: str + def __getitem__(self, key: Hashable, /) -> Any: ... @@ -83,7 +86,7 @@ def __init__(self, ctx: Context, path: str, **attributes): def destroy(self) -> None: self._ctx.client.delete(self._path) - def update(self, **attributes): # type: ignore[reportIncompatibleMethodOverride] + def update(self, **attributes): # pyright: ignore[reportIncompatibleMethodOverride] response = self._ctx.client.put(self._path, json=attributes) result = response.json() super().update(**result) diff --git a/src/posit/connect/tags.py b/src/posit/connect/tags.py index 0ff303ae..47f8f809 100644 --- a/src/posit/connect/tags.py +++ b/src/posit/connect/tags.py @@ -4,6 +4,7 @@ from typing_extensions import TYPE_CHECKING, NotRequired, Optional, TypedDict, Unpack, overload +from ._utils import update_dict_values from .context import Context, ContextManager from .resources import Active @@ -161,14 +162,14 @@ def destroy(self) -> None: # Allow for every combination of `name` and (`parent` or `parent_id`) @overload - def update(self, /, *, name: str = ..., parent: Tag | None = ...) -> Tag: ... + def update(self, /, *, name: str = ..., parent: Tag | None = ...) -> None: ... @overload - def update(self, /, *, name: str = ..., parent_id: str | None = ...) -> Tag: ... + def update(self, /, *, name: str = ..., parent_id: str | None = ...) -> None: ... - def update( # pyright: ignore[reportIncompatibleMethodOverride] ; This method returns `Tag`. Parent method returns `None` + def update( self, **kwargs, - ) -> Tag: + ) -> None: """ Update the tag. @@ -213,7 +214,7 @@ def update( # pyright: ignore[reportIncompatibleMethodOverride] ; This method r updated_kwargs = _update_parent_kwargs(kwargs) response = self._ctx.client.patch(self._path, json=updated_kwargs) result = response.json() - return Tag(self._ctx, self._path, **result) + update_dict_values(self, **result) class TagContentItems(ContextManager): diff --git a/tests/posit/connect/api.py b/tests/posit/connect/api.py index 06b5f6cc..1b99badb 100644 --- a/tests/posit/connect/api.py +++ b/tests/posit/connect/api.py @@ -20,7 +20,7 @@ def load_mock(path: str): Returns ------- - Jsonifiable + dict | list The parsed data from the JSONC file. Examples diff --git a/tests/posit/connect/test_api_endpoint.py b/tests/posit/connect/test_api_endpoint.py deleted file mode 100644 index 2281084f..00000000 --- a/tests/posit/connect/test_api_endpoint.py +++ /dev/null @@ -1,18 +0,0 @@ -import pytest - -from posit.connect._api import ReadOnlyDict - - -class TestApiEndpoint: - def test_read_only(self): - obj = ReadOnlyDict({}) - - assert len(obj) == 0 - - assert obj.get("foo", "bar") == "bar" - - with pytest.raises(NotImplementedError): - obj["foo"] = "baz" - - eq_obj = ReadOnlyDict({"foo": "bar", "a": 1}) - assert eq_obj == {"foo": "bar", "a": 1} diff --git a/tests/posit/connect/test_content.py b/tests/posit/connect/test_content.py index 27f5318b..310582b7 100644 --- a/tests/posit/connect/test_content.py +++ b/tests/posit/connect/test_content.py @@ -3,7 +3,8 @@ from responses import matchers from posit.connect.client import Client -from posit.connect.content import ContentItem, ContentItemRepository +from posit.connect.content import ContentItem +from posit.connect.resources import _Resource from .api import load_mock, load_mock_dict @@ -576,7 +577,7 @@ def mock_repository_info(self): ) repository_info = content_item.repository - assert isinstance(repository_info, ContentItemRepository) + assert isinstance(repository_info, _Resource) assert mock_get.call_count == 1 return repository_info @@ -594,14 +595,14 @@ def test_repository_update(self): self.endpoint, json=load_mock_dict(f"v1/content/{self.content_guid}/repository_patch.json"), ) - new_repository_info = repository_info.update(branch="testing-main") + repository_info.update(branch="testing-main") assert mock_patch.call_count == 1 for key, value in repository_info.items(): if key == "branch": - assert new_repository_info[key] == "testing-main" + assert repository_info[key] == "testing-main" else: - assert new_repository_info[key] == value + assert repository_info[key] == value @responses.activate def test_repository_delete(self): diff --git a/tests/posit/connect/test_packages.py b/tests/posit/connect/test_packages.py index 28ac75f4..2f1917a5 100644 --- a/tests/posit/connect/test_packages.py +++ b/tests/posit/connect/test_packages.py @@ -2,7 +2,7 @@ from posit.connect.client import Client -from .api import load_mock # type: ignore +from .api import load_mock class TestPackagesFindBy: diff --git a/tests/posit/connect/test_tags.py b/tests/posit/connect/test_tags.py index 9f4f7fb4..4bce01cd 100644 --- a/tests/posit/connect/test_tags.py +++ b/tests/posit/connect/test_tags.py @@ -289,20 +289,17 @@ def test_update(self): tag33 = client.tags.get("33") # invoke - updated_tag33_0 = tag33.update(name="academy-updated", parent_id=None) - updated_tag33_1 = tag33.update(name="academy-updated", parent=None) + tag33.update(name="academy-updated", parent_id=None) + tag33.update(name="academy-updated", parent=None) parent_tag = Tag(client._ctx, "/v1/tags/1", id="42", name="Parent") - updated_tag33_2 = tag33.update(name="academy-updated", parent=parent_tag) - updated_tag33_3 = tag33.update(name="academy-updated", parent_id=parent_tag["id"]) + tag33.update(name="academy-updated", parent=parent_tag) + tag33.update(name="academy-updated", parent_id=parent_tag["id"]) # assert assert mock_get_33_tag.call_count == 1 assert mock_update_33_tag.call_count == 4 - for tag in [updated_tag33_0, updated_tag33_1, updated_tag33_2, updated_tag33_3]: - assert isinstance(tag, Tag) - # Asserting updated values are deferred to integration testing # to avoid agreening with the mocked data