diff --git a/integration/compose.yaml b/integration/compose.yaml index 35bb6a81..79e6973b 100644 --- a/integration/compose.yaml +++ b/integration/compose.yaml @@ -21,9 +21,11 @@ services: - test connect: image: ${DOCKER_CONNECT_IMAGE}:${DOCKER_CONNECT_IMAGE_TAG} + pull_policy: always environment: - CONNECT_BOOTSTRAP_ENABLED=true - CONNECT_BOOTSTRAP_SECRETKEY=${CONNECT_BOOTSTRAP_SECRETKEY} + - CONNECT_APPLICATIONS_PACKAGEAUDITINGENABLED=true networks: - test privileged: true diff --git a/integration/tests/posit/connect/__init__.py b/integration/tests/posit/connect/__init__.py index 8fcd91f1..1328517d 100644 --- a/integration/tests/posit/connect/__init__.py +++ b/integration/tests/posit/connect/__init__.py @@ -1,7 +1,8 @@ -from packaging import version +from packaging.version import parse from posit import connect client = connect.Client() -CONNECT_VERSION = version.parse(client.version) -print(CONNECT_VERSION) +version = client.version +assert version +CONNECT_VERSION = parse(version) diff --git a/integration/tests/posit/connect/test_jobs.py b/integration/tests/posit/connect/test_jobs.py index 100b70c9..3cb32527 100644 --- a/integration/tests/posit/connect/test_jobs.py +++ b/integration/tests/posit/connect/test_jobs.py @@ -49,5 +49,5 @@ def test_find_by(self): task = bundle.deploy() task.wait_for() - jobs = content.jobs.reload() + jobs = content.jobs assert len(jobs) != 0 diff --git a/integration/tests/posit/connect/test_packages.py b/integration/tests/posit/connect/test_packages.py index f322ddf9..1d56c420 100644 --- a/integration/tests/posit/connect/test_packages.py +++ b/integration/tests/posit/connect/test_packages.py @@ -28,13 +28,13 @@ def teardown_class(cls): cls.content.delete() def test(self): - # assert self.client.packages + assert self.client.packages assert self.content.packages def test_find_by(self): - # package = self.client.packages.find_by(name="flask") - # assert package - # assert package["name"] == "flask" + package = self.client.packages.find_by(name="flask") + assert package + assert package["name"] == "flask" package = self.content.packages.find_by(name="flask") assert package diff --git a/src/posit/connect/packages.py b/src/posit/connect/packages.py index b770584f..178ca4c5 100644 --- a/src/posit/connect/packages.py +++ b/src/posit/connect/packages.py @@ -1,7 +1,7 @@ from __future__ import annotations import posixpath -from typing import List, Literal, Optional, TypedDict, overload +from typing import Generator, Literal, Optional, TypedDict, overload from typing_extensions import NotRequired, Required, Unpack @@ -109,6 +109,9 @@ class _Package(TypedDict): language: Required[Literal["python", "r"]] """Programming language ecosystem, options are 'python' and 'r'""" + language_version: Required[str] + """Programming language version""" + name: Required[str] """The package name""" @@ -139,12 +142,13 @@ def __init__(self, ctx, path): def _create_instance(self, path, /, **attributes): return Package(self._ctx, **attributes) - def fetch(self, **conditions) -> List["Package"]: + def fetch(self, **conditions) -> Generator["Package"]: # todo - add pagination support to ActiveSequence url = self._ctx.url + self._path paginator = Paginator(self._ctx.session, url, conditions) - results = paginator.fetch_results() - return [self._create_instance("", **result) for result in results] + for page in paginator.fetch_pages(): + results = page.results + yield from (self._create_instance("", **result) for result in results) def find(self, uid): raise NotImplementedError("The 'find' method is not support by the Packages API.") @@ -153,6 +157,9 @@ class _FindBy(TypedDict, total=False): language: NotRequired[Literal["python", "r"]] """Programming language ecosystem, options are 'python' and 'r'""" + language_version: NotRequired[str] + """Programming language version""" + name: NotRequired[str] """The package name""" diff --git a/src/posit/connect/resources.py b/src/posit/connect/resources.py index 11d5aa6b..a36e0a79 100644 --- a/src/posit/connect/resources.py +++ b/src/posit/connect/resources.py @@ -4,9 +4,18 @@ import warnings from abc import ABC, abstractmethod from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, Generic, List, Optional, Sequence, TypeVar, overload - -from typing_extensions import Self +from itertools import islice +from typing import ( + TYPE_CHECKING, + Any, + Generic, + Iterable, + List, + Optional, + Sequence, + TypeVar, + overload, +) if TYPE_CHECKING: import requests @@ -101,14 +110,13 @@ def __init__(self, ctx: Context, path: str, uid: str = "guid"): self._ctx = ctx self._path = path self._uid = uid - self._cache = None @abstractmethod def _create_instance(self, path: str, /, **kwargs: Any) -> T: """Create an instance of 'T'.""" raise NotImplementedError() - def fetch(self, **conditions) -> List[T]: + def fetch(self, **conditions) -> Iterable[T]: """Fetch the collection. Fetches the collection directly from Connect. This operation does not effect the cache state. @@ -122,61 +130,57 @@ def fetch(self, **conditions) -> List[T]: results = response.json() return [self._to_instance(result) for result in results] - def reload(self) -> Self: - """Reloads the collection from Connect. - - Returns - ------- - Self - """ - self._cache = None - return self - def _to_instance(self, result: dict) -> T: """Converts a result into an instance of T.""" uid = result[self._uid] path = posixpath.join(self._path, uid) return self._create_instance(path, **result) - @property - def _data(self) -> List[T]: - """Get the collection. - - Fetches the collection from Connect and caches the result. Subsequent invocations return the cached results unless the cache is explicitly reset. - - Returns - ------- - List[T] - - See Also - -------- - cached - reload - """ - if self._cache is None: - self._cache = self.fetch() - return self._cache - @overload def __getitem__(self, index: int) -> T: ... @overload def __getitem__(self, index: slice) -> Sequence[T]: ... - def __getitem__(self, index): - return self._data[index] + def __getitem__(self, index) -> Sequence[T] | T: + data = self.fetch() + + if isinstance(index, int): + if index < 0: + # Handle negative indexing + data = list(data) + return data[index] + for i, value in enumerate(data): + if i == index: + return value + raise KeyError(f"Index {index} is out of range.") + + if isinstance(index, slice): + # Handle slicing with islice + start = index.start or 0 + stop = index.stop + step = index.step or 1 + if step == 0: + raise ValueError("slice step cannot be zero") + return [ + value + for i, value in enumerate(islice(data, start, stop)) + if (i + start) % step == 0 + ] + + raise TypeError(f"Index must be int or slice, not {type(index).__name__}.") def __iter__(self): - return iter(self._data) + return iter(self.fetch()) def __len__(self) -> int: - return len(self._data) + return len(list(self.fetch())) def __str__(self) -> str: - return str(self._data) + return str(list(self.fetch())) def __repr__(self) -> str: - return repr(self._data) + return repr(list(self.fetch())) class ActiveFinderMethods(ActiveSequence[T], ABC): @@ -220,5 +224,5 @@ def find_by(self, **conditions) -> Optional[T]: Optional[T] The first record matching the conditions, or `None` if no match is found. """ - collection = self.fetch() + collection = self.fetch(**conditions) return next((v for v in collection if v.items() >= conditions.items()), None) diff --git a/tests/posit/connect/test_jobs.py b/tests/posit/connect/test_jobs.py index 252bd2e8..6e6bc0c5 100644 --- a/tests/posit/connect/test_jobs.py +++ b/tests/posit/connect/test_jobs.py @@ -87,31 +87,6 @@ def test(self): assert job["key"] == "tHawGvHZTosJA2Dx" -class TestJobsReload: - @responses.activate - def test(self): - responses.get( - "https://connect.example/__api__/v1/content/f2f37341-e21d-3d80-c698-a935ad614066", - json=load_mock("v1/content/f2f37341-e21d-3d80-c698-a935ad614066.json"), - ) - - mock_get = responses.get( - "https://connect.example/__api__/v1/content/f2f37341-e21d-3d80-c698-a935ad614066/jobs", - json=load_mock("v1/content/f2f37341-e21d-3d80-c698-a935ad614066/jobs.json"), - ) - - c = Client("https://connect.example", "12345") - content = c.content.get("f2f37341-e21d-3d80-c698-a935ad614066") - - assert len(content.jobs) == 1 - assert mock_get.call_count == 1 - - content.jobs.reload() - - assert len(content.jobs) == 1 - assert mock_get.call_count == 2 - - class TestJobDestory: @responses.activate def test(self):