Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add jobs #314

Merged
merged 9 commits into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions integration/tests/posit/connect/test_jobs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from pathlib import Path

import pytest
from packaging import version

from posit import connect

from . import CONNECT_VERSION


class TestJobs:
@classmethod
def setup_class(cls):
cls.client = connect.Client()
cls.content = cls.client.content.create(name="example-quarto-minimal")

@classmethod
def teardown_class(cls):
cls.content.delete()
assert cls.client.content.count() == 0

@pytest.mark.skipif(
CONNECT_VERSION <= version.parse("2023.01.1"),
reason="Quarto not available",
)
def test(self):
content = self.content

path = Path("../../../resources/connect/bundles/example-quarto-minimal/bundle.tar.gz")
path = Path(__file__).parent / path
tdstein marked this conversation as resolved.
Show resolved Hide resolved
path = path.resolve()
path = str(path)

bundle = content.bundles.create(path)
bundle.deploy()

jobs = content.jobs
assert len(jobs) == 1
8 changes: 7 additions & 1 deletion src/posit/connect/content.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@

from . import tasks
from .bundles import Bundles
from .context import Context
from .env import EnvVars
from .jobs import JobsMixin
from .oauth.associations import ContentItemAssociations
from .permissions import Permissions
from .resources import Resource, ResourceParameters, Resources
Expand All @@ -32,7 +34,11 @@ class ContentItemOwner(Resource):
pass


class ContentItem(VanityMixin, Resource):
class ContentItem(JobsMixin, VanityMixin, Resource):
def __init__(self, /, params: ResourceParameters, **kwargs):
schloerke marked this conversation as resolved.
Show resolved Hide resolved
ctx = Context(params.session, params.url)
super().__init__(ctx, **kwargs)

def __getitem__(self, key: Any) -> Any:
v = super().__getitem__(key)
if key == "owner" and isinstance(v, dict):
Expand Down
6 changes: 4 additions & 2 deletions src/posit/connect/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import requests
from packaging.version import Version

from .urls import Url


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


class Context(dict):
def __init__(self, session: requests.Session, url: str):
def __init__(self, session: requests.Session, url: Url):
self.session = session
self.url = url

Expand All @@ -38,7 +40,7 @@ def version(self) -> Optional[str]:
return value

@version.setter
def version(self, value: str):
def version(self, value):
self["version"] = value


Expand Down
292 changes: 292 additions & 0 deletions src/posit/connect/jobs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
from typing import Literal, Optional, TypedDict, overload

from typing_extensions import NotRequired, Required, Unpack

from .resources import Active, ActiveFinderMethods, ActiveSequence, Resource

JobTag = Literal[
tdstein marked this conversation as resolved.
Show resolved Hide resolved
"unknown",
"build_report",
"build_site",
"build_jupyter",
"packrat_restore",
"python_restore",
"configure_report",
"run_app",
"run_api",
"run_tensorflow",
"run_python_api",
"run_dash_app",
"run_streamlit",
"run_bokeh_app",
"run_fastapi_app",
"run_pyshiny_app",
"render_shiny",
"run_voila_app",
"testing",
"git",
"val_py_ext_pkg",
"val_r_ext_pkg",
"val_r_install",
]


class Job(Active):
class _Job(TypedDict):
schloerke marked this conversation as resolved.
Show resolved Hide resolved
# Identifiers
id: Required[str]
tdstein marked this conversation as resolved.
Show resolved Hide resolved
"""A unique identifier for the job."""

ppid: Required[Optional[str]]
"""Identifier of the parent process."""

pid: Required[str]
"""Identifier of the process running the job."""

key: Required[str]
"""A unique key to identify this job."""

remote_id: Required[Optional[str]]
"""Identifier for off-host execution configurations."""

app_id: Required[str]
"""Identifier of the parent content associated with the job."""

variant_id: Required[str]
"""Identifier of the variant responsible for the job."""

bundle_id: Required[str]
"""Identifier of the content bundle linked to the job."""

# Timestamps
start_time: Required[str]
"""RFC3339 timestamp indicating when the job started."""

end_time: Required[Optional[str]]
"""RFC3339 timestamp indicating when the job finished."""

last_heartbeat_time: Required[str]
"""RFC3339 timestamp of the last recorded activity for the job."""

queued_time: Required[Optional[str]]
"""RFC3339 timestamp when the job was added to the queue."""

# Status and Exit Information
status: Required[Literal[0, 1, 2]]
"""Current status. Options are 0 (Active), 1 (Finished), and 2 (Finalized)"""

exit_code: Required[Optional[int]]
"""The job's exit code, available after completion."""

# Environment Information
hostname: Required[str]
"""Name of the node processing the job."""

cluster: Required[Optional[str]]
"""Location where the job runs, either 'Local' or the cluster name."""

image: Required[Optional[str]]
"""Location of the content in clustered environments."""

run_as: Required[str]
"""UNIX user responsible for executing the job."""

# Queue and Scheduling Information
queue_name: Required[Optional[str]]
"""Name of the queue processing the job, relevant for scheduled reports."""

# Job Metadata
tag: Required[JobTag]
"""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."""

def __init__(self, ctx, parent: Active, **kwargs: Unpack[_Job]):
super().__init__(ctx, parent, **kwargs)
self._parent = parent

@property
def _endpoint(self) -> str:
return self._ctx.url + f"v1/content/{self._parent['guid']}/jobs/{self['key']}"
tdstein marked this conversation as resolved.
Show resolved Hide resolved

def destroy(self) -> None:
"""Destroy the job.

Submit a request to kill the job.

Warnings
--------
This operation is irreversible.

Note
----
This action requires administrator, owner, or collaborator privileges.
"""
self._ctx.session.delete(self._endpoint)


class Jobs(
ActiveFinderMethods[Job],
ActiveSequence[Job],
):
def __init__(self, ctx, parent: Active, uid="key"):
"""A collection of jobs.

Parameters
----------
ctx : Context
The context containing the HTTP session used to interact with the API.
parent : Active
Parent resource for maintaining hierarchical relationships
uid : str, optional
The default field name used to uniquely identify records, by default "key"
"""
super().__init__(ctx, parent, uid)
self._parent = parent

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

def _create_instance(self, **kwargs) -> Job:
"""Creates a `Job` instance.

Returns
-------
Job
"""
return Job(self._ctx, self._parent, **kwargs)

class _FindByRequest(TypedDict, total=False):
# Identifiers
id: Required[str]
"""A unique identifier for the job."""

ppid: NotRequired[Optional[str]]
"""Identifier of the parent process."""

pid: NotRequired[str]
"""Identifier of the process running the job."""

key: NotRequired[str]
"""A unique key to identify this job."""

remote_id: NotRequired[Optional[str]]
"""Identifier for off-host execution configurations."""

app_id: NotRequired[str]
"""Identifier of the parent content associated with the job."""

variant_id: NotRequired[str]
"""Identifier of the variant responsible for the job."""

bundle_id: NotRequired[str]
"""Identifier of the content bundle linked to the job."""

# Timestamps
start_time: NotRequired[str]
"""RFC3339 timestamp indicating when the job started."""

end_time: NotRequired[Optional[str]]
schloerke marked this conversation as resolved.
Show resolved Hide resolved
"""RFC3339 timestamp indicating when the job finished."""

last_heartbeat_time: NotRequired[str]
"""RFC3339 timestamp of the last recorded activity for the job."""

queued_time: NotRequired[Optional[str]]
"""RFC3339 timestamp when the job was added to the queue."""

# Status and Exit Information
status: NotRequired[Literal[0, 1, 2]]
"""Current status. Options are 0 (Active), 1 (Finished), and 2 (Finalized)"""

exit_code: NotRequired[Optional[int]]
"""The job's exit code, available after completion."""

# Environment Information
hostname: NotRequired[str]
"""Name of the node processing the job."""

cluster: NotRequired[Optional[str]]
"""Location where the job runs, either 'Local' or the cluster name."""

image: NotRequired[Optional[str]]
"""Location of the content in clustered environments."""

run_as: NotRequired[str]
"""UNIX user responsible for executing the job."""

# Queue and Scheduling Information
queue_name: NotRequired[Optional[str]]
"""Name of the queue processing the job, relevant for scheduled reports."""

# Job Metadata
tag: NotRequired[JobTag]
"""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."""

@overload
def find_by(self, **conditions: Unpack[_FindByRequest]) -> Optional[Job]:
"""Finds the first record matching the specified conditions.
tdstein marked this conversation as resolved.
Show resolved Hide resolved

There is no implied ordering so if order matters, you should specify it yourself.

Parameters
----------
id : str, not required
A unique identifier for the job.
ppid : Optional[str], not required
Identifier of the parent process.
pid : str, not required
Identifier of the process running the job.
key : str, not required
A unique key to identify this job.
remote_id : Optional[str], not required
Identifier for off-host execution configurations.
app_id : str, not required
Identifier of the parent content associated with the job.
variant_id : str, not required
Identifier of the variant responsible for the job.
bundle_id : str, not required
Identifier of the content bundle linked to the job.
start_time : str, not required
RFC3339 timestamp indicating when the job started.
end_time : Optional[str], not required
RFC3339 timestamp indicating when the job finished.
last_heartbeat_time : str, not required
RFC3339 timestamp of the last recorded activity for the job.
queued_time : Optional[str], not required
RFC3339 timestamp when the job was added to the queue.
status : int, not required
Current status. Options are 0 (Active), 1 (Finished), and 2 (Finalized)
exit_code : Optional[int], not required
The job's exit code, available after completion.
hostname : str, not required
Name of the node processing the job.
cluster : Optional[str], not required
Location where the job runs, either 'Local' or the cluster name.
image : Optional[str], not required
Location of the content in clustered environments.
run_as : str, not required
UNIX user responsible for executing the job.
queue_name : Optional[str], not required
Name of the queue processing the job, relevant for scheduled reports.
tag : JobTag, not required
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.

Returns
-------
Optional[Job]
"""
...

@overload
def find_by(self, **conditions): ...

def find_by(self, **conditions) -> Optional[Job]:
return super().find_by(**conditions)


class JobsMixin(Active, Resource):
"""Mixin class to add a jobs attribute to a resource."""

def __init__(self, ctx, **kwargs):
tdstein marked this conversation as resolved.
Show resolved Hide resolved
super().__init__(ctx, **kwargs)
self.jobs = Jobs(ctx, self)
Loading