Skip to content

Commit 733135c

Browse files
tdsteinschloerke
andauthored
refactor: use static duck typing for packages (#356)
Co-authored-by: Barret Schloerke <[email protected]>
1 parent 70fd96f commit 733135c

File tree

5 files changed

+80
-356
lines changed

5 files changed

+80
-356
lines changed

src/posit/connect/client.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@
1414
from .groups import Groups
1515
from .metrics import Metrics
1616
from .oauth import OAuth
17-
from .packages import Packages
18-
from .resources import ResourceParameters, _ResourceSequence
17+
from .resources import ResourceParameters, _PaginatedResourceSequence, _ResourceSequence
1918
from .tags import Tags
2019
from .tasks import Tasks
2120
from .users import User, Users
2221
from .vanities import Vanities
2322

2423
if TYPE_CHECKING:
2524
from .environments import Environments
25+
from .packages import _Packages
2626

2727

2828
class Client(ContextManager):
@@ -297,9 +297,9 @@ def oauth(self) -> OAuth:
297297
return OAuth(self.resource_params, self.cfg.api_key)
298298

299299
@property
300-
@requires(version="2024.10.0-dev")
301-
def packages(self) -> Packages:
302-
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")
303303

304304
@property
305305
def vanities(self) -> Vanities:

src/posit/connect/content.py

Lines changed: 10 additions & 3 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
2526
from .oauth.associations import ContentItemAssociations
26-
from .packages import ContentPackagesMixin as PackagesMixin
2727
from .permissions import Permissions
28-
from .resources import Resource, ResourceParameters, Resources, _ResourceSequence
28+
from .resources import Active, Resource, ResourceParameters, Resources, _ResourceSequence
2929
from .tags import ContentItemTags
3030
from .vanities import VanityMixin
3131
from .variants import Variants
3232

3333
if TYPE_CHECKING:
3434
from .context import Context
3535
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(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
@@ -516,6 +517,12 @@ def jobs(self) -> Jobs:
516517
path = posixpath.join(self._path, "jobs")
517518
return _ResourceSequence(self._ctx, path, uid="key")
518519

520+
@property
521+
@requires(version="2024.11.0")
522+
def packages(self) -> _ContentPackages:
523+
path = posixpath.join(self._path, "packages")
524+
return _ResourceSequence(self._ctx, path, uid="name")
525+
519526

520527
class Content(Resources):
521528
"""Content resource.

src/posit/connect/packages.py

Lines changed: 45 additions & 159 deletions
Original file line numberDiff line numberDiff line change
@@ -1,213 +1,99 @@
11
from __future__ import annotations
22

3-
import posixpath
4-
from typing import Generator, Literal, Optional, TypedDict
3+
from collections.abc import Mapping, Sized
4+
from typing import Any, Literal, Protocol, SupportsIndex, overload
55

6-
from typing_extensions import NotRequired, Required, Unpack
76

8-
from posit.connect.context import requires
9-
from posit.connect.errors import ClientError
10-
from posit.connect.paginator import Paginator
7+
class _ContentPackage(Mapping[str, Any]):
8+
pass
119

12-
from .resources import Active, ActiveFinderMethods, ActiveSequence
1310

11+
class _ContentPackages(Sized, Protocol):
12+
@overload
13+
def __getitem__(self, index: SupportsIndex) -> _ContentPackage: ...
1414

15-
class ContentPackage(Active):
16-
class _Package(TypedDict):
17-
language: Required[str]
18-
name: Required[str]
19-
version: Required[str]
20-
hash: Required[Optional[str]]
15+
@overload
16+
def __getitem__(self, index: slice) -> _ContentPackage: ...
2117

22-
def __init__(self, ctx, /, **attributes: Unpack[_Package]):
23-
# 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.
24-
super().__init__(ctx, "", **attributes)
25-
26-
27-
class ContentPackages(ActiveFinderMethods["ContentPackage"], ActiveSequence["ContentPackage"]):
28-
"""A collection of packages."""
29-
30-
def __init__(self, ctx, path):
31-
super().__init__(ctx, path, "name")
32-
33-
def _create_instance(self, path, /, **attributes): # noqa: ARG002
34-
return ContentPackage(self._ctx, **attributes)
35-
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-
44-
def find(self, uid):
45-
raise NotImplementedError("The 'find' method is not support by the Packages API.")
46-
47-
class _FindBy(TypedDict, total=False):
48-
language: NotRequired[Literal["python", "r"]]
49-
"""Programming language ecosystem, options are 'python' and 'r'"""
50-
51-
name: NotRequired[str]
52-
"""The package name"""
53-
54-
version: NotRequired[str]
55-
"""The package version"""
56-
57-
hash: NotRequired[Optional[str]]
58-
"""Package description hash for R packages."""
59-
60-
def find_by(self, **conditions: Unpack[_FindBy]): # type: ignore
18+
def find_by(
19+
self,
20+
*,
21+
language: Literal["python", "r"] = ...,
22+
name: str = ...,
23+
version: str = ...,
24+
hash: str | None = ..., # noqa: A002
25+
) -> _ContentPackage | None:
6126
"""
6227
Find the first record matching the specified conditions.
6328
6429
There is no implied ordering, so if order matters, you should specify it yourself.
6530
6631
Parameters
6732
----------
68-
**conditions : Unpack[_FindBy]
69-
Conditions for filtering packages. The following keys are accepted:
70-
7133
language : {"python", "r"}, not required
7234
Programming language ecosystem, options are 'python' and 'r'
73-
7435
name : str, not required
7536
The package name
76-
7737
version : str, not required
7838
The package version
79-
8039
hash : str or None, optional, not required
8140
Package description hash for R packages.
8241
8342
Returns
8443
-------
85-
Optional[T]
44+
_ContentPackage | None
8645
The first record matching the specified conditions, or `None` if no such record exists.
8746
"""
88-
return super().find_by(**conditions)
89-
90-
91-
class ContentPackagesMixin(Active):
92-
"""Mixin class to add a packages attribute."""
93-
94-
@property
95-
@requires(version="2024.10.0-dev")
96-
def packages(self):
97-
path = posixpath.join(self._path, "packages")
98-
return ContentPackages(self._ctx, path)
99-
100-
101-
class Package(Active):
102-
class _Package(TypedDict):
103-
language: Required[Literal["python", "r"]]
104-
"""Programming language ecosystem, options are 'python' and 'r'"""
105-
106-
language_version: Required[str]
107-
"""Programming language version"""
108-
109-
name: Required[str]
110-
"""The package name"""
47+
...
11148

112-
version: Required[str]
113-
"""The package version"""
11449

115-
hash: Required[Optional[str]]
116-
"""Package description hash for R packages."""
50+
class _Package(Mapping[str, Any]):
51+
pass
11752

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

121-
app_id: Required[str]
122-
"""The numerical identifier of the application this package is associated with"""
54+
class _Packages(Sized, Protocol):
55+
@overload
56+
def __getitem__(self, index: SupportsIndex) -> _ContentPackage: ...
12357

124-
app_guid: Required[str]
125-
"""The unique identifier of the application this package is associated with"""
58+
@overload
59+
def __getitem__(self, index: slice) -> _ContentPackage: ...
12660

127-
def __init__(self, ctx, /, **attributes: Unpack[_Package]):
128-
# 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.
129-
super().__init__(ctx, "", **attributes)
130-
131-
132-
class Packages(ActiveFinderMethods["Package"], ActiveSequence["Package"]):
133-
def __init__(self, ctx, path):
134-
super().__init__(ctx, path, "name")
135-
136-
def _create_instance(self, path, /, **attributes): # noqa: ARG002
137-
return Package(self._ctx, **attributes)
138-
139-
class _Fetch(TypedDict, total=False):
140-
language: Required[Literal["python", "r"]]
141-
"""Programming language ecosystem, options are 'python' and 'r'"""
142-
143-
name: Required[str]
144-
"""The package name"""
145-
146-
version: Required[str]
147-
"""The package version"""
148-
149-
def fetch(self, **conditions: Unpack[_Fetch]) -> Generator["Package"]: # type: ignore
150-
# todo - add pagination support to ActiveSequence
151-
url = self._ctx.url + self._path
152-
paginator = Paginator(self._ctx.session, url, dict(**conditions))
153-
for page in paginator.fetch_pages():
154-
results = page.results
155-
yield from (self._create_instance("", **result) for result in results)
156-
157-
def find(self, uid):
158-
raise NotImplementedError("The 'find' method is not support by the Packages API.")
159-
160-
class _FindBy(TypedDict, total=False):
161-
language: NotRequired[Literal["python", "r"]]
162-
"""Programming language ecosystem, options are 'python' and 'r'"""
163-
164-
language_version: NotRequired[str]
165-
"""Programming language version"""
166-
167-
name: NotRequired[str]
168-
"""The package name"""
169-
170-
version: NotRequired[str]
171-
"""The package version"""
172-
173-
hash: NotRequired[Optional[str]]
174-
"""Package description hash for R packages."""
175-
176-
bundle_id: NotRequired[str]
177-
"""The unique identifier of the bundle this package is associated with"""
178-
179-
app_id: NotRequired[str]
180-
"""The numerical identifier of the application this package is associated with"""
181-
182-
app_guid: NotRequired[str]
183-
"""The unique identifier of the application this package is associated with"""
184-
185-
def find_by(self, **conditions: Unpack[_FindBy]) -> "Package | None": # type: ignore
61+
def find_by(
62+
self,
63+
*,
64+
language: Literal["python", "r"] = ...,
65+
name: str = ...,
66+
version: str = ...,
67+
hash: str | None = ..., # noqa: A002,
68+
bundle_id: str = ...,
69+
app_id: str = ...,
70+
app_guid: str = ...,
71+
) -> _ContentPackage | None:
18672
"""
18773
Find the first record matching the specified conditions.
18874
18975
There is no implied ordering, so if order matters, you should specify it yourself.
19076
19177
Parameters
19278
----------
193-
**conditions : Unpack[_FindBy]
194-
Conditions for filtering packages. The following keys are accepted:
195-
19679
language : {"python", "r"}, not required
19780
Programming language ecosystem, options are 'python' and 'r'
198-
19981
name : str, not required
20082
The package name
201-
20283
version : str, not required
20384
The package version
204-
20585
hash : str or None, optional, not required
20686
Package description hash for R packages.
87+
bundle_id: str, not required
88+
The unique identifier of the bundle this package is associated with.
89+
app_id: str, not required
90+
The numerical identifier of the application this package is associated with.
91+
app_guid: str, not required
92+
The unique identifier of the application this package is associated with.
20793
20894
Returns
20995
-------
210-
Optional[Package]
96+
_Package | None
21197
The first record matching the specified conditions, or `None` if no such record exists.
21298
"""
213-
return super().find_by(**conditions)
99+
...

0 commit comments

Comments
 (0)