Skip to content

Commit 3bed626

Browse files
committed
feat: adds packages
1 parent 173e634 commit 3bed626

File tree

7 files changed

+154
-71
lines changed

7 files changed

+154
-71
lines changed

integration/Makefile

Lines changed: 47 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ CONNECT_BOOTSTRAP_SECRETKEY ?= $(shell head -c 32 /dev/random | base64)
1111
.DEFAULT_GOAL := latest
1212

1313
.PHONY: $(CONNECT_VERSIONS) \
14-
all \
15-
build \
14+
all \
15+
build \
1616
down \
1717
down-% \
1818
latest \
@@ -22,8 +22,9 @@ CONNECT_BOOTSTRAP_SECRETKEY ?= $(shell head -c 32 /dev/random | base64)
2222
help
2323

2424
# Versions
25-
CONNECT_VERSIONS := 2024.08.0 \
26-
2024.06.0 \
25+
CONNECT_VERSIONS := 2024.09.0 \
26+
2024.08.0 \
27+
2024.06.0 \
2728
2024.05.0 \
2829
2024.04.1 \
2930
2024.04.0 \
@@ -42,8 +43,8 @@ CONNECT_VERSIONS := 2024.08.0 \
4243
2022.11.0
4344

4445
clean:
45-
rm -rf logs reports
46-
find . -type d -empty -delete
46+
rm -rf logs reports
47+
find . -type d -empty -delete
4748

4849
# Run test suite for a specific Connect version.
4950
#
@@ -60,19 +61,19 @@ all: $(CONNECT_VERSIONS:%=%) preview
6061

6162
# Run test suite against latest Connect version.
6263
latest:
63-
$(MAKE) $(firstword $(CONNECT_VERSIONS))
64+
$(MAKE) $(firstword $(CONNECT_VERSIONS))
6465

6566
# Run test suite against preview Connect version.
6667
preview:
67-
$(MAKE) \
68-
DOCKER_CONNECT_IMAGE=rstudio/rstudio-connect-preview \
69-
DOCKER_CONNECT_IMAGE_TAG=dev-jammy-daily \
70-
down-preview up-preview
68+
$(MAKE) \
69+
DOCKER_CONNECT_IMAGE=rstudio/rstudio-connect-preview \
70+
DOCKER_CONNECT_IMAGE_TAG=dev-jammy-daily \
71+
down-preview up-preview
7172

7273
# Build Dockerfile
7374
build:
74-
make -C .. $(UV_LOCK)
75-
docker build -t $(DOCKER_PROJECT_IMAGE_TAG) ..
75+
make -C .. $(UV_LOCK)
76+
docker build -t $(DOCKER_PROJECT_IMAGE_TAG) ..
7677

7778
# Tear down resources.
7879
#
@@ -86,12 +87,12 @@ down: $(CONNECT_VERSIONS:%=down-%)
8687
down-%: DOCKER_CONNECT_IMAGE_TAG=jammy-$*
8788
down-%: CONNECT_VERSION=$*
8889
down-%:
89-
CONNECT_BOOTSTRAP_SECRETKEY=$(CONNECT_BOOTSTRAP_SECRETKEY) \
90-
DOCKER_CONNECT_IMAGE=$(DOCKER_CONNECT_IMAGE) \
91-
DOCKER_CONNECT_IMAGE_TAG=$(DOCKER_CONNECT_IMAGE_TAG) \
92-
CONNECT_VERSION=$* \
93-
DOCKER_PROJECT_IMAGE_TAG=$(DOCKER_PROJECT_IMAGE_TAG) \
94-
$(DOCKER_COMPOSE) -p $(PROJECT_NAME)-$(subst .,-,$(CONNECT_VERSION)) down -v
90+
CONNECT_BOOTSTRAP_SECRETKEY=$(CONNECT_BOOTSTRAP_SECRETKEY) \
91+
DOCKER_CONNECT_IMAGE=$(DOCKER_CONNECT_IMAGE) \
92+
DOCKER_CONNECT_IMAGE_TAG=$(DOCKER_CONNECT_IMAGE_TAG) \
93+
CONNECT_VERSION=$* \
94+
DOCKER_PROJECT_IMAGE_TAG=$(DOCKER_PROJECT_IMAGE_TAG) \
95+
$(DOCKER_COMPOSE) -p $(PROJECT_NAME)-$(subst .,-,$(CONNECT_VERSION)) down -v
9596

9697
# Create, start, and run Docker Compose.
9798
#
@@ -103,39 +104,39 @@ up: $(CONNECT_VERSIONS:%=up-%)
103104
up-%: CONNECT_VERSION=$*
104105
up-%: DOCKER_CONNECT_IMAGE_TAG=jammy-$*
105106
up-%: build
106-
CONNECT_BOOTSTRAP_SECRETKEY=$(CONNECT_BOOTSTRAP_SECRETKEY) \
107-
DOCKER_CONNECT_IMAGE=$(DOCKER_CONNECT_IMAGE) \
108-
DOCKER_CONNECT_IMAGE_TAG=$(DOCKER_CONNECT_IMAGE_TAG) \
109-
CONNECT_VERSION=$* \
110-
DOCKER_PROJECT_IMAGE_TAG=$(DOCKER_PROJECT_IMAGE_TAG) \
111-
$(DOCKER_COMPOSE) -p $(PROJECT_NAME)-$(subst .,-,$(CONNECT_VERSION)) up -V --abort-on-container-exit --no-build
107+
CONNECT_BOOTSTRAP_SECRETKEY=$(CONNECT_BOOTSTRAP_SECRETKEY) \
108+
DOCKER_CONNECT_IMAGE=$(DOCKER_CONNECT_IMAGE) \
109+
DOCKER_CONNECT_IMAGE_TAG=$(DOCKER_CONNECT_IMAGE_TAG) \
110+
CONNECT_VERSION=$* \
111+
DOCKER_PROJECT_IMAGE_TAG=$(DOCKER_PROJECT_IMAGE_TAG) \
112+
$(DOCKER_COMPOSE) -p $(PROJECT_NAME)-$(subst .,-,$(CONNECT_VERSION)) up -V --abort-on-container-exit --no-build
112113

113114
# Show help message.
114115
help:
115-
@echo "Makefile Targets:"
116-
@echo " all (default) Run test suite for all Connect versions."
117-
@echo " latest Run test suite for latest Connect version."
118-
@echo " preview Run test suite for preview Connect version."
119-
@echo " <version> Run test suite for the specified Connect version. (e.g., make 2024.05.0)"
120-
@echo " up Start Docker Compose for all Connect version."
121-
@echo " down Tear down Docker resources for all Connect versions."
122-
@echo " clean Clean up the project directory."
123-
@echo " help Show this help message."
124-
@echo
125-
@echo "Common Usage:"
126-
@echo " make -j 4 Run test suite in parallel for all Connect versions."
127-
@echo " make latest Run test suite for latest Connect version."
128-
@echo " make preview Run test suite for preview Connect version."
129-
@echo " make 2024.05.0 Run test suite for specific Connect version."
116+
@echo "Makefile Targets:"
117+
@echo " all (default) Run test suite for all Connect versions."
118+
@echo " latest Run test suite for latest Connect version."
119+
@echo " preview Run test suite for preview Connect version."
120+
@echo " <version> Run test suite for the specified Connect version. (e.g., make 2024.05.0)"
121+
@echo " up Start Docker Compose for all Connect version."
122+
@echo " down Tear down Docker resources for all Connect versions."
123+
@echo " clean Clean up the project directory."
124+
@echo " help Show this help message."
125+
@echo
126+
@echo "Common Usage:"
127+
@echo " make -j 4 Run test suite in parallel for all Connect versions."
128+
@echo " make latest Run test suite for latest Connect version."
129+
@echo " make preview Run test suite for preview Connect version."
130+
@echo " make 2024.05.0 Run test suite for specific Connect version."
130131

131132
# Run tests.
132133
#
133134
# Typically call from docker-compose.yaml. Assumes Connect server is running
134135
# on local network.
135136
test:
136-
mkdir -p logs
137-
set -o pipefail; \
138-
CONNECT_VERSION=${CONNECT_VERSION} \
139-
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-
tee ./logs/$(CONNECT_VERSION).log;
137+
mkdir -p logs
138+
set -o pipefail; \
139+
CONNECT_VERSION=${CONNECT_VERSION} \
140+
CONNECT_API_KEY="$(shell $(UV) run rsconnect bootstrap -i -s http://connect:3939 --raw)" \
141+
$(UV) run pytest -s --junit-xml=./reports/$(CONNECT_VERSION).xml | \
142+
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)