Skip to content

Commit 7eeb054

Browse files
authored
refactor: wrap cache interactions (#318)
1 parent 97d24f6 commit 7eeb054

File tree

1 file changed

+64
-72
lines changed

1 file changed

+64
-72
lines changed

src/posit/connect/resources.py

Lines changed: 64 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -85,53 +85,80 @@ def __init__(self, ctx: Context, path: str, pathinfo: str = "", /, **attributes)
8585

8686

8787
class ActiveSequence(ABC, Generic[T], Sequence[T]):
88+
"""A sequence for any HTTP endpoint that returns a collection."""
89+
8890
def __init__(self, ctx: Context, path: str, pathinfo: str = "", uid: str = "guid"):
89-
"""A sequence abstraction for any HTTP GET endpoint that returns a collection.
91+
"""A sequence abstraction for any HTTP GET endpoint that returns a collection."""
92+
super().__init__()
93+
self._ctx: Context = ctx
94+
self._path: str = posixpath.join(path, pathinfo)
95+
self._uid: str = uid
96+
self._cache: Optional[List[T]] = None
9097

91-
It lazily fetches data on demand, caches the results, and allows for standard sequence operations like indexing and slicing.
98+
@abstractmethod
99+
def _create_instance(self, path: str, pathinfo: str, /, **kwargs: Any) -> T:
100+
"""Create an instance of 'T'."""
101+
raise NotImplementedError()
92102

93-
Attributes
94-
----------
95-
_ctx : Context
96-
The context object containing the session and URL for API interactions
97-
_path : str
98-
The HTTP path for the collection endpoint.
99-
_uid : str
100-
The field name used to uniquely identify records.
101-
_cache: Optional[List[T]]
103+
def cached(self) -> bool:
104+
"""Returns True if the collection is cached.
105+
106+
Returns
107+
-------
108+
bool
109+
110+
See Also
111+
--------
112+
reload
102113
"""
103-
super().__init__()
104-
self._ctx = ctx
105-
self._path = posixpath.join(path, pathinfo)
106-
self._uid = uid
107-
self._cache: Optional[List[T]] = None
114+
return self._cache is not None
115+
116+
def reload(self) -> Self:
117+
"""Reloads the collection from Connect.
108118
109-
def _get_or_fetch(self) -> List[T]:
119+
Returns
120+
-------
121+
Self
110122
"""
111-
Fetch and cache the data from the API.
123+
self._cache = None
124+
return self
125+
126+
def _fetch(self) -> List[T]:
127+
"""Fetch the collection.
112128
113-
This method sends a GET request to the `_endpoint` and parses the response as a list of JSON objects.
114-
Each JSON object is used to instantiate an item of type `T` using the class specified by `_cls`.
115-
The results are cached after the first request and reused for subsequent access unless reloaded.
129+
Fetches the collection directly from Connect. This operation does not effect the cache state.
116130
117131
Returns
118132
-------
119133
List[T]
120-
A list of items of type `T` representing the fetched data.
121134
"""
122-
if self._cache is not None:
123-
return self._cache
124-
125135
endpoint = self._ctx.url + self._path
126136
response = self._ctx.session.get(endpoint)
127137
results = response.json()
138+
return [self._to_instance(result) for result in results]
128139

129-
self._cache = []
130-
for result in results:
131-
uid = result[self._uid]
132-
instance = self._create_instance(self._path, uid, **result)
133-
self._cache.append(instance)
140+
def _to_instance(self, result: dict) -> T:
141+
"""Converts a result into an instance of T."""
142+
uid = result[self._uid]
143+
return self._create_instance(self._path, uid, **result)
134144

145+
@property
146+
def _data(self) -> List[T]:
147+
"""Get the collection.
148+
149+
Fetches the collection from Connect and caches the result. Subsequent invocations return the cached results unless the cache is explicitly reset.
150+
151+
Returns
152+
-------
153+
List[T]
154+
155+
See Also
156+
--------
157+
cached
158+
reload
159+
"""
160+
if self._cache is None:
161+
self._cache = self._fetch()
135162
return self._cache
136163

137164
@overload
@@ -141,42 +168,16 @@ def __getitem__(self, index: int) -> T: ...
141168
def __getitem__(self, index: slice) -> Sequence[T]: ...
142169

143170
def __getitem__(self, index):
144-
data = self._get_or_fetch()
145-
return data[index]
171+
return self._data[index]
146172

147173
def __len__(self) -> int:
148-
data = self._get_or_fetch()
149-
return len(data)
174+
return len(self._data)
150175

151176
def __str__(self) -> str:
152-
data = self._get_or_fetch()
153-
return str(data)
177+
return str(self._data)
154178

155179
def __repr__(self) -> str:
156-
data = self._get_or_fetch()
157-
return repr(data)
158-
159-
@abstractmethod
160-
def _create_instance(self, path: str, pathinfo: str, /, **kwargs: Any) -> T:
161-
"""Create an instance of 'T'.
162-
163-
Returns
164-
-------
165-
T
166-
"""
167-
raise NotImplementedError()
168-
169-
def reload(self) -> Self:
170-
"""
171-
Clear the cache and reload the data from the API on the next access.
172-
173-
Returns
174-
-------
175-
ActiveSequence
176-
The current instance with cleared cache, ready to reload data on next access.
177-
"""
178-
self._cache = None
179-
return self
180+
return repr(self._data)
180181

181182

182183
class ActiveFinderMethods(ActiveSequence[T], ABC):
@@ -200,9 +201,7 @@ def find(self, uid) -> T:
200201
-------
201202
T
202203
"""
203-
if self._cache:
204-
# Check if the record already exists in the cache.
205-
# It is assumed that local cache scan is faster than an additional HTTP request.
204+
if self.cached():
206205
conditions = {self._uid: uid}
207206
result = self.find_by(**conditions)
208207
if result:
@@ -211,13 +210,7 @@ def find(self, uid) -> T:
211210
endpoint = self._ctx.url + self._path + uid
212211
response = self._ctx.session.get(endpoint)
213212
result = response.json()
214-
result = self._create_instance(self._path, uid, **result)
215-
216-
# Invalidate the cache.
217-
# It is assumed that the cache is stale since a record exists on the server and not in the cache.
218-
self._cache = None
219-
220-
return result
213+
return self._to_instance(result)
221214

222215
def find_by(self, **conditions: Any) -> Optional[T]:
223216
"""
@@ -234,5 +227,4 @@ def find_by(self, **conditions: Any) -> Optional[T]:
234227
Optional[T]
235228
The first record matching the conditions, or `None` if no match is found.
236229
"""
237-
data = self._get_or_fetch()
238-
return next((v for v in data if v.items() >= conditions.items()), None)
230+
return next((v for v in self._data if v.items() >= conditions.items()), None)

0 commit comments

Comments
 (0)