Skip to content

Commit 39e1eb0

Browse files
committed
feat: add factory support to resource sequences
1 parent f4c5445 commit 39e1eb0

File tree

2 files changed

+33
-26
lines changed

2 files changed

+33
-26
lines changed

src/posit/connect/client.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
from requests import Response, Session
66
from typing_extensions import TYPE_CHECKING, overload
77

8+
from posit.connect.environments import Environment
9+
from posit.connect.packages import Package
10+
811
from . import hooks, me
912
from .auth import Auth
1013
from .config import Config
@@ -298,7 +301,7 @@ def oauth(self) -> OAuth:
298301
@property
299302
@requires(version="2024.11.0")
300303
def packages(self) -> Packages:
301-
return _PaginatedResourceSequence(self._ctx, "v1/packages", uid="name")
304+
return _PaginatedResourceSequence[Package](self._ctx, "v1/packages", uid="name")
302305

303306
@property
304307
def vanities(self) -> Vanities:
@@ -311,7 +314,7 @@ def system(self) -> System:
311314
@property
312315
@requires(version="2023.05.0")
313316
def environments(self) -> Environments:
314-
return _ResourceSequence(self._ctx, "v1/environments")
317+
return _ResourceSequence[Environment](self._ctx, "v1/environments")
315318

316319
def __del__(self):
317320
"""Close the session when the Client instance is deleted."""

src/posit/connect/resources.py

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import posixpath
44
import warnings
55
from abc import ABC
6-
from typing import ItemsView, cast
6+
from typing import ItemsView, Type, cast
77

88
from typing_extensions import (
99
TYPE_CHECKING,
@@ -92,49 +92,50 @@ def update(self, **attributes): # type: ignore[reportIncompatibleMethodOverride
9292
super().update(**result)
9393

9494

95-
T = TypeVar("T", bound=Resource)
95+
_T = TypeVar("_T", bound=Resource)
96+
_T_co = TypeVar("_T_co", bound=Resource, covariant=True)
9697

9798

98-
class ResourceFactory(Protocol):
99-
def __call__(self, ctx: Context, path: str, **attributes) -> Resource: ...
99+
class ResourceFactory(Protocol[_T_co]):
100+
def __call__(self, ctx: Context, path: str, **attributes: Any) -> _T_co: ...
100101

101102

102-
class ResourceSequence(Protocol[T]):
103+
class ResourceSequence(Protocol[_T]):
103104
@overload
104-
def __getitem__(self, index: SupportsIndex, /) -> T: ...
105+
def __getitem__(self, index: SupportsIndex, /) -> _T: ...
105106

106107
@overload
107-
def __getitem__(self, index: slice, /) -> List[T]: ...
108+
def __getitem__(self, index: slice, /) -> List[_T]: ...
108109

109110
def __len__(self) -> int: ...
110111

111-
def __iter__(self) -> Iterator[T]: ...
112+
def __iter__(self) -> Iterator[_T]: ...
112113

113114
def __str__(self) -> str: ...
114115

115116
def __repr__(self) -> str: ...
116117

117118

118-
class _ResourceSequence(Sequence[T], ResourceSequence[T]):
119+
class _ResourceSequence(Sequence[_T], ResourceSequence[_T]):
119120
def __init__(
120121
self,
121122
ctx: Context,
122123
path: str,
123-
factory: ResourceFactory = _Resource,
124+
factory: ResourceFactory[_T] | None = None,
124125
uid: str = "guid",
125126
):
126127
self._ctx = ctx
127128
self._path = path
128129
self._uid = uid
129-
self._factory = factory
130+
self._factory = factory or cast(ResourceFactory[_T], _Resource)
130131

131132
def __getitem__(self, index):
132133
return list(self.fetch())[index]
133134

134135
def __len__(self) -> int:
135136
return len(list(self.fetch()))
136137

137-
def __iter__(self) -> Iterator[T]:
138+
def __iter__(self) -> Iterator[_T]:
138139
return iter(self.fetch())
139140

140141
def __str__(self) -> str:
@@ -143,32 +144,34 @@ def __str__(self) -> str:
143144
def __repr__(self) -> str:
144145
return repr(self.fetch())
145146

146-
def create(self, **attributes: Any) -> T:
147+
def create(self, **attributes: Any) -> _T:
147148
response = self._ctx.client.post(self._path, json=attributes)
148149
result = response.json()
149150
uid = result[self._uid]
150151
path = posixpath.join(self._path, uid)
151-
return cast(T, self._factory(self._ctx, path, **result))
152+
resource = self._factory(self._ctx, path, **result)
153+
return resource
152154

153-
def fetch(self, **conditions) -> Iterable[T]:
155+
def fetch(self, **conditions: Any) -> Iterable[_T]:
154156
response = self._ctx.client.get(self._path, params=conditions)
155157
results = response.json()
156-
resources: List[T] = []
158+
resources: List[_T] = []
157159
for result in results:
158160
uid = result[self._uid]
159161
path = posixpath.join(self._path, uid)
160-
resource = cast(T, self._factory(self._ctx, path, **result))
162+
resource = self._factory(self._ctx, path, **result)
161163
resources.append(resource)
162164

163165
return resources
164166

165-
def find(self, *args: str) -> T:
167+
def find(self, *args: str) -> _T:
166168
path = posixpath.join(self._path, *args)
167169
response = self._ctx.client.get(path)
168170
result = response.json()
169-
return cast(T, self._factory(self._ctx, path, **result))
171+
resource = self._factory(self._ctx, path, **result)
172+
return resource
170173

171-
def find_by(self, **conditions) -> T | None:
174+
def find_by(self, **conditions: Any) -> _T | None:
172175
"""
173176
Find the first record matching the specified conditions.
174177
@@ -183,19 +186,20 @@ def find_by(self, **conditions) -> T | None:
183186
Optional[T]
184187
The first record matching the conditions, or `None` if no match is found.
185188
"""
186-
collection: Iterable[T] = self.fetch(**conditions)
189+
collection = self.fetch(**conditions)
187190
return next((v for v in collection if v.items() >= conditions.items()), None)
188191

189192

190-
class _PaginatedResourceSequence(_ResourceSequence[T]):
191-
def fetch(self, **conditions) -> Iterator[T]:
193+
class _PaginatedResourceSequence(_ResourceSequence[_T]):
194+
def fetch(self, **conditions: Any) -> Iterable[_T]:
192195
paginator = Paginator(self._ctx, self._path, dict(**conditions))
193196
for page in paginator.fetch_pages():
194197
resources = []
195198
results = page.results
196199
for result in results:
197200
uid = result[self._uid]
198201
path = posixpath.join(self._path, uid)
199-
resource = cast(T, self._factory(self._ctx, path, **result))
202+
resource = self._factory(self._ctx, path, **result)
203+
200204
resources.append(resource)
201205
yield from resources

0 commit comments

Comments
 (0)