Skip to content

Commit 4f6096b

Browse files
schloerketdstein
andauthored
refactor: use context client to make requests (#360)
Original request: #352 (review) I like it. So I updated as many as I could where I didn't have to guess at the intent. `Sessions` is still used by other classes that use `.params`. Given all `Paginator` instances could use context, I switched those as well. --------- Co-authored-by: Taylor Steinberg <[email protected]>
1 parent b182233 commit 4f6096b

File tree

7 files changed

+52
-222
lines changed

7 files changed

+52
-222
lines changed

src/posit/connect/_api.py

Lines changed: 2 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
1-
# TODO-barret-future; Piecemeal migrate everything to leverage `ApiDictEndpoint` and `ApiListEndpoint` classes.
1+
# TODO-barret-future; Piecemeal migrate everything to leverage `ApiDictEndpoint`
22
# TODO-barret-future; Merge any trailing behavior of `Active` or `ActiveList` into the new classes.
33

44
from __future__ import annotations
55

6-
import itertools
7-
import posixpath
8-
from abc import ABC, abstractmethod
96
from collections.abc import Mapping
10-
from typing import TYPE_CHECKING, Any, Generator, Generic, Optional, TypeVar, cast, overload
7+
from typing import TYPE_CHECKING, Any, Optional, cast
118

129
from ._api_call import ApiCallMixin, get_api
1310
from ._json import Jsonifiable, JsonifiableDict, ResponseAttrs
@@ -143,136 +140,3 @@ def __init__(
143140
super().__init__(attrs)
144141
self._ctx = ctx
145142
self._path = path
146-
147-
148-
T = TypeVar("T", bound="ReadOnlyDict")
149-
"""A type variable that is bound to the `Active` class"""
150-
151-
152-
class ApiListEndpoint(ApiCallMixin, Generic[T], ABC, object):
153-
"""A HTTP GET endpoint that can fetch a collection."""
154-
155-
def __init__(self, *, ctx: Context, path: str, uid_key: str = "guid") -> None:
156-
"""A sequence abstraction for any HTTP GET endpoint that returns a collection.
157-
158-
Parameters
159-
----------
160-
ctx : Context
161-
The context object containing the session and URL for API interactions.
162-
path : str
163-
The HTTP path component for the collection endpoint
164-
uid_key : str, optional
165-
The field name of that uniquely identifiers an instance of T, by default "guid"
166-
"""
167-
super().__init__()
168-
self._ctx = ctx
169-
self._path = path
170-
self._uid_key = uid_key
171-
172-
@abstractmethod
173-
def _create_instance(self, path: str, /, **kwargs: Any) -> T:
174-
"""Create an instance of 'T'."""
175-
raise NotImplementedError()
176-
177-
def fetch(self) -> Generator[T, None, None]:
178-
"""Fetch the collection.
179-
180-
Fetches the collection directly from Connect. This operation does not effect the cache state.
181-
182-
Returns
183-
-------
184-
List[T]
185-
"""
186-
results: Jsonifiable = self._get_api()
187-
results_list = cast(list[JsonifiableDict], results)
188-
for result in results_list:
189-
yield self._to_instance(result)
190-
191-
def __iter__(self) -> Generator[T, None, None]:
192-
return self.fetch()
193-
194-
def _to_instance(self, result: dict) -> T:
195-
"""Converts a result into an instance of T."""
196-
uid = result[self._uid_key]
197-
path = posixpath.join(self._path, uid)
198-
return self._create_instance(path, **result)
199-
200-
@overload
201-
def __getitem__(self, index: int) -> T: ...
202-
203-
@overload
204-
def __getitem__(self, index: slice) -> Generator[T, None, None]: ...
205-
206-
def __getitem__(self, index: int | slice) -> T | Generator[T, None, None]:
207-
if isinstance(index, slice):
208-
results = itertools.islice(self.fetch(), index.start, index.stop, index.step)
209-
for result in results:
210-
yield result
211-
else:
212-
return list(itertools.islice(self.fetch(), index, index + 1))[0]
213-
214-
# def __len__(self) -> int:
215-
# return len(self.fetch())
216-
217-
def __str__(self) -> str:
218-
return self.__repr__()
219-
220-
def __repr__(self) -> str:
221-
# Jobs - 123 items
222-
return repr(
223-
f"{self.__class__.__name__} - { len(list(self.fetch())) } items - {self._path}"
224-
)
225-
226-
def find(self, uid: str) -> T | None:
227-
"""
228-
Find a record by its unique identifier.
229-
230-
Fetches the record from Connect by it's identifier.
231-
232-
Parameters
233-
----------
234-
uid : str
235-
The unique identifier of the record.
236-
237-
Returns
238-
-------
239-
:
240-
Single instance of T if found, else None
241-
"""
242-
result: Jsonifiable = self._get_api(uid)
243-
result_obj = cast(JsonifiableDict, result)
244-
245-
return self._to_instance(result_obj)
246-
247-
def find_by(self, **conditions: Any) -> T | None:
248-
"""
249-
Find the first record matching the specified conditions.
250-
251-
There is no implied ordering, so if order matters, you should specify it yourself.
252-
253-
Parameters
254-
----------
255-
**conditions : Any
256-
257-
Returns
258-
-------
259-
T
260-
The first record matching the conditions, or `None` if no match is found.
261-
"""
262-
results = self.fetch()
263-
264-
conditions_items = conditions.items()
265-
266-
# Get the first item of the generator that matches the conditions
267-
# If no item is found, return None
268-
return next(
269-
(
270-
# Return result
271-
result
272-
# Iterate through `results` generator
273-
for result in results
274-
# If all `conditions`'s key/values are found in `result`'s key/values...
275-
if result.items() >= conditions_items
276-
),
277-
None,
278-
)

src/posit/connect/content.py

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -295,8 +295,7 @@ def create_repository(
295295
def delete(self) -> None:
296296
"""Delete the content item."""
297297
path = f"v1/content/{self['guid']}"
298-
url = self._ctx.url + path
299-
self._ctx.session.delete(url)
298+
self._ctx.client.delete(path)
300299

301300
def deploy(self) -> tasks.Task:
302301
"""Deploy the content.
@@ -315,8 +314,7 @@ def deploy(self) -> tasks.Task:
315314
None
316315
"""
317316
path = f"v1/content/{self['guid']}/deploy"
318-
url = self._ctx.url + path
319-
response = self._ctx.session.post(url, json={"bundle_id": None})
317+
response = self._ctx.client.post(path, json={"bundle_id": None})
320318
result = response.json()
321319
ts = tasks.Tasks(self.params)
322320
return ts.get(result["task_id"])
@@ -442,8 +440,7 @@ def update(
442440
-------
443441
None
444442
"""
445-
url = self._ctx.url + f"v1/content/{self['guid']}"
446-
response = self._ctx.session.patch(url, json=attrs)
443+
response = self._ctx.client.patch(f"v1/content/{self['guid']}", json=attrs)
447444
super().update(**response.json())
448445

449446
# Relationships
@@ -619,9 +616,7 @@ def create(
619616
-------
620617
ContentItem
621618
"""
622-
path = "v1/content"
623-
url = self._ctx.url + path
624-
response = self._ctx.session.post(url, json=attrs)
619+
response = self._ctx.client.post("v1/content", json=attrs)
625620
return ContentItem(self._ctx, **response.json())
626621

627622
@overload
@@ -707,9 +702,7 @@ def find(self, include: Optional[str | list[Any]] = None, **conditions) -> List[
707702
if self.owner_guid:
708703
conditions["owner_guid"] = self.owner_guid
709704

710-
path = "v1/content"
711-
url = self._ctx.url + path
712-
response = self._ctx.session.get(url, params=conditions)
705+
response = self._ctx.client.get("v1/content", params=conditions)
713706
return [
714707
ContentItem(
715708
self._ctx,
@@ -880,7 +873,5 @@ def get(self, guid: str) -> ContentItem:
880873
-------
881874
ContentItem
882875
"""
883-
path = f"v1/content/{guid}"
884-
url = self._ctx.url + path
885-
response = self._ctx.session.get(url)
876+
response = self._ctx.client.get(f"v1/content/{guid}")
886877
return ContentItem(self._ctx, **response.json())

src/posit/connect/groups.py

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,7 @@ def delete(self) -> None:
6161
group.delete()
6262
```
6363
"""
64-
path = f"v1/groups/{self['guid']}"
65-
url = self._ctx.url + path
66-
self._ctx.session.delete(url)
64+
self._ctx.client.delete(f"v1/groups/{self['guid']}")
6765

6866

6967
class GroupMembers(Resources):
@@ -128,9 +126,10 @@ def add(self, user: Optional[User] = None, /, *, user_guid: Optional[str] = None
128126
if not user_guid:
129127
raise ValueError("`user_guid=` should not be empty.")
130128

131-
path = f"v1/groups/{self._group_guid}/members"
132-
url = self._ctx.url + path
133-
self._ctx.session.post(url, json={"user_guid": user_guid})
129+
self._ctx.client.post(
130+
f"v1/groups/{self._group_guid}/members",
131+
json={"user_guid": user_guid},
132+
)
134133

135134
@overload
136135
def delete(self, user: User, /) -> None: ...
@@ -188,9 +187,7 @@ def delete(self, user: Optional[User] = None, /, *, user_guid: Optional[str] = N
188187
if not user_guid:
189188
raise ValueError("`user_guid=` should not be empty.")
190189

191-
path = f"v1/groups/{self._group_guid}/members/{user_guid}"
192-
url = self._ctx.url + path
193-
self._ctx.session.delete(url)
190+
self._ctx.client.delete(f"v1/groups/{self._group_guid}/members/{user_guid}")
194191

195192
def find(self) -> list[User]:
196193
"""Find group members.
@@ -221,8 +218,7 @@ def find(self) -> list[User]:
221218
from .users import User
222219

223220
path = f"v1/groups/{self._group_guid}/members"
224-
url = self._ctx.url + path
225-
paginator = Paginator(self._ctx.session, url)
221+
paginator = Paginator(self._ctx, path)
226222
member_dicts = paginator.fetch_results()
227223

228224
# For each member in the group
@@ -253,9 +249,10 @@ def count(self) -> int:
253249
--------
254250
* https://docs.posit.co/connect/api/#get-/v1/groups/-group_guid-/members
255251
"""
256-
path = f"v1/groups/{self._group_guid}/members"
257-
url = self._ctx.url + path
258-
response = self._ctx.session.get(url, params={"page_size": 1})
252+
response = self._ctx.client.get(
253+
f"v1/groups/{self._group_guid}/members",
254+
params={"page_size": 1},
255+
)
259256
result = response.json()
260257
return result["total"]
261258

@@ -306,9 +303,7 @@ def create(self, **kwargs) -> Group:
306303
-------
307304
Group
308305
"""
309-
path = "v1/groups"
310-
url = self._ctx.url + path
311-
response = self._ctx.session.post(url, json=kwargs)
306+
response = self._ctx.client.post("v1/groups", json=kwargs)
312307
return Group(self._ctx, **response.json())
313308

314309
@overload
@@ -338,8 +333,7 @@ def find(self, **kwargs):
338333
* https://docs.posit.co/connect/api/#get-/v1/groups
339334
"""
340335
path = "v1/groups"
341-
url = self._ctx.url + path
342-
paginator = Paginator(self._ctx.session, url, params=kwargs)
336+
paginator = Paginator(self._ctx, path, params=kwargs)
343337
results = paginator.fetch_results()
344338
return [
345339
Group(
@@ -376,8 +370,7 @@ def find_one(self, **kwargs) -> Group | None:
376370
* https://docs.posit.co/connect/api/#get-/v1/groups
377371
"""
378372
path = "v1/groups"
379-
url = self._ctx.url + path
380-
paginator = Paginator(self._ctx.session, url, params=kwargs)
373+
paginator = Paginator(self._ctx, path, params=kwargs)
381374
pages = paginator.fetch_pages()
382375
results = (result for page in pages for result in page.results)
383376
groups = (
@@ -404,8 +397,7 @@ def get(self, guid: str) -> Group:
404397
--------
405398
* https://docs.posit.co/connect/api/#get-/v1/groups
406399
"""
407-
url = self._ctx.url + f"v1/groups/{guid}"
408-
response = self._ctx.session.get(url)
400+
response = self._ctx.client.get(f"v1/groups/{guid}")
409401
return Group(
410402
self._ctx,
411403
**response.json(),

src/posit/connect/paginator.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from typing import TYPE_CHECKING, Generator, List
55

66
if TYPE_CHECKING:
7-
import requests
7+
from .context import Context
88

99
# The maximum page size supported by the API.
1010
_MAX_PAGE_SIZE = 500
@@ -43,15 +43,15 @@ class Paginator:
4343

4444
def __init__(
4545
self,
46-
session: requests.Session,
47-
url: str,
46+
ctx: Context,
47+
path: str,
4848
params: dict | None = None,
4949
) -> None:
5050
if params is None:
5151
params = {}
52-
self.session = session
53-
self.url = url
54-
self.params = params
52+
self._ctx = ctx
53+
self._path = path
54+
self._params = params
5555

5656
def fetch_results(self) -> List[dict]:
5757
"""
@@ -106,9 +106,9 @@ def fetch_page(self, page_number: int) -> Page:
106106
107107
"""
108108
params = {
109-
**self.params,
109+
**self._params,
110110
"page_number": page_number,
111111
"page_size": _MAX_PAGE_SIZE,
112112
}
113-
response = self.session.get(self.url, params=params)
113+
response = self._ctx.client.get(self._path, params=params)
114114
return Page(**response.json())

src/posit/connect/resources.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,8 +167,7 @@ def find_by(self, **conditions) -> Any | None:
167167

168168
class _PaginatedResourceSequence(_ResourceSequence):
169169
def fetch(self, **conditions):
170-
url = self._ctx.url + self._path
171-
paginator = Paginator(self._ctx.session, url, dict(**conditions))
170+
paginator = Paginator(self._ctx, self._path, dict(**conditions))
172171
for page in paginator.fetch_pages():
173172
resources = []
174173
results = page.results

0 commit comments

Comments
 (0)