Skip to content

Commit 82b9b7e

Browse files
committed
applies feedback discussed in pull requests
1 parent a1ca377 commit 82b9b7e

File tree

2 files changed

+89
-127
lines changed

2 files changed

+89
-127
lines changed

src/posit/connect/jobs.py

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from typing_extensions import NotRequired, Required, Unpack
44

5-
from .resources import Active, ActiveFinderMethods, Resource
5+
from .resources import Active, ActiveFinderMethods, ActiveSequence, Resource
66

77
JobTag = Literal[
88
"unknown",
@@ -123,22 +123,41 @@ def destroy(self) -> None:
123123
self._ctx.session.delete(self._endpoint)
124124

125125

126-
class Jobs(ActiveFinderMethods[Job]):
127-
"""A collection of jobs."""
126+
class Jobs(
127+
ActiveFinderMethods[Job],
128+
ActiveSequence[Job],
129+
):
130+
def __init__(self, ctx, parent: Active, uid="key"):
131+
"""A collection of jobs.
128132
129-
_uid = "key"
130-
131-
def __init__(self, cls, ctx, parent: Active):
132-
super().__init__(cls, ctx, parent)
133+
Parameters
134+
----------
135+
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"
141+
"""
142+
super().__init__(ctx, parent, uid)
133143
self._parent = parent
134144

135145
@property
136146
def _endpoint(self) -> str:
137147
return self._ctx.url + f"v1/content/{self._parent['guid']}/jobs"
138148

149+
def _create_instance(self, **kwargs) -> Job:
150+
"""Creates a `Job` instance.
151+
152+
Returns
153+
-------
154+
Job
155+
"""
156+
return Job(self._ctx, self._parent, **kwargs)
157+
139158
class _FindByRequest(TypedDict, total=False):
140159
# Identifiers
141-
id: NotRequired[str]
160+
id: Required[str]
142161
"""A unique identifier for the job."""
143162

144163
ppid: NotRequired[Optional[str]]
@@ -270,4 +289,4 @@ class JobsMixin(Active, Resource):
270289

271290
def __init__(self, ctx, **kwargs):
272291
super().__init__(ctx, **kwargs)
273-
self.jobs = Jobs(Job, ctx, self)
292+
self.jobs = Jobs(ctx, self)

src/posit/connect/resources.py

Lines changed: 61 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import warnings
22
from abc import ABC, abstractmethod
33
from dataclasses import dataclass
4-
from typing import Any, Generic, List, Optional, Sequence, Type, TypeVar, overload
4+
from typing import Any, Generic, List, Optional, Sequence, TypeVar, overload
55

66
import requests
7+
from typing_extensions import Self
78

89
from .context import Context
910
from .urls import Url
@@ -48,39 +49,18 @@ def __init__(self, params: ResourceParameters) -> None:
4849
self.params = params
4950

5051

51-
class Active(Resource):
52-
"""
53-
A base class representing an active resource.
54-
55-
Extends the `Resource` class and provides additional functionality for via the session context and an optional parent resource.
56-
57-
Parameters
58-
----------
59-
ctx : Context
60-
The context object containing the session and URL for API interactions.
61-
parent : Optional[Active], optional
62-
An optional parent resource that establishes a hierarchical relationship, by default None.
63-
**kwargs : dict
64-
Additional keyword arguments passed to the parent `Resource` class.
65-
66-
Attributes
67-
----------
68-
_ctx : Context
69-
The session context.
70-
_parent : Optional[Active]
71-
The parent resource, if provided, which establishes a hierarchical relationship.
72-
"""
73-
52+
class Active(ABC, Resource):
7453
def __init__(self, ctx: Context, parent: Optional["Active"] = None, **kwargs):
75-
"""
76-
Initialize the `Active` resource.
54+
"""A base class representing an active resource.
55+
56+
Extends the `Resource` class and provides additional functionality for via the session context and an optional parent resource.
7757
7858
Parameters
7959
----------
8060
ctx : Context
81-
The context object containing session and URL for API interactions.
61+
The context object containing the session and URL for API interactions.
8262
parent : Optional[Active], optional
83-
An optional parent resource to establish a hierarchical relationship, by default None.
63+
An optional parent resource that establishes a hierarchical relationship, by default None.
8464
**kwargs : dict
8565
Additional keyword arguments passed to the parent `Resource` class.
8666
"""
@@ -90,69 +70,27 @@ def __init__(self, ctx: Context, parent: Optional["Active"] = None, **kwargs):
9070
self._parent = parent
9171

9272

93-
T_co = TypeVar("T_co", bound="Active", covariant=True)
94-
"""A covariant type variable that is bound to the `Active` class and must inherit from it."""
73+
T = TypeVar("T", bound="Active")
74+
"""A type variable that is bound to the `Active` class"""
9575

9676

97-
class ActiveSequence(ABC, Generic[T_co], Sequence[T_co]):
98-
"""
99-
A sequence abstraction for any HTTP GET endpoint that returns a collection.
100-
101-
It lazily fetches data on demand, caches the results, and allows for standard sequence operations like indexing and slicing.
77+
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.
10280
103-
Parameters
104-
----------
105-
cls : Type[T_co]
106-
The class used to represent each item in the sequence.
107-
ctx : Context
108-
The context object that holds the HTTP session used for sending the GET request.
109-
parent : Optional[Active], optional
110-
An optional parent resource to establish a nested relationship, by default None.
81+
It lazily fetches data on demand, caches the results, and allows for standard sequence operations like indexing and slicing.
11182
112-
Attributes
113-
----------
114-
_cls : Type[T_co]
115-
The class used to instantiate each item in the sequence.
116-
_ctx : Context
117-
The context containing the HTTP session used to interact with the API.
118-
_parent : Optional[Active]
119-
Optional parent resource for maintaining hierarchical relationships.
120-
_cache : Optional[List[T_co]]
121-
Cached list of items returned from the API. Set to None initially, and populated after the first request.
122-
123-
Abstract Properties
124-
-------------------
125-
_endpoint : str
126-
The API endpoint URL for the HTTP GET request. Subclasses are required to implement this property.
127-
128-
Methods
129-
-------
130-
_data() -> List[T_co]
131-
Fetch and cache the data from the API. This method sends a GET request to `_endpoint`, parses the
132-
response as JSON, and instantiates each item using `cls`.
133-
134-
__getitem__(index) -> Union[T_co, Sequence[T_co]]
135-
Retrieve an item or slice from the sequence. Indexing follows the standard Python sequence semantics.
136-
137-
__len__() -> int
138-
Return the number of items in the sequence.
139-
140-
__str__() -> str
141-
Return a string representation of the cached data.
142-
143-
__repr__() -> str
144-
Return a detailed string representation of the cached data.
145-
146-
reload() -> ActiveSequence
147-
Clear the cache and mark to reload the data from the API on the next operation.
148-
"""
149-
150-
def __init__(self, cls: Type[T_co], ctx: Context, parent: Optional[Active] = None):
83+
Parameters
84+
----------
85+
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+
"""
15190
super().__init__()
152-
self._cls = cls
15391
self._ctx = ctx
15492
self._parent = parent
155-
self._cache = None
93+
self._cache: Optional[List[T]] = None
15694

15795
@property
15896
@abstractmethod
@@ -171,32 +109,32 @@ def _endpoint(self) -> str:
171109
raise NotImplementedError()
172110

173111
@property
174-
def _data(self) -> List[T_co]:
112+
def _data(self) -> List[T]:
175113
"""
176114
Fetch and cache the data from the API.
177115
178116
This method sends a GET request to the `_endpoint` and parses the response as a list of JSON objects.
179-
Each JSON object is used to instantiate an item of type `T_co` using the class specified by `_cls`.
117+
Each JSON object is used to instantiate an item of type `T` using the class specified by `_cls`.
180118
The results are cached after the first request and reused for subsequent access unless reloaded.
181119
182120
Returns
183121
-------
184-
List[T_co]
185-
A list of items of type `T_co` representing the fetched data.
122+
List[T]
123+
A list of items of type `T` representing the fetched data.
186124
"""
187125
if self._cache:
188126
return self._cache
189127

190128
response = self._ctx.session.get(self._endpoint)
191129
results = response.json()
192-
self._cache = [self._cls(self._ctx, self._parent, **result) for result in results]
130+
self._cache = [self._create_instance(**result) for result in results]
193131
return self._cache
194132

195133
@overload
196-
def __getitem__(self, index: int) -> T_co: ...
134+
def __getitem__(self, index: int) -> T: ...
197135

198136
@overload
199-
def __getitem__(self, index: slice) -> Sequence[T_co]: ...
137+
def __getitem__(self, index: slice) -> Sequence[T]: ...
200138

201139
def __getitem__(self, index):
202140
return self._data[index]
@@ -210,7 +148,17 @@ def __str__(self) -> str:
210148
def __repr__(self) -> str:
211149
return repr(self._data)
212150

213-
def reload(self) -> "ActiveSequence":
151+
@abstractmethod
152+
def _create_instance(self, **kwargs) -> T:
153+
"""Create an instance of 'T'.
154+
155+
Returns
156+
-------
157+
T
158+
"""
159+
raise NotImplementedError()
160+
161+
def reload(self) -> Self:
214162
"""
215163
Clear the cache and reload the data from the API on the next access.
216164
@@ -223,31 +171,25 @@ def reload(self) -> "ActiveSequence":
223171
return self
224172

225173

226-
class ActiveFinderMethods(ActiveSequence[T_co], ABC, Generic[T_co]):
227-
"""
228-
Finder methods.
174+
class ActiveFinderMethods(ActiveSequence[T], ABC, Generic[T]):
175+
def __init__(self, ctx: Context, parent: Optional[Active] = None, uid: str = "guid"):
176+
"""Finder methods.
229177
230-
Provides various finder methods for locating records in any endpoint supporting HTTP GET requests.
178+
Provides various finder methods for locating records in any endpoint supporting HTTP GET requests.
231179
232-
Attributes
233-
----------
234-
_uid : str
235-
The default field name used to uniquely identify records. Defaults to 'guid'.
236-
237-
Methods
238-
-------
239-
find(uid) -> T_co
240-
Finds and returns a record by its unique identifier (`uid`). If a cached result exists, it searches within the cache;
241-
otherwise, it makes a GET request to retrieve the data from the endpoint.
242-
243-
find_by(**conditions: Any) -> Optional[T_co]
244-
Finds the first record that matches the provided conditions. If no record is found, returns None.
245-
"""
246-
247-
_uid: str = "guid"
248-
"""The default field name used to uniquely identify records. Defaults to 'guid'."""
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
249191

250-
def find(self, uid) -> T_co:
192+
def find(self, uid) -> T:
251193
"""
252194
Find a record by its unique identifier.
253195
@@ -260,28 +202,29 @@ def find(self, uid) -> T_co:
260202
261203
Returns
262204
-------
263-
T_co
205+
T
264206
265207
Raises
266208
------
267209
ValueError
268210
If no record is found.
269211
"""
212+
# todo - add some more comments about this
270213
if self._cache:
271214
conditions = {self._uid: uid}
272215
result = self.find_by(**conditions)
273216
else:
274217
endpoint = self._endpoint + uid
275218
response = self._ctx.session.get(endpoint)
276219
result = response.json()
277-
result = self._cls(self._ctx, self._parent, **result)
220+
result = self._create_instance(**result)
278221

279222
if not result:
280223
raise ValueError(f"Failed to find instance where {self._uid} is '{uid}'")
281224

282225
return result
283226

284-
def find_by(self, **conditions: Any) -> Optional[T_co]:
227+
def find_by(self, **conditions: Any) -> Optional[T]:
285228
"""
286229
Find the first record matching the specified conditions.
287230
@@ -293,7 +236,7 @@ def find_by(self, **conditions: Any) -> Optional[T_co]:
293236
294237
Returns
295238
-------
296-
Optional[T_co]
239+
Optional[T]
297240
The first record matching the conditions, or `None` if no match is found.
298241
"""
299242
return next((v for v in self._data if v.items() >= conditions.items()), None)

0 commit comments

Comments
 (0)