Skip to content
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
7 changes: 7 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# connectapi (development version)

## Breaking changes

- `get_jobs()` now uses the public endpoint. The data returned by this function
has changed. In particular, the `finalized` column is no longer present,
replaced by `status`. `status == 0` corresponds to `isFALSE(finalized)`. See
`?get_jobs()` for more details about the new return format. (#340)

## New features

- `get_users()` now supports filtering users with the `account_status` and
Expand Down
1 change: 1 addition & 0 deletions R/connectapi.R
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,6 @@ current_connect_version <- "2024.03.0"

.onLoad <- function(...) {
vctrs::s3_register("dplyr::collect", "tbl_connect")
vctrs::s3_register("vctrs::vec_cast", "character.integer")
invisible()
}
83 changes: 72 additions & 11 deletions R/content.R
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,23 @@ Content <- R6::R6Class(
},
#' @description Return the jobs for this content.
jobs = function() {
warn_experimental("jobs")
url <- unversioned_url("applications", self$get_content()$guid, "jobs")
res <- self$get_connect()$GET(url)
res <- self$connect$GET(v1_url("content", self$content$guid, "jobs"), parser = NULL)
use_unversioned <- endpoint_does_not_exist(res)
if (use_unversioned) {
res <- self$connect$GET(unversioned_url("applications", self$content$guid, "jobs"), parser = NULL)
}
self$connect$raise_error(res)
parsed <- httr::content(res, as = "parsed")
if (use_unversioned) {
# The unversioned endpoint does not contain a `status` field. Its field
# `finalized` is `FALSE` corresponds to active jobs. The `finalized`
# field is dropped during parsing.
parsed <- purrr::modify_if(parsed, ~ isFALSE(.x$finalized), function(x) {
x$status <- 0
x
})
}
parsed
},
#' @description Return a single job for this content.
#' @param key The job key.
Expand Down Expand Up @@ -588,26 +602,73 @@ content_ensure <- function(

#' Get Jobs
#'
#' `r lifecycle::badge('experimental')` Retrieve details about jobs associated with a `content_item`.
#' "Jobs" in Posit Connect are content executions
#' Retrieve details about server processes associated with a `content_item`,
#' such as a FastAPI app or a Quarto render.
#'
#' Note that Connect versions below 2022.10.0 use a legacy endpoint, and will
#' not return the complete set of information provided by newer versions.
#'
#' @param content A Content object, as returned by `content_item()`
#' @param key The key for a job
#'
#' @rdname jobs
#' @return A data frame with a row for each job, with the following columns:
#'
#' - `id`: The job identifier.
#' - `ppid`: The job's parent process identifier (see Note 1).
#' - `pid`: The job's process identifier.
#' - `key`: The job's unique key identifier.
#' - `remote_id`: The job's identifier for off-host execution configurations
#' (see Note 1).
#' - `app_id`: The job's parent content identifier
#' - `variant_id`: The identifier of the variant owning this job.
#' - `bundle_id`: The identifier of a content bundle linked to this job.
#' - `start_time`: The timestamp (RFC3339) indicating when this job started.
#' - `end_time`: The timestamp (RFC3339) indicating when this job finished.
#' - `last_heartbeat_time`: The timestamp (RFC3339) indicating the last time
#' this job was observed to be running (see Note 1).
#' - `queued_time`: The timestamp (RFC3339) indicating when this job was added
#' to the queue to be processed. Only scheduled reports will present a value
#' for this field (see Note 1).
#' - `queue_name`: The name of the queue which processes the job. Only
#' scheduled reports will present a value for this field (see Note 1).
#' - `tag`: A tag to identify the nature of the job.
#' - `exit_code`: The job's exit code. Present only when job is finished.
#' - `status`: The current status of the job. On Connect 2022.10.0 and newer,
#' one of Active: 0, Finished: 1, Finalized: 2; on earlier versions, Active:
#' 0, otherwise `NA`.
#' - `hostname`: The name of the node which processes the job.
#' - `cluster`: The location where this content runs. Content running on the
#' same server as Connect will have either a null value or the string Local.
#' Gives the name of the cluster when run external to the Connect host
#' (see Note 1).
#' - `image`: The location where this content runs. Content running on
#' the same server as Connect will have either a null value or the string
#' Local. References the name of the target image when content runs in
#' a clustered environment such as Kubernetes (see Note 1).
#' - `run_as`: The UNIX user that executed this job.
#'
#' @note
#' 1. On Connect instances earlier than 2022.10.0, these columns will contain `NA` values.
#'
#' @family job functions
#' @family content functions
#' @export
get_jobs <- function(content) {
warn_experimental("get_jobs")
scoped_experimental_silence()
validate_R6_class(content, "Content")

jobs <- content$jobs()
parse_connectapi_typed(jobs, connectapi_ptypes$jobs)
parse_connectapi_typed(jobs, connectapi_ptypes$jobs, order_columns = TRUE)
}

# TODO: Need to test `logged_error` on a real error
#' @rdname jobs
#'
#' Retrieve details about a server process
#' associated with a `content_item`, such as a FastAPI app or a Quarto render.
#'
#' @param content A Content object, as returned by `content_item()`
#' @param key The key for a job
#'
#' @family job functions
#' @family content functions
#' @export
get_job <- function(content, key) {
warn_experimental("get_job")
Expand Down
17 changes: 13 additions & 4 deletions R/parse.R
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,19 @@ make_timestamp <- function(input) {
safe_format(input, "%Y-%m-%dT%H:%M:%SZ", tz = "UTC", usetz = FALSE)
}

ensure_columns <- function(.data, ptype) {
ensure_columns <- function(.data, ptype, order_columns = FALSE) {
# Given a prototype, ensure that all columns are present and cast to the correct type.
# If a column is missing in .data, it will be created with all missing values of the correct type.
# If a column is present in both, it will be cast to the correct type.
# If a column is present in .data but not in ptype, it will be left as is.
for (i in names(ptype)) {
.data <- ensure_column(.data, ptype[[i]], i)
}

if (order_columns) {
.data <- .data[, unique(c(names(ptype), names(.data))), drop = FALSE]
}

.data
}

Expand All @@ -59,14 +64,14 @@ ensure_column <- function(data, default, name) {
if (inherits(default, "list") && !inherits(col, "list")) {
col <- list(col)
}
col <- vctrs::vec_cast(col, default)
col <- vctrs::vec_cast(col, default, x_arg = name)
}
data[[name]] <- col
data
}

parse_connectapi_typed <- function(data, ptype) {
ensure_columns(parse_connectapi(data), ptype)
parse_connectapi_typed <- function(data, ptype, order_columns = FALSE) {
ensure_columns(parse_connectapi(data), ptype, order_columns)
}

parse_connectapi <- function(data) {
Expand Down Expand Up @@ -185,6 +190,10 @@ tzone <- function(x) {
attr(x, "tzone")[[1]] %||% ""
}

vec_cast.character.integer <- function(x, to, ...) { # nolint: object_name_linter
as.character(x)
}

new_datetime <- function(x = double(), tzone = "") {
tzone <- tzone %||% ""
if (is.integer(x)) {
Expand Down
22 changes: 14 additions & 8 deletions R/ptype.R
Original file line number Diff line number Diff line change
Expand Up @@ -161,20 +161,26 @@ connectapi_ptypes <- list(
variant_key = NA_character_,
),
jobs = tibble::tibble(
id = NA_integer_,
pid = NA_integer_,
id = NA_character_,
ppid = NA_character_,
pid = NA_character_,
key = NA_character_,
app_id = NA_integer_,
app_guid = NA_character_,
variant_id = NA_integer_,
bundle_id = NA_integer_,
remote_id = NA_character_,
app_id = NA_character_,
variant_id = NA_character_,
bundle_id = NA_character_,
start_time = NA_datetime_,
end_time = NA_datetime_,
last_heartbeat_time = NA_datetime_,
queued_time = NA_datetime_,
queue_name = NA_character_,
tag = NA_character_,
exit_code = NA_integer_,
finalized = NA,
status = NA_integer_,
hostname = NA_character_,
variant_key = NA_character_
cluster = NA_character_,
image = NA_character_,
run_as = NA_character_,
),
job = tibble::tibble(
pid = NA_integer_,
Expand Down
10 changes: 10 additions & 0 deletions R/utils.R
Original file line number Diff line number Diff line change
Expand Up @@ -186,3 +186,13 @@ token_hex <- function(n) {
raw <- as.raw(sample(0:255, n, replace = TRUE))
paste(as.character(raw), collapse = "")
}

# Checks to see if an http status contains a 404 error, and that that 404
# response does not contain an error code indicating that an endpoint was called
# but encountered a 404 for some other reason.
endpoint_does_not_exist <- function(res) {
return(
httr::status_code(res) == "404" &&
!("code" %in% names(httr::content(res, as = "parsed")))
)
}
2 changes: 1 addition & 1 deletion man/Content.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions man/content_delete.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions man/content_item.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions man/content_title.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions man/content_update.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions man/create_random_name.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions man/dashboard_url.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions man/dashboard_url_chr.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions man/delete_thumbnail.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions man/delete_vanity_url.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions man/deploy_repo.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions man/environment.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions man/get_bundles.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions man/get_image.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 10 additions & 7 deletions man/jobs.Rd → man/get_job.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading