Skip to content

Commit c62956c

Browse files
tdsteinschloerke
andauthored
refactor: inject url path parts instead of endpoints (#315)
Refactors path building responsibilities to the creating action, eliminating a ton of complexity along the way. --------- Co-authored-by: Barret Schloerke <[email protected]>
1 parent 7c8acce commit c62956c

File tree

5 files changed

+115
-159
lines changed

5 files changed

+115
-159
lines changed

src/posit/connect/content.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ class ContentItemOwner(Resource):
3737
class ContentItem(JobsMixin, VanityMixin, Resource):
3838
def __init__(self, /, params: ResourceParameters, **kwargs):
3939
ctx = Context(params.session, params.url)
40-
super().__init__(ctx, **kwargs)
40+
uid = kwargs["guid"]
41+
path = f"v1/content/{uid}"
42+
super().__init__(ctx, path, **kwargs)
4143

4244
def __getitem__(self, key: Any) -> Any:
4345
v = super().__getitem__(key)

src/posit/connect/jobs.py

Lines changed: 36 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
from typing import Literal, Optional, TypedDict, overload
1+
import posixpath
2+
from typing import Any, Literal, Optional, TypedDict, overload
23

34
from typing_extensions import NotRequired, Required, Unpack
45

6+
from .context import Context
57
from .resources import Active, ActiveFinderMethods, ActiveSequence, Resource
68

79
JobTag = Literal[
@@ -99,13 +101,8 @@ class _Job(TypedDict):
99101
tag: Required[JobTag]
100102
"""A tag categorizing the job type. Options are build_jupyter, build_report, build_site, configure_report, git, packrat_restore, python_restore, render_shiny, run_api, run_app, run_bokeh_app, run_dash_app, run_fastapi_app, run_pyshiny_app, run_python_api, run_streamlit, run_tensorflow, run_voila_app, testing, unknown, val_py_ext_pkg, val_r_ext_pkg, and val_r_install."""
101103

102-
def __init__(self, ctx, parent: Active, **kwargs: Unpack[_Job]):
103-
super().__init__(ctx, parent, **kwargs)
104-
self._parent = parent
105-
106-
@property
107-
def _endpoint(self) -> str:
108-
return self._ctx.url + f"v1/content/{self._parent['guid']}/jobs/{self['key']}"
104+
def __init__(self, ctx: Context, path: str, /, **attributes: Unpack[_Job]):
105+
super().__init__(ctx, path, **attributes)
109106

110107
def destroy(self) -> None:
111108
"""Destroy the job.
@@ -120,40 +117,36 @@ def destroy(self) -> None:
120117
----
121118
This action requires administrator, owner, or collaborator privileges.
122119
"""
123-
self._ctx.session.delete(self._endpoint)
120+
endpoint = self._ctx.url + self._path
121+
self._ctx.session.delete(endpoint)
124122

125123

126-
class Jobs(
127-
ActiveFinderMethods[Job],
128-
ActiveSequence[Job],
129-
):
130-
def __init__(self, ctx, parent: Active, uid="key"):
124+
class Jobs(ActiveFinderMethods[Job], ActiveSequence[Job]):
125+
def __init__(self, ctx: Context, path: str):
131126
"""A collection of jobs.
132127
133128
Parameters
134129
----------
135130
ctx : Context
136-
The context containing the HTTP session used to interact with the API.
137-
parent : Active
138-
Parent resource for maintaining hierarchical relationships
139-
uid : str, optional
140-
The default field name used to uniquely identify records, by default "key"
131+
The context object containing the session and URL for API interactions
132+
path : str
133+
The HTTP path component for the jobs endpoint (e.g., 'v1/content/544509fc-e4f0-41de-acb4-1fe3a2c1d797/jobs')
141134
"""
142-
super().__init__(ctx, parent, uid)
143-
self._parent = parent
135+
super().__init__(ctx, path, "key")
144136

145-
@property
146-
def _endpoint(self) -> str:
147-
return self._ctx.url + f"v1/content/{self._parent['guid']}/jobs"
137+
def _create_instance(self, path: str, /, **attributes: Any) -> Job:
138+
"""Creates a Job instance.
148139
149-
def _create_instance(self, **kwargs) -> Job:
150-
"""Creates a `Job` instance.
140+
Parameters
141+
----------
142+
path : str
143+
The HTTP path component for the Job resource endpoint (e.g., 'v1/content/544509fc-e4f0-41de-acb4-1fe3a2c1d797/jobs/7add0bc0-0d89-4397-ab51-90ad4bc3f5c9')
151144
152145
Returns
153146
-------
154147
Job
155148
"""
156-
return Job(self._ctx, self._parent, **kwargs)
149+
return Job(self._ctx, path, **attributes)
157150

158151
class _FindByRequest(TypedDict, total=False):
159152
# Identifiers
@@ -287,6 +280,19 @@ def find_by(self, **conditions) -> Optional[Job]:
287280
class JobsMixin(Active, Resource):
288281
"""Mixin class to add a jobs attribute to a resource."""
289282

290-
def __init__(self, ctx, **kwargs):
291-
super().__init__(ctx, **kwargs)
292-
self.jobs = Jobs(ctx, self)
283+
def __init__(self, ctx, path, /, **attributes):
284+
"""Mixin class which adds a `jobs` attribute to the Active Resource.
285+
286+
Parameters
287+
----------
288+
ctx : Context
289+
The context object containing the session and URL for API interactions
290+
path : str
291+
The HTTP path component for the resource endpoint
292+
**attributes : dict
293+
Resource attributes passed
294+
"""
295+
super().__init__(ctx, path, **attributes)
296+
297+
path = posixpath.join(path, "jobs")
298+
self.jobs = Jobs(ctx, path)

src/posit/connect/resources.py

Lines changed: 67 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import posixpath
12
import warnings
23
from abc import ABC, abstractmethod
34
from dataclasses import dataclass
@@ -50,84 +51,105 @@ def __init__(self, params: ResourceParameters) -> None:
5051

5152

5253
class Active(ABC, Resource):
53-
def __init__(self, ctx: Context, parent: Optional["Active"] = None, **kwargs):
54-
"""A base class representing an active resource.
54+
def __init__(self, ctx: Context, path: str, /, **attributes):
55+
"""A dict abstraction for any HTTP endpoint that returns a singular resource.
5556
5657
Extends the `Resource` class and provides additional functionality for via the session context and an optional parent resource.
5758
5859
Parameters
5960
----------
6061
ctx : Context
6162
The context object containing the session and URL for API interactions.
62-
parent : Optional[Active], optional
63-
An optional parent resource that establishes a hierarchical relationship, by default None.
64-
**kwargs : dict
65-
Additional keyword arguments passed to the parent `Resource` class.
63+
path : str
64+
The HTTP path component for the resource endpoint
65+
**attributes : dict
66+
Resource attributes passed
6667
"""
6768
params = ResourceParameters(ctx.session, ctx.url)
68-
super().__init__(params, **kwargs)
69+
super().__init__(params, **attributes)
6970
self._ctx = ctx
70-
self._parent = parent
71+
self._path = path
7172

7273

7374
T = TypeVar("T", bound="Active")
7475
"""A type variable that is bound to the `Active` class"""
7576

7677

7778
class ActiveSequence(ABC, Generic[T], Sequence[T]):
78-
def __init__(self, ctx: Context, parent: Optional[Active] = None):
79-
"""A sequence abstraction for any HTTP GET endpoint that returns a collection.
79+
"""A sequence for any HTTP GET endpoint that returns a collection."""
80+
81+
_cache: Optional[List[T]]
8082

81-
It lazily fetches data on demand, caches the results, and allows for standard sequence operations like indexing and slicing.
83+
def __init__(self, ctx: Context, path: str, uid: str = "guid"):
84+
"""A sequence abstraction for any HTTP GET endpoint that returns a collection.
8285
8386
Parameters
8487
----------
8588
ctx : Context
86-
The context object that holds the HTTP session used for sending the GET request.
87-
parent : Optional[Active], optional
88-
An optional parent resource to establish a nested relationship, by default None.
89+
The context object containing the session and URL for API interactions.
90+
path : str
91+
The HTTP path component for the collection endpoint
92+
uid : str, optional
93+
The field name of that uniquely identifiers an instance of T, by default "guid"
8994
"""
9095
super().__init__()
9196
self._ctx = ctx
92-
self._parent = parent
93-
self._cache: Optional[List[T]] = None
97+
self._path = path
98+
self._uid = uid
99+
self._cache = None
94100

95-
@property
96101
@abstractmethod
97-
def _endpoint(self) -> str:
102+
def _create_instance(self, path: str, /, **kwargs: Any) -> T:
103+
"""Create an instance of 'T'."""
104+
raise NotImplementedError()
105+
106+
def reload(self) -> Self:
107+
"""Reloads the collection from Connect.
108+
109+
Returns
110+
-------
111+
Self
98112
"""
99-
Abstract property to define the endpoint URL for the GET request.
113+
self._cache = None
114+
return self
115+
116+
def _fetch(self) -> List[T]:
117+
"""Fetch the collection.
100118
101-
Subclasses must implement this property to return the API endpoint URL that will
102-
be queried to fetch the data.
119+
Fetches the collection directly from Connect. This operation does not effect the cache state.
103120
104121
Returns
105122
-------
106-
str
107-
The API endpoint URL.
123+
List[T]
108124
"""
109-
raise NotImplementedError()
125+
endpoint = self._ctx.url + self._path
126+
response = self._ctx.session.get(endpoint)
127+
results = response.json()
128+
return [self._to_instance(result) for result in results]
129+
130+
def _to_instance(self, result: dict) -> T:
131+
"""Converts a result into an instance of T."""
132+
uid = result[self._uid]
133+
path = posixpath.join(self._path, uid)
134+
return self._create_instance(path, **result)
110135

111136
@property
112137
def _data(self) -> List[T]:
113-
"""
114-
Fetch and cache the data from the API.
138+
"""Get the collection.
115139
116-
This method sends a GET request to the `_endpoint` and parses the response as a list of JSON objects.
117-
Each JSON object is used to instantiate an item of type `T` using the class specified by `_cls`.
118-
The results are cached after the first request and reused for subsequent access unless reloaded.
140+
Fetches the collection from Connect and caches the result. Subsequent invocations return the cached results unless the cache is explicitly reset.
119141
120142
Returns
121143
-------
122144
List[T]
123-
A list of items of type `T` representing the fetched data.
124-
"""
125-
if self._cache:
126-
return self._cache
127145
128-
response = self._ctx.session.get(self._endpoint)
129-
results = response.json()
130-
self._cache = [self._create_instance(**result) for result in results]
146+
See Also
147+
--------
148+
cached
149+
reload
150+
"""
151+
if self._cache is None:
152+
self._cache = self._fetch()
131153
return self._cache
132154

133155
@overload
@@ -148,52 +170,18 @@ def __str__(self) -> str:
148170
def __repr__(self) -> str:
149171
return repr(self._data)
150172

151-
@abstractmethod
152-
def _create_instance(self, **kwargs) -> T:
153-
"""Create an instance of 'T'.
154173

155-
Returns
156-
-------
157-
T
158-
"""
159-
raise NotImplementedError()
174+
class ActiveFinderMethods(ActiveSequence[T], ABC):
175+
"""Finder methods.
160176
161-
def reload(self) -> Self:
162-
"""
163-
Clear the cache and reload the data from the API on the next access.
164-
165-
Returns
166-
-------
167-
ActiveSequence
168-
The current instance with cleared cache, ready to reload data on next access.
169-
"""
170-
self._cache = None
171-
return self
172-
173-
174-
class ActiveFinderMethods(ActiveSequence[T], ABC, Generic[T]):
175-
def __init__(self, ctx: Context, parent: Optional[Active] = None, uid: str = "guid"):
176-
"""Finder methods.
177-
178-
Provides various finder methods for locating records in any endpoint supporting HTTP GET requests.
179-
180-
Parameters
181-
----------
182-
ctx : Context
183-
The context containing the HTTP session used to interact with the API.
184-
parent : Optional[Active], optional
185-
Optional parent resource for maintaining hierarchical relationships, by default None
186-
uid : str, optional
187-
The default field name used to uniquely identify records, by default "guid"
188-
"""
189-
super().__init__(ctx, parent)
190-
self._uid = uid
177+
Provides various finder methods for locating records in any endpoint supporting HTTP GET requests.
178+
"""
191179

192180
def find(self, uid) -> T:
193181
"""
194182
Find a record by its unique identifier.
195183
196-
Fetches a record either by searching the cache or by making a GET request to the endpoint.
184+
Fetches the record from Connect by it's identifier.
197185
198186
Parameters
199187
----------
@@ -203,26 +191,11 @@ def find(self, uid) -> T:
203191
Returns
204192
-------
205193
T
206-
207-
Raises
208-
------
209-
ValueError
210-
If no record is found.
211194
"""
212-
# todo - add some more comments about this
213-
if self._cache:
214-
conditions = {self._uid: uid}
215-
result = self.find_by(**conditions)
216-
else:
217-
endpoint = self._endpoint + uid
218-
response = self._ctx.session.get(endpoint)
219-
result = response.json()
220-
result = self._create_instance(**result)
221-
222-
if not result:
223-
raise ValueError(f"Failed to find instance where {self._uid} is '{uid}'")
224-
225-
return result
195+
endpoint = self._ctx.url + self._path + uid
196+
response = self._ctx.session.get(endpoint)
197+
result = response.json()
198+
return self._to_instance(result)
226199

227200
def find_by(self, **conditions: Any) -> Optional[T]:
228201
"""

0 commit comments

Comments
 (0)