Skip to content

Commit 7744523

Browse files
committed
feat: adds packages
1 parent e57498f commit 7744523

File tree

7 files changed

+110
-27
lines changed

7 files changed

+110
-27
lines changed

integration/Makefile

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ CONNECT_BOOTSTRAP_SECRETKEY ?= $(shell head -c 32 /dev/random | base64)
2222
help
2323

2424
# Versions
25-
CONNECT_VERSIONS := 2024.08.0 \
25+
CONNECT_VERSIONS := 2024.09.0 \
26+
2024.08.0 \
2627
2024.06.0 \
2728
2024.05.0 \
2829
2024.04.1 \
@@ -137,5 +138,5 @@ test:
137138
set -o pipefail; \
138139
CONNECT_VERSION=${CONNECT_VERSION} \
139140
CONNECT_API_KEY="$(shell $(UV) run rsconnect bootstrap -i -s http://connect:3939 --raw)" \
140-
$(UV) run pytest -s -k TestJobs --junit-xml=./reports/$(CONNECT_VERSION).xml | \
141+
$(UV) run pytest -s --junit-xml=./reports/$(CONNECT_VERSION).xml | \
141142
tee ./logs/$(CONNECT_VERSION).log;

integration/tests/posit/connect/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44

55
client = connect.Client()
66
CONNECT_VERSION = version.parse(client.version)
7+
print(CONNECT_VERSION)
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from pathlib import Path
2+
3+
import pytest
4+
from packaging import version
5+
6+
from posit import connect
7+
8+
from . import CONNECT_VERSION
9+
10+
11+
@pytest.mark.skipif(
12+
CONNECT_VERSION <= version.parse("2024.09.0"),
13+
reason="Packages API unavailable",
14+
)
15+
class TestPackages:
16+
@classmethod
17+
def setup_class(cls):
18+
cls.client = connect.Client()
19+
cls.content = cls.client.content.create(name=cls.__name__)
20+
path = Path("../../../resources/connect/bundles/example-flask-minimal/bundle.tar.gz")
21+
path = (Path(__file__).parent / path).resolve()
22+
bundle = cls.content.bundles.create(str(path))
23+
task = bundle.deploy()
24+
task.wait_for()
25+
26+
@classmethod
27+
def teardown_class(cls):
28+
cls.content.delete()
29+
30+
def test(self):
31+
# assert self.client.packages
32+
assert self.content.packages
33+
34+
def test_find_by(self):
35+
# package = self.client.packages.find_by(name="flask")
36+
# assert package
37+
# assert package["name"] == "flask"
38+
39+
package = self.content.packages.find_by(name="flask")
40+
assert package
41+
assert package["name"] == "flask"

src/posit/connect/client.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from .groups import Groups
1515
from .metrics import Metrics
1616
from .oauth import OAuth
17-
from .packages import GlobalPackages
17+
from .packages import Packages
1818
from .resources import ResourceParameters
1919
from .tasks import Tasks
2020
from .users import User, Users
@@ -271,8 +271,9 @@ def oauth(self) -> OAuth:
271271
return OAuth(self.resource_params, self.cfg.api_key)
272272

273273
@property
274-
def packages(self) -> GlobalPackages:
275-
return GlobalPackages(self._ctx, "v1/packages")
274+
@requires(version="2024.10.0-dev")
275+
def packages(self) -> Packages:
276+
return Packages(self._ctx, "v1/packages")
276277

277278
@property
278279
def vanities(self) -> Vanities:

src/posit/connect/content.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from .env import EnvVars
1414
from .jobs import JobsMixin
1515
from .oauth.associations import ContentItemAssociations
16-
from .packages.packages import PackagesMixin
16+
from .packages import ContentPackagesMixin as PackagesMixin
1717
from .permissions import Permissions
1818
from .resources import Resource, ResourceParameters, Resources
1919
from .vanities import VanityMixin

src/posit/connect/packages.py

Lines changed: 54 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
from __future__ import annotations
2+
13
import posixpath
2-
from typing import Literal, Optional, TypedDict, overload
4+
from typing import List, Literal, Optional, TypedDict, overload
35

46
from typing_extensions import NotRequired, Required, Unpack
57

68
from posit.connect.context import requires
9+
from posit.connect.errors import ClientError
710
from posit.connect.paginator import Paginator
811

912
from .resources import Active, ActiveFinderMethods, ActiveSequence
@@ -30,6 +33,14 @@ def __init__(self, ctx, path):
3033
def _create_instance(self, path, /, **attributes):
3134
return ContentPackage(self._ctx, **attributes)
3235

36+
def fetch(self, **conditions):
37+
try:
38+
return super().fetch(**conditions)
39+
except ClientError as e:
40+
if e.http_status == 204:
41+
return []
42+
raise e
43+
3344
def find(self, uid):
3445
raise NotImplementedError("The 'find' method is not support by the Packages API.")
3546

@@ -87,30 +98,53 @@ class ContentPackagesMixin(Active):
8798
"""Mixin class to add a packages attribute."""
8899

89100
@property
90-
@requires(version="2024.11.0")
101+
@requires(version="2024.10.0-dev")
91102
def packages(self):
92103
path = posixpath.join(self._path, "packages")
93104
return ContentPackages(self._ctx, path)
94105

95106

96-
class GlobalPackage(Active):
97-
class _GlobalPackage(TypedDict):
98-
language: Required[str]
107+
class Package(Active):
108+
class _Package(TypedDict):
109+
language: Required[Literal["python", "r"]]
110+
"""Programming language ecosystem, options are 'python' and 'r'"""
111+
99112
name: Required[str]
113+
"""The package name"""
114+
100115
version: Required[str]
116+
"""The package version"""
117+
101118
hash: Required[Optional[str]]
119+
"""Package description hash for R packages."""
120+
121+
bundle_id: Required[str]
122+
"""The unique identifier of the bundle this package is associated with"""
102123

103-
def __init__(self, ctx, /, **attributes: Unpack[_GlobalPackage]):
124+
app_id: Required[str]
125+
"""The numerical identifier of the application this package is associated with"""
126+
127+
app_guid: Required[str]
128+
"""The numerical identifier of the application this package is associated with"""
129+
130+
def __init__(self, ctx, /, **attributes: Unpack[_Package]):
104131
# todo - passing "" is a hack since path isn't needed. Instead, this class should inherit from Resource, but ActiveSequence is designed to operate on Active. That should change.
105132
super().__init__(ctx, "", **attributes)
106133

107134

108-
class GlobalPackages(ContentPackages):
135+
class Packages(ActiveFinderMethods["Package"], ActiveSequence["Package"]):
109136
def __init__(self, ctx, path):
110137
super().__init__(ctx, path, "name")
111138

112139
def _create_instance(self, path, /, **attributes):
113-
return ContentPackage(self._ctx, **attributes)
140+
return Package(self._ctx, **attributes)
141+
142+
def fetch(self, **conditions) -> List["Package"]:
143+
# todo - add pagination support to ActiveSequence
144+
url = self._ctx.url + self._path
145+
paginator = Paginator(self._ctx.session, url, conditions)
146+
results = paginator.fetch_results()
147+
return [self._create_instance("", **result) for result in results]
114148

115149
def find(self, uid):
116150
raise NotImplementedError("The 'find' method is not support by the Packages API.")
@@ -128,14 +162,17 @@ class _FindBy(TypedDict, total=False):
128162
hash: NotRequired[Optional[str]]
129163
"""Package description hash for R packages."""
130164

131-
def fetch(self, **conditions):
132-
url = self._ctx.url + self._path
133-
paginator = Paginator(self._ctx.session, url, conditions)
134-
results = paginator.fetch_results()
135-
return [self._create_instance("", **result) for result in results]
165+
bundle_id: NotRequired[str]
166+
"""The unique identifier of the bundle this package is associated with"""
167+
168+
app_id: NotRequired[str]
169+
"""The numerical identifier of the application this package is associated with"""
170+
171+
app_guid: NotRequired[str]
172+
"""The numerical identifier of the application this package is associated with"""
136173

137174
@overload
138-
def find_by(self, **conditions: Unpack[_FindBy]):
175+
def find_by(self, **conditions: Unpack[_FindBy]) -> "Package | None":
139176
"""
140177
Find the first record matching the specified conditions.
141178
@@ -160,12 +197,12 @@ def find_by(self, **conditions: Unpack[_FindBy]):
160197
161198
Returns
162199
-------
163-
Optional[T]
200+
Optional[Package]
164201
The first record matching the specified conditions, or `None` if no such record exists.
165202
"""
166203

167204
@overload
168-
def find_by(self, **conditions): ...
205+
def find_by(self, **conditions) -> "Package | None": ...
169206

170-
def find_by(self, **conditions):
207+
def find_by(self, **conditions) -> "Package | None":
171208
return super().find_by(**conditions)

src/posit/connect/resources.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44
import warnings
55
from abc import ABC, abstractmethod
66
from dataclasses import dataclass
7-
from typing import Any, Generic, List, Optional, Sequence, TypeVar, overload
7+
from typing import TYPE_CHECKING, Any, Generic, List, Optional, Sequence, TypeVar, overload
88

9-
import requests
109
from typing_extensions import Self
1110

12-
from .context import Context
13-
from .urls import Url
11+
if TYPE_CHECKING:
12+
import requests
13+
14+
from .context import Context
15+
from .urls import Url
1416

1517

1618
@dataclass(frozen=True)

0 commit comments

Comments
 (0)