Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: adds packages
Browse files Browse the repository at this point in the history
tdstein committed Nov 7, 2024
1 parent e57498f commit 7744523
Showing 7 changed files with 110 additions and 27 deletions.
5 changes: 3 additions & 2 deletions integration/Makefile
Original file line number Diff line number Diff line change
@@ -22,7 +22,8 @@ CONNECT_BOOTSTRAP_SECRETKEY ?= $(shell head -c 32 /dev/random | base64)
help

# Versions
CONNECT_VERSIONS := 2024.08.0 \
CONNECT_VERSIONS := 2024.09.0 \
2024.08.0 \
2024.06.0 \
2024.05.0 \
2024.04.1 \
@@ -137,5 +138,5 @@ test:
set -o pipefail; \
CONNECT_VERSION=${CONNECT_VERSION} \
CONNECT_API_KEY="$(shell $(UV) run rsconnect bootstrap -i -s http://connect:3939 --raw)" \
$(UV) run pytest -s -k TestJobs --junit-xml=./reports/$(CONNECT_VERSION).xml | \
$(UV) run pytest -s --junit-xml=./reports/$(CONNECT_VERSION).xml | \
tee ./logs/$(CONNECT_VERSION).log;
1 change: 1 addition & 0 deletions integration/tests/posit/connect/__init__.py
Original file line number Diff line number Diff line change
@@ -4,3 +4,4 @@

client = connect.Client()
CONNECT_VERSION = version.parse(client.version)
print(CONNECT_VERSION)
41 changes: 41 additions & 0 deletions integration/tests/posit/connect/test_packages.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from pathlib import Path

import pytest
from packaging import version

from posit import connect

from . import CONNECT_VERSION


@pytest.mark.skipif(
CONNECT_VERSION <= version.parse("2024.09.0"),
reason="Packages API unavailable",
)
class TestPackages:
@classmethod
def setup_class(cls):
cls.client = connect.Client()
cls.content = cls.client.content.create(name=cls.__name__)
path = Path("../../../resources/connect/bundles/example-flask-minimal/bundle.tar.gz")
path = (Path(__file__).parent / path).resolve()
bundle = cls.content.bundles.create(str(path))
task = bundle.deploy()
task.wait_for()

@classmethod
def teardown_class(cls):
cls.content.delete()

def test(self):
# 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.content.packages.find_by(name="flask")
assert package
assert package["name"] == "flask"
7 changes: 4 additions & 3 deletions src/posit/connect/client.py
Original file line number Diff line number Diff line change
@@ -14,7 +14,7 @@
from .groups import Groups
from .metrics import Metrics
from .oauth import OAuth
from .packages import GlobalPackages
from .packages import Packages
from .resources import ResourceParameters
from .tasks import Tasks
from .users import User, Users
@@ -271,8 +271,9 @@ def oauth(self) -> OAuth:
return OAuth(self.resource_params, self.cfg.api_key)

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

@property
def vanities(self) -> Vanities:
2 changes: 1 addition & 1 deletion src/posit/connect/content.py
Original file line number Diff line number Diff line change
@@ -13,7 +13,7 @@
from .env import EnvVars
from .jobs import JobsMixin
from .oauth.associations import ContentItemAssociations
from .packages.packages import PackagesMixin
from .packages import ContentPackagesMixin as PackagesMixin
from .permissions import Permissions
from .resources import Resource, ResourceParameters, Resources
from .vanities import VanityMixin
71 changes: 54 additions & 17 deletions src/posit/connect/packages.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
from __future__ import annotations

import posixpath
from typing import Literal, Optional, TypedDict, overload
from typing import List, Literal, Optional, TypedDict, overload

from typing_extensions import NotRequired, Required, Unpack

from posit.connect.context import requires
from posit.connect.errors import ClientError
from posit.connect.paginator import Paginator

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

def fetch(self, **conditions):
try:
return super().fetch(**conditions)
except ClientError as e:
if e.http_status == 204:
return []
raise e

def find(self, uid):
raise NotImplementedError("The 'find' method is not support by the Packages API.")

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

@property
@requires(version="2024.11.0")
@requires(version="2024.10.0-dev")
def packages(self):
path = posixpath.join(self._path, "packages")
return ContentPackages(self._ctx, path)


class GlobalPackage(Active):
class _GlobalPackage(TypedDict):
language: Required[str]
class Package(Active):
class _Package(TypedDict):
language: Required[Literal["python", "r"]]
"""Programming language ecosystem, options are 'python' and 'r'"""

name: Required[str]
"""The package name"""

version: Required[str]
"""The package version"""

hash: Required[Optional[str]]
"""Package description hash for R packages."""

bundle_id: Required[str]
"""The unique identifier of the bundle this package is associated with"""

def __init__(self, ctx, /, **attributes: Unpack[_GlobalPackage]):
app_id: Required[str]
"""The numerical identifier of the application this package is associated with"""

app_guid: Required[str]
"""The numerical identifier of the application this package is associated with"""

def __init__(self, ctx, /, **attributes: Unpack[_Package]):
# 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.
super().__init__(ctx, "", **attributes)


class GlobalPackages(ContentPackages):
class Packages(ActiveFinderMethods["Package"], ActiveSequence["Package"]):
def __init__(self, ctx, path):
super().__init__(ctx, path, "name")

def _create_instance(self, path, /, **attributes):
return ContentPackage(self._ctx, **attributes)
return Package(self._ctx, **attributes)

def fetch(self, **conditions) -> List["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]

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

def fetch(self, **conditions):
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]
bundle_id: NotRequired[str]
"""The unique identifier of the bundle this package is associated with"""

app_id: NotRequired[str]
"""The numerical identifier of the application this package is associated with"""

app_guid: NotRequired[str]
"""The numerical identifier of the application this package is associated with"""

@overload
def find_by(self, **conditions: Unpack[_FindBy]):
def find_by(self, **conditions: Unpack[_FindBy]) -> "Package | None":
"""
Find the first record matching the specified conditions.
@@ -160,12 +197,12 @@ def find_by(self, **conditions: Unpack[_FindBy]):
Returns
-------
Optional[T]
Optional[Package]
The first record matching the specified conditions, or `None` if no such record exists.
"""

@overload
def find_by(self, **conditions): ...
def find_by(self, **conditions) -> "Package | None": ...

def find_by(self, **conditions):
def find_by(self, **conditions) -> "Package | None":
return super().find_by(**conditions)
10 changes: 6 additions & 4 deletions src/posit/connect/resources.py
Original file line number Diff line number Diff line change
@@ -4,13 +4,15 @@
import warnings
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Any, Generic, List, Optional, Sequence, TypeVar, overload
from typing import TYPE_CHECKING, Any, Generic, List, Optional, Sequence, TypeVar, overload

import requests
from typing_extensions import Self

from .context import Context
from .urls import Url
if TYPE_CHECKING:
import requests

from .context import Context
from .urls import Url


@dataclass(frozen=True)

0 comments on commit 7744523

Please sign in to comment.