Skip to content

Commit 1ab80f6

Browse files
authored
Merge branch 'main' into client_requests
2 parents a4d51c1 + f709cc1 commit 1ab80f6

File tree

14 files changed

+611
-510
lines changed

14 files changed

+611
-510
lines changed

.coveragerc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
# This file contains the configuration settings for the coverage report generated.
22

33
[report]
4-
# exclude '...' (ellipsis literal). This option uses regex, so an escaped literal is required.
54
exclude_also =
65
\.\.\.
6+
exclude_lines =
7+
if TYPE_CHECKING:
78

89
fail_under = 80
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import pytest
2+
from packaging import version
3+
4+
from posit import connect
5+
6+
from . import CONNECT_VERSION
7+
8+
9+
@pytest.mark.skipif(
10+
CONNECT_VERSION < version.parse("2023.05.0"),
11+
reason="Environments API unavailable",
12+
)
13+
class TestEnvironments:
14+
@classmethod
15+
def setup_class(cls):
16+
cls.client = connect.Client()
17+
cls.environment = cls.client.environments.create(
18+
title="title",
19+
name="name",
20+
cluster_name="Kubernetes",
21+
)
22+
23+
@classmethod
24+
def teardown_class(cls):
25+
cls.environment.destroy()
26+
assert len(cls.client.environments) == 0
27+
28+
def test_find(self):
29+
uid = self.environment["guid"]
30+
environment = self.client.environments.find(uid)
31+
assert environment == self.environment
32+
33+
def test_find_by(self):
34+
environment = self.client.environments.find_by(name="name")
35+
assert environment == self.environment
36+
37+
def test_update(self):
38+
assert self.environment["title"] == "title"
39+
self.environment.update(title="new-title")
40+
assert self.environment["title"] == "new-title"

integration/tests/posit/connect/test_jobs.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,7 @@ def test_find_by(self):
5151

5252
jobs = content.jobs
5353
assert len(jobs) != 0
54+
55+
job = jobs[0]
56+
key = job["key"]
57+
assert content.jobs.find_by(key=key) == job

src/posit/connect/client.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,10 @@
22

33
from __future__ import annotations
44

5-
from typing import overload
5+
from typing import TYPE_CHECKING, overload
66

77
from requests import Response, Session
88

9-
from posit.connect.tags import Tags
10-
119
from . import hooks, me
1210
from .auth import Auth
1311
from .config import Config
@@ -16,12 +14,16 @@
1614
from .groups import Groups
1715
from .metrics import Metrics
1816
from .oauth import OAuth
19-
from .packages import Packages
20-
from .resources import ResourceParameters
17+
from .resources import ResourceParameters, _PaginatedResourceSequence, _ResourceSequence
18+
from .tags import Tags
2119
from .tasks import Tasks
2220
from .users import User, Users
2321
from .vanities import Vanities
2422

23+
if TYPE_CHECKING:
24+
from .environments import Environments
25+
from .packages import _Packages
26+
2527

2628
class Client(ContextManager):
2729
"""
@@ -295,14 +297,19 @@ def oauth(self) -> OAuth:
295297
return OAuth(self.resource_params, self.cfg.api_key)
296298

297299
@property
298-
@requires(version="2024.10.0-dev")
299-
def packages(self) -> Packages:
300-
return Packages(self._ctx, "v1/packages")
300+
@requires(version="2024.11.0")
301+
def packages(self) -> _Packages:
302+
return _PaginatedResourceSequence(self._ctx, "v1/packages", uid="name")
301303

302304
@property
303305
def vanities(self) -> Vanities:
304306
return Vanities(self.resource_params)
305307

308+
@property
309+
@requires(version="2023.05.0")
310+
def environments(self) -> Environments:
311+
return _ResourceSequence(self._ctx, "v1/environments")
312+
306313
def __del__(self):
307314
"""Close the session when the Client instance is deleted."""
308315
if hasattr(self, "session") and self.session is not None:

src/posit/connect/content.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,20 @@
2020
from . import tasks
2121
from ._api import ApiDictEndpoint, JsonifiableDict
2222
from .bundles import Bundles
23+
from .context import requires
2324
from .env import EnvVars
2425
from .errors import ClientError
25-
from .jobs import JobsMixin
2626
from .oauth.associations import ContentItemAssociations
27-
from .packages import ContentPackagesMixin as PackagesMixin
2827
from .permissions import Permissions
29-
from .resources import Resource, ResourceParameters, Resources
28+
from .resources import Active, Resource, ResourceParameters, Resources, _ResourceSequence
3029
from .tags import ContentItemTags
3130
from .vanities import VanityMixin
3231
from .variants import Variants
3332

3433
if TYPE_CHECKING:
3534
from .context import Context
35+
from .jobs import Jobs
36+
from .packages import _ContentPackages
3637
from .tasks import Task
3738

3839

@@ -174,7 +175,7 @@ class ContentItemOwner(Resource):
174175
pass
175176

176177

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

512+
@property
513+
def jobs(self) -> Jobs:
514+
path = posixpath.join(self._path, "jobs")
515+
return _ResourceSequence(self._ctx, path, uid="key")
516+
517+
@property
518+
@requires(version="2024.11.0")
519+
def packages(self) -> _ContentPackages:
520+
path = posixpath.join(self._path, "packages")
521+
return _ResourceSequence(self._ctx, path, uid="name")
522+
511523

512524
class Content(Resources):
513525
"""Content resource.

src/posit/connect/environments.py

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
from __future__ import annotations
2+
3+
from abc import abstractmethod
4+
from collections.abc import Mapping, Sized
5+
6+
from typing_extensions import (
7+
Any,
8+
List,
9+
Literal,
10+
Protocol,
11+
SupportsIndex,
12+
TypedDict,
13+
overload,
14+
runtime_checkable,
15+
)
16+
17+
MatchingType = Literal["any", "exact", "none"]
18+
"""Directions for how environments are considered for selection.
19+
20+
- any: The image may be selected by Connect if not defined in the bundle manifest.
21+
- exact: The image must be defined in the bundle manifest
22+
- none: Never use this environment
23+
"""
24+
25+
26+
class Installation(TypedDict):
27+
"""Interpreter installation in an execution environment."""
28+
29+
path: str
30+
"""The absolute path to the interpreter's executable."""
31+
32+
version: str
33+
"""The semantic version of the interpreter."""
34+
35+
36+
class Installations(TypedDict):
37+
"""Interpreter installations in an execution environment."""
38+
39+
installations: List[Installation]
40+
"""Interpreter installations in an execution environment."""
41+
42+
43+
class Environment(Mapping[str, Any]):
44+
@abstractmethod
45+
def destroy(self) -> None:
46+
"""Destroy the environment.
47+
48+
Warnings
49+
--------
50+
This operation is irreversible.
51+
52+
Note
53+
----
54+
This action requires administrator privileges.
55+
"""
56+
57+
@abstractmethod
58+
def update(
59+
self,
60+
*,
61+
title: str,
62+
description: str | None = ...,
63+
matching: MatchingType | None = ...,
64+
supervisor: str | None = ...,
65+
python: Installations | None = ...,
66+
quarto: Installations | None = ...,
67+
r: Installations | None = ...,
68+
tensorflow: Installations | None = ...,
69+
) -> None:
70+
"""Update the environment.
71+
72+
Parameters
73+
----------
74+
title : str
75+
A human-readable title.
76+
description : str | None, optional, not required
77+
A human-readable description.
78+
matching : MatchingType, optional, not required
79+
Directions for how the environment is considered for selection
80+
supervisor : str | None, optional, not required
81+
Path to the supervisor script.
82+
python : Installations, optional, not required
83+
The Python installations available in this environment
84+
quarto : Installations, optional, not required
85+
The Quarto installations available in this environment
86+
r : Installations, optional, not required
87+
The R installations available in this environment
88+
tensorflow : Installations, optional, not required
89+
The Tensorflow installations available in this environment
90+
91+
Note
92+
----
93+
This action requires administrator privileges.
94+
"""
95+
96+
97+
@runtime_checkable
98+
class Environments(Sized, Protocol):
99+
@overload
100+
def __getitem__(self, index: SupportsIndex) -> Environment: ...
101+
102+
@overload
103+
def __getitem__(self, index: slice) -> List[Environment]: ...
104+
105+
def create(
106+
self,
107+
*,
108+
title: str,
109+
name: str,
110+
cluster_name: str | Literal["Kubernetes"],
111+
matching: MatchingType = "any",
112+
description: str | None = ...,
113+
supervisor: str | None = ...,
114+
python: Installations | None = ...,
115+
quarto: Installations | None = ...,
116+
r: Installations | None = ...,
117+
tensorflow: Installations | None = ...,
118+
) -> Environment:
119+
"""Create an environment.
120+
121+
Parameters
122+
----------
123+
title : str
124+
A human-readable title.
125+
name : str
126+
The container image name used for execution in this environment.
127+
cluster_name : str | Literal["Kubernetes"]
128+
The cluster identifier for this environment. Defaults to "Kubernetes" when Off-Host-Execution is enabled.
129+
description : str, optional
130+
A human-readable description.
131+
matching : MatchingType
132+
Directions for how the environment is considered for selection, by default is "any".
133+
supervisor : str, optional
134+
Path to the supervisor script
135+
python : Installations, optional
136+
The Python installations available in this environment
137+
quarto : Installations, optional
138+
The Quarto installations available in this environment
139+
r : Installations, optional
140+
The R installations available in this environment
141+
tensorflow : Installations, optional
142+
The Tensorflow installations available in this environment
143+
144+
Returns
145+
-------
146+
Environment
147+
148+
Note
149+
----
150+
This action requires administrator privileges.
151+
"""
152+
...
153+
154+
def find(self, guid: str, /) -> Environment: ...
155+
156+
def find_by(
157+
self,
158+
*,
159+
id: str = ..., # noqa: A002
160+
guid: str = ...,
161+
created_time: str = ...,
162+
updated_time: str = ...,
163+
title: str = ...,
164+
name: str = ...,
165+
description: str | None = ...,
166+
cluster_name: str | Literal["Kubernetes"] = ...,
167+
environment_type: str | Literal["Kubernetes"] = ...,
168+
matching: MatchingType = ...,
169+
supervisor: str | None = ...,
170+
python: Installations | None = ...,
171+
quarto: Installations | None = ...,
172+
r: Installations | None = ...,
173+
tensorflow: Installations | None = ...,
174+
) -> Environment | None:
175+
"""Find the first record matching the specified conditions.
176+
177+
There is no implied ordering, so if order matters, you should specify it yourself.
178+
179+
Parameters
180+
----------
181+
id : str
182+
The numerical identifier.
183+
guid : str
184+
The unique identifier.
185+
created_time : str
186+
The timestamp (RFC3339) when the environment was created.
187+
updated_time : str
188+
The timestamp (RFC3339) when the environment was updated.
189+
title : str
190+
A human-readable title.
191+
name : str
192+
The container image name used for execution in this environment.
193+
description : str, optional
194+
A human-readable description.
195+
cluster_name : str | Literal["Kubernetes"]
196+
The cluster identifier for this environment. Defaults to "Kubernetes" when Off-Host-Execution is enabled.
197+
environment_type : str | Literal["Kubernetes"]
198+
The cluster environment type. Defaults to "Kubernetes" when Off-Host-Execution is enabled.
199+
matching : MatchingType
200+
Directions for how the environment is considered for selection.
201+
supervisor : str, optional
202+
Path to the supervisor script
203+
python : Installations, optional
204+
The Python installations available in this environment
205+
quarto : Installations, optional
206+
The Quarto installations available in this environment
207+
r : Installations, optional
208+
The R installations available in this environment
209+
tensorflow : Installations, optional
210+
The Tensorflow installations available in this environment
211+
212+
Returns
213+
-------
214+
Environment | None
215+
216+
Note
217+
----
218+
This action requires administrator or publisher privileges.
219+
"""
220+
...

0 commit comments

Comments
 (0)