Skip to content

Commit 279fcd6

Browse files
committed
refactor: introduce the active pattern
1 parent 6b79912 commit 279fcd6

File tree

4 files changed

+92
-84
lines changed

4 files changed

+92
-84
lines changed

src/posit/connect/content.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from . import tasks
1111
from .bundles import Bundles
12+
from .context import Context
1213
from .env import EnvVars
1314
from .jobs import JobsMixin
1415
from .oauth.associations import ContentItemAssociations
@@ -34,6 +35,10 @@ class ContentItemOwner(Resource):
3435

3536

3637
class ContentItem(JobsMixin, VanityMixin, Resource):
38+
def __init__(self, /, params: ResourceParameters, **kwargs):
39+
ctx = Context(params.session, params.url)
40+
super().__init__(ctx, **kwargs)
41+
3742
def __getitem__(self, key: Any) -> Any:
3843
v = super().__getitem__(key)
3944
if key == "owner" and isinstance(v, dict):

src/posit/connect/context.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def version(self) -> Optional[str]:
4040
return value
4141

4242
@version.setter
43-
def version(self, value: str):
43+
def version(self, value):
4444
self["version"] = value
4545

4646

src/posit/connect/jobs.py

Lines changed: 22 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
from typing import List, Literal, Optional, Sequence, TypedDict, overload
1+
from typing import Literal, Optional, TypedDict, overload
22

33
from typing_extensions import NotRequired, Required, Unpack
44

5-
from .errors import ClientError
6-
from .resources import FinderMethods, Resource, ResourceParameters, Resources
5+
from .resources import Active, ActiveFinderMethods, Resource
76

87
JobTag = Literal[
98
"unknown",
@@ -32,7 +31,7 @@
3231
]
3332

3433

35-
class Job(Resource):
34+
class Job(Active):
3635
class _Job(TypedDict):
3736
# Identifiers
3837
id: Required[str]
@@ -100,10 +99,12 @@ class _Job(TypedDict):
10099
tag: Required[JobTag]
101100
"""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."""
102101

103-
def __init__(self, /, params, endpoint, **kwargs: Unpack[_Job]):
102+
def __init__(self, /, params, **kwargs: Unpack[_Job]):
104103
super().__init__(params, **kwargs)
105-
key = kwargs["key"]
106-
self._endpoint = endpoint + key
104+
105+
@property
106+
def _endpoint(self) -> str:
107+
return self._ctx.url + f"v1/content/{self['app_id']}/jobs/{self['key']}"
107108

108109
def destroy(self) -> None:
109110
"""Destroy the job.
@@ -118,48 +119,21 @@ def destroy(self) -> None:
118119
----
119120
This action requires administrator, owner, or collaborator privileges.
120121
"""
121-
self.params.session.delete(self._endpoint)
122+
self._ctx.session.delete(self._endpoint)
122123

123124

124-
class Jobs(FinderMethods[Job], Sequence[Job], Resources):
125+
class Jobs(ActiveFinderMethods[Job]):
125126
"""A collection of jobs."""
126127

127-
def __init__(self, params, endpoint):
128-
super().__init__(Job, params, endpoint)
129-
self._endpoint = endpoint + "jobs"
130-
self._cache = None
131-
132-
@property
133-
def _data(self) -> List[Job]:
134-
if self._cache:
135-
return self._cache
136-
137-
response = self.params.session.get(self._endpoint)
138-
results = response.json()
139-
self._cache = [Job(self.params, self._endpoint, **result) for result in results]
140-
return self._cache
141-
142-
def __getitem__(self, index):
143-
"""Retrieve an item or slice from the sequence."""
144-
return self._data[index]
145-
146-
def __len__(self):
147-
"""Return the length of the sequence."""
148-
return len(self._data)
128+
_uid = "key"
149129

150-
def __repr__(self):
151-
"""Return the string representation of the sequence."""
152-
return f"Jobs({', '.join(map(str, self._data))})"
130+
def __init__(self, cls, ctx, parent: Active):
131+
super().__init__(cls, ctx)
132+
self._parent = parent
153133

154-
def count(self, value):
155-
"""Return the number of occurrences of a value in the sequence."""
156-
return self._data.count(value)
157-
158-
def index(self, value, start=0, stop=None):
159-
"""Return the index of the first occurrence of a value in the sequence."""
160-
if stop is None:
161-
stop = len(self._data)
162-
return self._data.index(value, start, stop)
134+
@property
135+
def _endpoint(self) -> str:
136+
return self._ctx.url + f"v1/content/{self._parent['guid']}/jobs"
163137

164138
class _FindByRequest(TypedDict, total=False):
165139
# Identifiers
@@ -286,37 +260,18 @@ def find_by(self, **conditions: Unpack[_FindByRequest]) -> Optional[Job]:
286260
@overload
287261
def find_by(self, **conditions): ...
288262

289-
def find_by(self, **conditions):
290-
if "key" in conditions and self._cache is None:
291-
key = conditions["key"]
292-
try:
293-
return self.find(key)
294-
except ClientError as e:
295-
if e.http_status == 404:
296-
return None
297-
raise e
298-
263+
def find_by(self, **conditions) -> Optional[Job]:
299264
return super().find_by(**conditions)
300265

301-
def reload(self) -> "Jobs":
302-
"""Unload the cached jobs.
303266

304-
Forces the next access, if any, to query the jobs from the Connect server.
305-
"""
306-
self._cache = None
307-
return self
308-
309-
310-
class JobsMixin(Resource):
267+
class JobsMixin(Active, Resource):
311268
"""Mixin class to add a jobs attribute to a resource."""
312269

313270
class HasGuid(TypedDict):
314271
"""Has a guid."""
315272

316273
guid: Required[str]
317274

318-
def __init__(self, params: ResourceParameters, **kwargs: Unpack[HasGuid]):
319-
super().__init__(params, **kwargs)
320-
uid = kwargs["guid"]
321-
endpoint = self.params.url + f"v1/content/{uid}"
322-
self.jobs = Jobs(self.params, endpoint)
275+
def __init__(self, ctx, **kwargs):
276+
super().__init__(ctx, **kwargs)
277+
self.jobs = Jobs(Job, ctx, self)

src/posit/connect/resources.py

Lines changed: 64 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
import posixpath
12
import warnings
23
from abc import ABC, abstractmethod
34
from dataclasses import dataclass
4-
from typing import Any, Generic, List, Optional, Type, TypeVar
5+
from typing import Any, Generic, List, Optional, Sequence, Type, TypeVar
56

67
import requests
78

9+
from posit.connect.context import Context
10+
811
from .urls import Url
912

1013

@@ -47,29 +50,74 @@ def __init__(self, params: ResourceParameters) -> None:
4750
self.params = params
4851

4952

50-
T = TypeVar("T", bound=Resource)
53+
T = TypeVar("T", bound="Active", covariant=True)
54+
55+
56+
class Active(Resource):
57+
def __init__(self, ctx: Context, **kwargs):
58+
params = ResourceParameters(ctx.session, ctx.url)
59+
super().__init__(params, **kwargs)
60+
self._ctx = ctx
5161

5262

53-
class FinderMethods(
54-
Generic[T],
55-
ABC,
56-
Resources,
57-
):
58-
def __init__(self, cls: Type[T], params, endpoint):
59-
super().__init__(params)
63+
class ActiveReader(ABC, Generic[T], Sequence[T]):
64+
def __init__(self, cls: Type[T], ctx: Context):
65+
super().__init__()
6066
self._cls = cls
61-
self._endpoint = endpoint
67+
self._ctx = ctx
68+
self._cache = None
6269

6370
@property
6471
@abstractmethod
65-
def _data(self) -> List[T]:
72+
def _endpoint(self) -> str:
6673
raise NotImplementedError()
6774

68-
def find(self, uid):
69-
endpoint = self._endpoint + str(uid)
70-
response = self.params.session.get(endpoint)
71-
result = response.json()
72-
return self._cls(self.params, endpoint=self._endpoint, **result)
75+
@property
76+
def _data(self) -> List[T]:
77+
if self._cache:
78+
return self._cache
79+
80+
response = self._ctx.session.get(self._endpoint)
81+
results = response.json()
82+
self._cache = [self._cls(self._ctx, **result) for result in results]
83+
return self._cache
84+
85+
def __getitem__(self, index):
86+
"""Retrieve an item or slice from the sequence."""
87+
return self._data[index]
88+
89+
def __len__(self):
90+
"""Return the length of the sequence."""
91+
return len(self._data)
92+
93+
def __str__(self):
94+
return str(self._data)
95+
96+
def __repr__(self):
97+
return repr(self._data)
98+
99+
def reload(self):
100+
self._cache = None
101+
return self
102+
103+
104+
class ActiveFinderMethods(ActiveReader[T], ABC, Generic[T]):
105+
_uid: str = "guid"
106+
107+
def find(self, uid) -> T:
108+
if self._cache:
109+
conditions = {self._uid: uid}
110+
result = self.find_by(**conditions)
111+
else:
112+
endpoint = posixpath.join(self._endpoint + uid)
113+
response = self._ctx.session.get(endpoint)
114+
result = response.json()
115+
result = self._cls(self._ctx, **result)
116+
117+
if not result:
118+
raise ValueError("")
119+
120+
return result
73121

74122
def find_by(self, **conditions: Any) -> Optional[T]:
75123
"""Finds the first record matching the specified conditions.

0 commit comments

Comments
 (0)