Skip to content

Commit 6b79912

Browse files
committed
--wip-- [skip ci]
1 parent 0533f19 commit 6b79912

File tree

5 files changed

+302
-200
lines changed

5 files changed

+302
-200
lines changed

src/posit/connect/context.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import requests
55
from packaging.version import Version
66

7+
from .urls import Url
8+
79

810
def requires(version: str):
911
def decorator(func):
@@ -22,7 +24,7 @@ def wrapper(instance: ContextManager, *args, **kwargs):
2224

2325

2426
class Context(dict):
25-
def __init__(self, session: requests.Session, url: str):
27+
def __init__(self, session: requests.Session, url: Url):
2628
self.session = session
2729
self.url = url
2830

src/posit/connect/jobs.py

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

3-
from typing_extensions import Required, Unpack
3+
from typing_extensions import NotRequired, Required, Unpack
44

5-
from .resources import Resource, ResourceParameters, Resources
5+
from .errors import ClientError
6+
from .resources import FinderMethods, Resource, ResourceParameters, Resources
7+
8+
JobTag = Literal[
9+
"unknown",
10+
"build_report",
11+
"build_site",
12+
"build_jupyter",
13+
"packrat_restore",
14+
"python_restore",
15+
"configure_report",
16+
"run_app",
17+
"run_api",
18+
"run_tensorflow",
19+
"run_python_api",
20+
"run_dash_app",
21+
"run_streamlit",
22+
"run_bokeh_app",
23+
"run_fastapi_app",
24+
"run_pyshiny_app",
25+
"render_shiny",
26+
"run_voila_app",
27+
"testing",
28+
"git",
29+
"val_py_ext_pkg",
30+
"val_r_ext_pkg",
31+
"val_r_install",
32+
]
633

734

835
class Job(Resource):
9-
pass
36+
class _Job(TypedDict):
37+
# Identifiers
38+
id: Required[str]
39+
"""A unique identifier for the job."""
40+
41+
ppid: Required[Optional[str]]
42+
"""Identifier of the parent process."""
43+
44+
pid: Required[str]
45+
"""Identifier of the process running the job."""
46+
47+
key: Required[str]
48+
"""A unique key to identify this job."""
49+
50+
remote_id: Required[Optional[str]]
51+
"""Identifier for off-host execution configurations."""
52+
53+
app_id: Required[str]
54+
"""Identifier of the parent content associated with the job."""
55+
56+
variant_id: Required[str]
57+
"""Identifier of the variant responsible for the job."""
58+
59+
bundle_id: Required[str]
60+
"""Identifier of the content bundle linked to the job."""
61+
62+
# Timestamps
63+
start_time: Required[str]
64+
"""RFC3339 timestamp indicating when the job started."""
65+
66+
end_time: Required[Optional[str]]
67+
"""RFC3339 timestamp indicating when the job finished."""
68+
69+
last_heartbeat_time: Required[str]
70+
"""RFC3339 timestamp of the last recorded activity for the job."""
71+
72+
queued_time: Required[Optional[str]]
73+
"""RFC3339 timestamp when the job was added to the queue."""
74+
75+
# Status and Exit Information
76+
status: Required[Literal[0, 1, 2]]
77+
"""Current status. Options are 0 (Active), 1 (Finished), and 2 (Finalized)"""
78+
79+
exit_code: Required[Optional[int]]
80+
"""The job's exit code, available after completion."""
1081

82+
# Environment Information
83+
hostname: Required[str]
84+
"""Name of the node processing the job."""
1185

12-
class Jobs(Resources, Sequence[Job]):
86+
cluster: Required[Optional[str]]
87+
"""Location where the job runs, either 'Local' or the cluster name."""
88+
89+
image: Required[Optional[str]]
90+
"""Location of the content in clustered environments."""
91+
92+
run_as: Required[str]
93+
"""UNIX user responsible for executing the job."""
94+
95+
# Queue and Scheduling Information
96+
queue_name: Required[Optional[str]]
97+
"""Name of the queue processing the job, relevant for scheduled reports."""
98+
99+
# Job Metadata
100+
tag: Required[JobTag]
101+
"""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."""
102+
103+
def __init__(self, /, params, endpoint, **kwargs: Unpack[_Job]):
104+
super().__init__(params, **kwargs)
105+
key = kwargs["key"]
106+
self._endpoint = endpoint + key
107+
108+
def destroy(self) -> None:
109+
"""Destroy the job.
110+
111+
Submit a request to kill the job.
112+
113+
Warnings
114+
--------
115+
This operation is irreversible.
116+
117+
Note
118+
----
119+
This action requires administrator, owner, or collaborator privileges.
120+
"""
121+
self.params.session.delete(self._endpoint)
122+
123+
124+
class Jobs(FinderMethods[Job], Sequence[Job], Resources):
13125
"""A collection of jobs."""
14126

15127
def __init__(self, params, endpoint):
16-
super().__init__(params)
17-
self._endpoint = endpoint
128+
super().__init__(Job, params, endpoint)
129+
self._endpoint = endpoint + "jobs"
18130
self._cache = None
19131

20132
@property
@@ -24,7 +136,7 @@ def _data(self) -> List[Job]:
24136

25137
response = self.params.session.get(self._endpoint)
26138
results = response.json()
27-
self._cache = [Job(self.params, **result) for result in results]
139+
self._cache = [Job(self.params, self._endpoint, **result) for result in results]
28140
return self._cache
29141

30142
def __getitem__(self, index):
@@ -49,6 +161,143 @@ def index(self, value, start=0, stop=None):
49161
stop = len(self._data)
50162
return self._data.index(value, start, stop)
51163

164+
class _FindByRequest(TypedDict, total=False):
165+
# Identifiers
166+
id: NotRequired[str]
167+
"""A unique identifier for the job."""
168+
169+
ppid: NotRequired[Optional[str]]
170+
"""Identifier of the parent process."""
171+
172+
pid: NotRequired[str]
173+
"""Identifier of the process running the job."""
174+
175+
key: NotRequired[str]
176+
"""A unique key to identify this job."""
177+
178+
remote_id: NotRequired[Optional[str]]
179+
"""Identifier for off-host execution configurations."""
180+
181+
app_id: NotRequired[str]
182+
"""Identifier of the parent content associated with the job."""
183+
184+
variant_id: NotRequired[str]
185+
"""Identifier of the variant responsible for the job."""
186+
187+
bundle_id: NotRequired[str]
188+
"""Identifier of the content bundle linked to the job."""
189+
190+
# Timestamps
191+
start_time: NotRequired[str]
192+
"""RFC3339 timestamp indicating when the job started."""
193+
194+
end_time: NotRequired[Optional[str]]
195+
"""RFC3339 timestamp indicating when the job finished."""
196+
197+
last_heartbeat_time: NotRequired[str]
198+
"""RFC3339 timestamp of the last recorded activity for the job."""
199+
200+
queued_time: NotRequired[Optional[str]]
201+
"""RFC3339 timestamp when the job was added to the queue."""
202+
203+
# Status and Exit Information
204+
status: NotRequired[Literal[0, 1, 2]]
205+
"""Current status. Options are 0 (Active), 1 (Finished), and 2 (Finalized)"""
206+
207+
exit_code: NotRequired[Optional[int]]
208+
"""The job's exit code, available after completion."""
209+
210+
# Environment Information
211+
hostname: NotRequired[str]
212+
"""Name of the node processing the job."""
213+
214+
cluster: NotRequired[Optional[str]]
215+
"""Location where the job runs, either 'Local' or the cluster name."""
216+
217+
image: NotRequired[Optional[str]]
218+
"""Location of the content in clustered environments."""
219+
220+
run_as: NotRequired[str]
221+
"""UNIX user responsible for executing the job."""
222+
223+
# Queue and Scheduling Information
224+
queue_name: NotRequired[Optional[str]]
225+
"""Name of the queue processing the job, relevant for scheduled reports."""
226+
227+
# Job Metadata
228+
tag: NotRequired[JobTag]
229+
"""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."""
230+
231+
@overload
232+
def find_by(self, **conditions: Unpack[_FindByRequest]) -> Optional[Job]:
233+
"""Finds the first record matching the specified conditions.
234+
235+
There is no implied ordering so if order matters, you should specify it yourself.
236+
237+
Parameters
238+
----------
239+
id : str, not required
240+
A unique identifier for the job.
241+
ppid : Optional[str], not required
242+
Identifier of the parent process.
243+
pid : str, not required
244+
Identifier of the process running the job.
245+
key : str, not required
246+
A unique key to identify this job.
247+
remote_id : Optional[str], not required
248+
Identifier for off-host execution configurations.
249+
app_id : str, not required
250+
Identifier of the parent content associated with the job.
251+
variant_id : str, not required
252+
Identifier of the variant responsible for the job.
253+
bundle_id : str, not required
254+
Identifier of the content bundle linked to the job.
255+
start_time : str, not required
256+
RFC3339 timestamp indicating when the job started.
257+
end_time : Optional[str], not required
258+
RFC3339 timestamp indicating when the job finished.
259+
last_heartbeat_time : str, not required
260+
RFC3339 timestamp of the last recorded activity for the job.
261+
queued_time : Optional[str], not required
262+
RFC3339 timestamp when the job was added to the queue.
263+
status : int, not required
264+
Current status. Options are 0 (Active), 1 (Finished), and 2 (Finalized)
265+
exit_code : Optional[int], not required
266+
The job's exit code, available after completion.
267+
hostname : str, not required
268+
Name of the node processing the job.
269+
cluster : Optional[str], not required
270+
Location where the job runs, either 'Local' or the cluster name.
271+
image : Optional[str], not required
272+
Location of the content in clustered environments.
273+
run_as : str, not required
274+
UNIX user responsible for executing the job.
275+
queue_name : Optional[str], not required
276+
Name of the queue processing the job, relevant for scheduled reports.
277+
tag : JobTag, not required
278+
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.
279+
280+
Returns
281+
-------
282+
Optional[Job]
283+
"""
284+
...
285+
286+
@overload
287+
def find_by(self, **conditions): ...
288+
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+
299+
return super().find_by(**conditions)
300+
52301
def reload(self) -> "Jobs":
53302
"""Unload the cached jobs.
54303
@@ -69,5 +318,5 @@ class HasGuid(TypedDict):
69318
def __init__(self, params: ResourceParameters, **kwargs: Unpack[HasGuid]):
70319
super().__init__(params, **kwargs)
71320
uid = kwargs["guid"]
72-
endpoint = self.params.url + f"v1/content/{uid}/jobs"
321+
endpoint = self.params.url + f"v1/content/{uid}"
73322
self.jobs = Jobs(self.params, endpoint)

src/posit/connect/resources.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import warnings
2+
from abc import ABC, abstractmethod
23
from dataclasses import dataclass
4+
from typing import Any, Generic, List, Optional, Type, TypeVar
35

46
import requests
57

@@ -43,3 +45,39 @@ def update(self, *args, **kwargs):
4345
class Resources:
4446
def __init__(self, params: ResourceParameters) -> None:
4547
self.params = params
48+
49+
50+
T = TypeVar("T", bound=Resource)
51+
52+
53+
class FinderMethods(
54+
Generic[T],
55+
ABC,
56+
Resources,
57+
):
58+
def __init__(self, cls: Type[T], params, endpoint):
59+
super().__init__(params)
60+
self._cls = cls
61+
self._endpoint = endpoint
62+
63+
@property
64+
@abstractmethod
65+
def _data(self) -> List[T]:
66+
raise NotImplementedError()
67+
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)
73+
74+
def find_by(self, **conditions: Any) -> Optional[T]:
75+
"""Finds the first record matching the specified conditions.
76+
77+
There is no implied ordering so if order matters, you should specify it yourself.
78+
79+
Returns
80+
-------
81+
Optional[T]
82+
"""
83+
return next((v for v in self._data if v.items() >= conditions.items()), None)

src/posit/connect/vanities.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ class CreateVanityRequest(TypedDict, total=False):
201201
"""A request schema for creating a vanity."""
202202

203203
path: Required[str]
204-
"""The vanity path (.e.g, 'my-dashboard')"""
204+
"""The vanity path (e.g., 'my-dashboard')"""
205205

206206
force: NotRequired[bool]
207207
"""Whether to force creation of the vanity"""

0 commit comments

Comments
 (0)