Skip to content

Commit 4501ace

Browse files
authored
Merge branch 'main' into require_ruff_on_save
2 parents c3f8dbf + c62956c commit 4501ace

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
@@ -39,7 +39,9 @@ class ContentItemOwner(Resource):
3939
class ContentItem(JobsMixin, VanityMixin, Resource):
4040
def __init__(self, /, params: ResourceParameters, **kwargs):
4141
ctx = Context(params.session, params.url)
42-
super().__init__(ctx, **kwargs)
42+
uid = kwargs["guid"]
43+
path = f"v1/content/{uid}"
44+
super().__init__(ctx, path, **kwargs)
4345

4446
def __getitem__(self, key: Any) -> Any:
4547
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
@@ -286,6 +279,19 @@ def find_by(self, **conditions) -> Optional[Job]:
286279
class JobsMixin(Active, Resource):
287280
"""Mixin class to add a jobs attribute to a resource."""
288281

289-
def __init__(self, ctx, **kwargs):
290-
super().__init__(ctx, **kwargs)
291-
self.jobs = Jobs(ctx, self)
282+
def __init__(self, ctx, path, /, **attributes):
283+
"""Mixin class which adds a `jobs` attribute to the Active Resource.
284+
285+
Parameters
286+
----------
287+
ctx : Context
288+
The context object containing the session and URL for API interactions
289+
path : str
290+
The HTTP path component for the resource endpoint
291+
**attributes : dict
292+
Resource attributes passed
293+
"""
294+
super().__init__(ctx, path, **attributes)
295+
296+
path = posixpath.join(path, "jobs")
297+
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
@@ -51,84 +52,105 @@ def __init__(self, params: ResourceParameters) -> None:
5152

5253

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

7374

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

7778

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

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

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

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

134156
@overload
@@ -149,52 +171,18 @@ def __str__(self) -> str:
149171
def __repr__(self) -> str:
150172
return repr(self._data)
151173

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

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

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

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

0 commit comments

Comments
 (0)