Skip to content
Open
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
1 change: 0 additions & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@ Collate:
'page.R'
'parse.R'
'promote.R'
'ptype.R'
'remote.R'
'runtime-caches.R'
'schedule.R'
Expand Down
7 changes: 7 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

- When using integrations, prefer to read from `CONNECT_CONTENT_SESSION_TOKEN_FILE` to find the session token. This helps long-running processes ensure that they can maintain fresh credentials.

- Improved performance of API response parsing and pagination. Data frames
returned by getter functions now include all columns from the server, so new
fields added in future Connect releases will appear automatically. Column
names and types are now determined by the server response rather than a
hardcoded schema, so they may vary across Connect versions.


# connectapi 0.11.1

- `get_usage()` now returns the id column as a character to match other parts of the API (#512).
Expand Down
28 changes: 24 additions & 4 deletions R/connect.R
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,11 @@ Connect <- R6::R6Class(
#' @param parser How the response is parsed. If `NULL`, the `httr_response`
#' will be returned. Otherwise, the argument is forwarded to
#' `httr::content(res, as = parser)`.
#' @param simplify Logical; if `TRUE`, JSON arrays of objects are
#' simplified to data frames by jsonlite. Default `FALSE` preserves
#' list-of-lists for compatibility with pagination helpers.
#' @param ... Additional arguments passed to the request function
request = function(method, url, ..., parser = "parsed") {
request = function(method, url, ..., parser = "parsed", simplify = FALSE) {
old_opt <- options(scipen = 999)
on.exit(options(old_opt), add = TRUE)

Expand All @@ -161,7 +164,21 @@ Connect <- R6::R6Class(
res
} else {
self$raise_error(res)
httr::content(res, as = parser)
if (parser != "parsed") {
return(httr::content(res, as = parser))
}
if (is.null(res$content) || length(res$content) == 0) {
return(NULL)
}
content_text <- httr::content(res, as = "text", encoding = "UTF-8")
if (is.null(content_text) || nchar(content_text) == 0) {
return(NULL)
}
jsonlite::fromJSON(
content_text,
simplifyVector = simplify,
simplifyDataFrame = simplify
)
}
},

Expand All @@ -173,8 +190,11 @@ Connect <- R6::R6Class(
#' @param parser How the response is parsed. If `NULL`, the `httr_response`
#' will be returned. Otherwise, the argument is forwarded to
#' `httr::content(res, as = parser)`.
GET = function(path, ..., url = self$api_url(path), parser = "parsed") {
self$request("GET", url, parser = parser, ...)
#' @param simplify Logical; if `TRUE`, JSON arrays of objects are
#' simplified to data frames by jsonlite. Default `FALSE` preserves
#' list-of-lists for compatibility with pagination helpers.
GET = function(path, ..., url = self$api_url(path), parser = "parsed", simplify = FALSE) {
self$request("GET", url, parser = parser, simplify = simplify, ...)
},

#' @description Perform an HTTP PUT request of the named API path.
Expand Down
2 changes: 0 additions & 2 deletions R/connectapi.R
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ utils::globalVariables(
c(
".",
"access_type",
"connectapi_ptypes",
"guid",
"last_deployed_time",
"owner_guid",
Expand All @@ -27,6 +26,5 @@ current_connect_version <- "2024.03.0"

.onLoad <- function(...) {
vctrs::s3_register("dplyr::collect", "tbl_connect")
vctrs::s3_register("vctrs::vec_cast", "character.integer")
invisible()
}
47 changes: 32 additions & 15 deletions R/content.R
Original file line number Diff line number Diff line change
Expand Up @@ -788,7 +788,23 @@ get_jobs <- function(content) {
validate_R6_class(content, "Content")

jobs <- content$jobs()
parse_connectapi_typed(jobs, connectapi_ptypes$jobs, strict = TRUE)
out <- parse_connectapi_typed(
jobs,
datetime_cols = c("start_time", "end_time", "last_heartbeat_time", "queued_time")
)

# The older /applications/ endpoint returns timestamps as Unix epoch integers
# and ID fields as integers. Normalize to match the v1 endpoint's types.
# For the v1 endpoint these are already character/POSIXct, so the coercions
# are no-ops.
Comment on lines +796 to +799
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably not worth too much digging, but if you know off the top of your head: how old are older versions here?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GET /v1/content/{guid}/jobs was added in October 2022 https://docs.posit.co/connect/news/#rstudio-connect-2022.10.0; the old applications endpoint was removed July 2025.

out <- coerce_epoch_to_posixct(
out,
c("start_time", "end_time", "last_heartbeat_time", "queued_time")
)
coerce_to_character(
out,
c("id", "ppid", "pid", "app_id", "content_id", "variant_id", "bundle_id")
)
}

#' Terminate Jobs
Expand Down Expand Up @@ -832,22 +848,19 @@ terminate_jobs <- function(content, keys = NULL) {
keys <- all_jobs[all_jobs$status == 0, ]$key
if (length(keys) == 0) {
message("No active jobs found.")
return(vctrs::vec_ptype(connectapi_ptypes$job_termination))
return(tibble::tibble())
}
}

res <- purrr::map(keys, content$register_job_kill_order)
res_content <- purrr::map(res, httr::content)
res_df <- tibble::tibble(
parse_connectapi_typed(
res_content,
connectapi_ptypes$job_termination,
strict = TRUE
)
)
res_df <- parse_connectapi_typed(res_content)
# Errors will not have the job_key.
res_df$job_key <- keys
res_df
# Keep only the columns relevant to job termination; the API response
# includes extra fields (e.g. payload, guid) on error that vary by outcome.
keep <- c("app_id", "app_guid", "job_key", "job_id", "result", "code", "error")
Comment on lines +860 to +862
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it a problem that we get variable data at this point?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe that the variable data is for error cases (i.e. trying to terminate a job that is not currently active, which returns a 409 but doesn't raise an R error) vs. successful requests. I guess it's debatable whether we should be raising an R error more eagerly, but I think the behavior of accommodating different field names is consistent with main.

The one difference here is that main will always return the columns of keep whereas on this branch if any columns from keep are missing across all responses, they'd be omitted. I'll update to make it more consistent with main.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was actually thinking a bit in the opposite direction: if we sometimes get different fieldnames that's probably totally ok. This is probably more important (or really, more possible) when we get to having these objects not be DFs that get passed around. Then the normalization of "here are the columns you're getting" can be done at the as.data.frame() point). We don't need to do this here, it just smelled a little funny to me

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha yeah, if we were returning a list then I agree it'd make more sense to not worry about the columns -- the responses are what they are, and that may include different fields.

res_df[, keep, drop = FALSE]
}

#' @rdname get_jobs
Expand Down Expand Up @@ -896,7 +909,7 @@ get_log <- function(job, max_log_lines = NULL) {
v1_url("content", job$app_guid, "jobs", job$key, "log"),
query = query
)
parse_connectapi_typed(res$entries, connectapi_ptypes$job_log)
parse_connectapi_typed(res$entries, datetime_cols = "timestamp")
}

#' Set RunAs User
Expand Down Expand Up @@ -1141,7 +1154,8 @@ get_bundles <- function(content) {
validate_R6_class(content, "Content")
bundles <- content$get_bundles()

parse_connectapi_typed(bundles, connectapi_ptypes$bundles)
out <- parse_connectapi_typed(bundles, datetime_cols = "created_time")
coerce_fs_bytes(out, "size")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can keep this here, but I do wonder if providing these as fs::byte types is really useful?

}

#' @rdname get_bundles
Expand Down Expand Up @@ -1347,7 +1361,7 @@ get_group_permission <- function(content, guid) {
get_content_permissions <- function(content, add_owner = TRUE) {
validate_R6_class(content, "Content")
res <- content$permissions(add_owner = add_owner)
parse_connectapi_typed(res, connectapi_ptypes$permissions)
parse_connectapi_typed(res)
}

#' Render a content item.
Expand Down Expand Up @@ -1495,7 +1509,7 @@ content_restart <- function(content) {
get_content_packages <- function(content) {
error_if_less_than(content$connect$version, "2025.01.0")
res <- content$packages()
parse_connectapi_typed(res, connectapi_ptypes$content_packages)
parse_connectapi_typed(res)
}

#' Search for content on the Connect server
Expand Down Expand Up @@ -1627,5 +1641,8 @@ as.data.frame.connect_content_list <- function(
#' @export
as_tibble.connect_content_list <- function(x, ...) {
content_data <- purrr::map(x, "content")
parse_connectapi_typed(content_data, connectapi_ptypes$content)
parse_connectapi_typed(
content_data,
datetime_cols = datetime_columns$content
)
}
34 changes: 20 additions & 14 deletions R/get.R
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,10 @@ get_users <- function(
limit = limit
)

out <- parse_connectapi_typed(res, connectapi_ptypes$users)
out <- parse_connectapi_typed(
res,
datetime_cols = datetime_columns$users
)

return(out)
}
Expand Down Expand Up @@ -229,12 +232,8 @@ get_content <- function(
# v2024.06.0.
if (compare_connect_version(src$version, "2024.06.0") < 0) {
include <- "tags,owner"
content_ptype <- connectapi_ptypes$content[,
names(connectapi_ptypes$content) != "vanity_url"
]
} else {
include <- "tags,owner,vanity_url"
content_ptype <- connectapi_ptypes$content
}

res <- src$content(
Expand All @@ -253,7 +252,10 @@ get_content <- function(
res <- res %>% purrr::keep(.p = .p)
}

out <- parse_connectapi_typed(res, content_ptype)
out <- parse_connectapi_typed(
res,
datetime_cols = datetime_columns$content
)

return(out)
}
Expand Down Expand Up @@ -327,7 +329,10 @@ content_list_by_tag <- function(src, tag) {

res <- src$GET(v1_url("tags", tag_id, "content"))

out <- parse_connectapi_typed(res, connectapi_ptypes$content)
out <- parse_connectapi_typed(
res,
datetime_cols = datetime_columns$content
)
return(out)
}

Expand Down Expand Up @@ -425,7 +430,7 @@ get_usage_shiny <- function(

res <- page_cursor(src, res, limit = limit)

out <- parse_connectapi_typed(res, connectapi_ptypes$usage_shiny)
out <- parse_connectapi_typed(res, datetime_cols = datetime_columns$usage_shiny)

return(out)
}
Expand Down Expand Up @@ -521,7 +526,7 @@ get_usage_static <- function(

res <- page_cursor(src, res, limit = limit)

out <- parse_connectapi_typed(res, connectapi_ptypes$usage_static)
out <- parse_connectapi_typed(res, datetime_cols = datetime_columns$usage_static)

return(out)
}
Expand Down Expand Up @@ -658,7 +663,7 @@ as.data.frame.connect_list_hits <- function(
...,
unnest = TRUE
) {
usage_df <- parse_connectapi_typed(x, connectapi_ptypes$usage)
usage_df <- parse_connectapi_typed(x, datetime_cols = "timestamp")
if (unnest) {
if (!requireNamespace("tidyr", quietly = TRUE)) {
stop(
Expand Down Expand Up @@ -750,7 +755,7 @@ get_audit_logs <- function(

res <- page_cursor(src, res, limit = limit)

out <- parse_connectapi_typed(res, connectapi_ptypes$audit_logs)
out <- parse_connectapi_typed(res, datetime_cols = datetime_columns$audit_logs)

return(out)
}
Expand Down Expand Up @@ -792,7 +797,8 @@ get_procs <- function(src) {
c(list(pid = y), x)
}
)
tbl_data <- parse_connectapi_typed(proc_prep, connectapi_ptypes$procs)
tbl_data <- parse_connectapi_typed(proc_prep)
tbl_data <- coerce_fs_bytes(tbl_data, "ram")

return(tbl_data)
}
Expand Down Expand Up @@ -1217,7 +1223,7 @@ get_packages <- function(src, name = NULL, page_size = 100000, limit = Inf) {
page_size = page_size
)
)
out <- parse_connectapi_typed(res, connectapi_ptypes$packages)
out <- parse_connectapi_typed(res)

# Connect is standardizing on using `content_id` and `content_guid`.
# Handle that name change now in a forward-compatible way.
Expand Down Expand Up @@ -1253,5 +1259,5 @@ get_packages <- function(src, name = NULL, page_size = 100000, limit = Inf) {
#' @export
get_vanity_urls <- function(client) {
res <- client$vanities()
parse_connectapi_typed(res, connectapi_ptypes$vanities)
parse_connectapi_typed(res, datetime_cols = "created_time")
}
9 changes: 6 additions & 3 deletions R/groups.R
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ get_groups <- function(src, page_size = 500, prefix = NULL, limit = Inf) {
limit = limit
)

parse_connectapi_typed(res, connectapi_ptypes$groups)
parse_connectapi_typed(res, datetime_cols = datetime_columns$groups)
}

#' Get users within a specific group
Expand Down Expand Up @@ -106,7 +106,10 @@ get_group_members <- function(src, guid) {

res <- src$group_members(guid)

parse_connectapi(res$results)
parse_connectapi_typed(
res$results,
datetime_cols = c("created_time", "updated_time", "active_time")
)
}

#' Get content access permissions for a group or groups
Expand Down Expand Up @@ -172,7 +175,7 @@ get_one_groups_content <- function(src, guid) {
role = NA_character_
))
}
parsed <- parse_connectapi_typed(res, connectapi_ptypes$group_content)
parsed <- parse_connectapi_typed(res)

permissions_df <- purrr::list_rbind(
purrr::map(
Expand Down
5 changes: 4 additions & 1 deletion R/integrations.R
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,10 @@ as.data.frame.connect_integration_list <- function(
#' @return A tibble with one row per integration.
#' @export
as_tibble.connect_integration_list <- function(x, ...) {
parse_connectapi_typed(x, connectapi_ptypes$integrations)
parse_connectapi_typed(
x,
datetime_cols = c("created_time", "updated_time")
)
}

# Integration class ----
Expand Down
Loading
Loading