Skip to content

Commit

Permalink
Merge branch 'main' into client_requests
Browse files Browse the repository at this point in the history
  • Loading branch information
schloerke authored Dec 13, 2024
2 parents a4d51c1 + f709cc1 commit 1ab80f6
Show file tree
Hide file tree
Showing 14 changed files with 611 additions and 510 deletions.
3 changes: 2 additions & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# This file contains the configuration settings for the coverage report generated.

[report]
# exclude '...' (ellipsis literal). This option uses regex, so an escaped literal is required.
exclude_also =
\.\.\.
exclude_lines =
if TYPE_CHECKING:

fail_under = 80
40 changes: 40 additions & 0 deletions integration/tests/posit/connect/test_environments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import pytest
from packaging import version

from posit import connect

from . import CONNECT_VERSION


@pytest.mark.skipif(
CONNECT_VERSION < version.parse("2023.05.0"),
reason="Environments API unavailable",
)
class TestEnvironments:
@classmethod
def setup_class(cls):
cls.client = connect.Client()
cls.environment = cls.client.environments.create(
title="title",
name="name",
cluster_name="Kubernetes",
)

@classmethod
def teardown_class(cls):
cls.environment.destroy()
assert len(cls.client.environments) == 0

def test_find(self):
uid = self.environment["guid"]
environment = self.client.environments.find(uid)
assert environment == self.environment

def test_find_by(self):
environment = self.client.environments.find_by(name="name")
assert environment == self.environment

def test_update(self):
assert self.environment["title"] == "title"
self.environment.update(title="new-title")
assert self.environment["title"] == "new-title"
4 changes: 4 additions & 0 deletions integration/tests/posit/connect/test_jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,7 @@ def test_find_by(self):

jobs = content.jobs
assert len(jobs) != 0

job = jobs[0]
key = job["key"]
assert content.jobs.find_by(key=key) == job
23 changes: 15 additions & 8 deletions src/posit/connect/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,10 @@

from __future__ import annotations

from typing import overload
from typing import TYPE_CHECKING, overload

from requests import Response, Session

from posit.connect.tags import Tags

from . import hooks, me
from .auth import Auth
from .config import Config
Expand All @@ -16,12 +14,16 @@
from .groups import Groups
from .metrics import Metrics
from .oauth import OAuth
from .packages import Packages
from .resources import ResourceParameters
from .resources import ResourceParameters, _PaginatedResourceSequence, _ResourceSequence
from .tags import Tags
from .tasks import Tasks
from .users import User, Users
from .vanities import Vanities

if TYPE_CHECKING:
from .environments import Environments
from .packages import _Packages


class Client(ContextManager):
"""
Expand Down Expand Up @@ -295,14 +297,19 @@ def oauth(self) -> OAuth:
return OAuth(self.resource_params, self.cfg.api_key)

@property
@requires(version="2024.10.0-dev")
def packages(self) -> Packages:
return Packages(self._ctx, "v1/packages")
@requires(version="2024.11.0")
def packages(self) -> _Packages:
return _PaginatedResourceSequence(self._ctx, "v1/packages", uid="name")

@property
def vanities(self) -> Vanities:
return Vanities(self.resource_params)

@property
@requires(version="2023.05.0")
def environments(self) -> Environments:
return _ResourceSequence(self._ctx, "v1/environments")

def __del__(self):
"""Close the session when the Client instance is deleted."""
if hasattr(self, "session") and self.session is not None:
Expand Down
20 changes: 16 additions & 4 deletions src/posit/connect/content.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,20 @@
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 .jobs import JobsMixin
from .oauth.associations import ContentItemAssociations
from .packages import ContentPackagesMixin as PackagesMixin
from .permissions import Permissions
from .resources import Resource, ResourceParameters, Resources
from .resources import Active, Resource, ResourceParameters, Resources, _ResourceSequence
from .tags import ContentItemTags
from .vanities import VanityMixin
from .variants import Variants

if TYPE_CHECKING:
from .context import Context
from .jobs import Jobs
from .packages import _ContentPackages
from .tasks import Task


Expand Down Expand Up @@ -174,7 +175,7 @@ class ContentItemOwner(Resource):
pass


class ContentItem(JobsMixin, PackagesMixin, VanityMixin, Resource):
class ContentItem(Active, VanityMixin, Resource):
class _AttrsBase(TypedDict, total=False):
# # `name` will be set by other _Attrs classes
# name: str
Expand Down Expand Up @@ -508,6 +509,17 @@ def tags(self) -> ContentItemTags:
content_guid=self["guid"],
)

@property
def jobs(self) -> Jobs:
path = posixpath.join(self._path, "jobs")
return _ResourceSequence(self._ctx, path, uid="key")

@property
@requires(version="2024.11.0")
def packages(self) -> _ContentPackages:
path = posixpath.join(self._path, "packages")
return _ResourceSequence(self._ctx, path, uid="name")


class Content(Resources):
"""Content resource.
Expand Down
220 changes: 220 additions & 0 deletions src/posit/connect/environments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
from __future__ import annotations

from abc import abstractmethod
from collections.abc import Mapping, Sized

from typing_extensions import (
Any,
List,
Literal,
Protocol,
SupportsIndex,
TypedDict,
overload,
runtime_checkable,
)

MatchingType = Literal["any", "exact", "none"]
"""Directions for how environments are considered for selection.
- any: The image may be selected by Connect if not defined in the bundle manifest.
- exact: The image must be defined in the bundle manifest
- none: Never use this environment
"""


class Installation(TypedDict):
"""Interpreter installation in an execution environment."""

path: str
"""The absolute path to the interpreter's executable."""

version: str
"""The semantic version of the interpreter."""


class Installations(TypedDict):
"""Interpreter installations in an execution environment."""

installations: List[Installation]
"""Interpreter installations in an execution environment."""


class Environment(Mapping[str, Any]):
@abstractmethod
def destroy(self) -> None:
"""Destroy the environment.
Warnings
--------
This operation is irreversible.
Note
----
This action requires administrator privileges.
"""

@abstractmethod
def update(
self,
*,
title: str,
description: str | None = ...,
matching: MatchingType | None = ...,
supervisor: str | None = ...,
python: Installations | None = ...,
quarto: Installations | None = ...,
r: Installations | None = ...,
tensorflow: Installations | None = ...,
) -> None:
"""Update the environment.
Parameters
----------
title : str
A human-readable title.
description : str | None, optional, not required
A human-readable description.
matching : MatchingType, optional, not required
Directions for how the environment is considered for selection
supervisor : str | None, optional, not required
Path to the supervisor script.
python : Installations, optional, not required
The Python installations available in this environment
quarto : Installations, optional, not required
The Quarto installations available in this environment
r : Installations, optional, not required
The R installations available in this environment
tensorflow : Installations, optional, not required
The Tensorflow installations available in this environment
Note
----
This action requires administrator privileges.
"""


@runtime_checkable
class Environments(Sized, Protocol):
@overload
def __getitem__(self, index: SupportsIndex) -> Environment: ...

@overload
def __getitem__(self, index: slice) -> List[Environment]: ...

def create(
self,
*,
title: str,
name: str,
cluster_name: str | Literal["Kubernetes"],
matching: MatchingType = "any",
description: str | None = ...,
supervisor: str | None = ...,
python: Installations | None = ...,
quarto: Installations | None = ...,
r: Installations | None = ...,
tensorflow: Installations | None = ...,
) -> Environment:
"""Create an environment.
Parameters
----------
title : str
A human-readable title.
name : str
The container image name used for execution in this environment.
cluster_name : str | Literal["Kubernetes"]
The cluster identifier for this environment. Defaults to "Kubernetes" when Off-Host-Execution is enabled.
description : str, optional
A human-readable description.
matching : MatchingType
Directions for how the environment is considered for selection, by default is "any".
supervisor : str, optional
Path to the supervisor script
python : Installations, optional
The Python installations available in this environment
quarto : Installations, optional
The Quarto installations available in this environment
r : Installations, optional
The R installations available in this environment
tensorflow : Installations, optional
The Tensorflow installations available in this environment
Returns
-------
Environment
Note
----
This action requires administrator privileges.
"""
...

def find(self, guid: str, /) -> Environment: ...

def find_by(
self,
*,
id: str = ..., # noqa: A002
guid: str = ...,
created_time: str = ...,
updated_time: str = ...,
title: str = ...,
name: str = ...,
description: str | None = ...,
cluster_name: str | Literal["Kubernetes"] = ...,
environment_type: str | Literal["Kubernetes"] = ...,
matching: MatchingType = ...,
supervisor: str | None = ...,
python: Installations | None = ...,
quarto: Installations | None = ...,
r: Installations | None = ...,
tensorflow: Installations | None = ...,
) -> Environment | None:
"""Find the first record matching the specified conditions.
There is no implied ordering, so if order matters, you should specify it yourself.
Parameters
----------
id : str
The numerical identifier.
guid : str
The unique identifier.
created_time : str
The timestamp (RFC3339) when the environment was created.
updated_time : str
The timestamp (RFC3339) when the environment was updated.
title : str
A human-readable title.
name : str
The container image name used for execution in this environment.
description : str, optional
A human-readable description.
cluster_name : str | Literal["Kubernetes"]
The cluster identifier for this environment. Defaults to "Kubernetes" when Off-Host-Execution is enabled.
environment_type : str | Literal["Kubernetes"]
The cluster environment type. Defaults to "Kubernetes" when Off-Host-Execution is enabled.
matching : MatchingType
Directions for how the environment is considered for selection.
supervisor : str, optional
Path to the supervisor script
python : Installations, optional
The Python installations available in this environment
quarto : Installations, optional
The Quarto installations available in this environment
r : Installations, optional
The R installations available in this environment
tensorflow : Installations, optional
The Tensorflow installations available in this environment
Returns
-------
Environment | None
Note
----
This action requires administrator or publisher privileges.
"""
...
Loading

0 comments on commit 1ab80f6

Please sign in to comment.