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
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Package: AzimuthAPI
Title: Pan-Azimuth Web API Interface
Version: 0.1.0
Version: 0.2.0
Authors@R:
person("Satija", "Lab", email = "satijalabnygc@gmail.com", role = c("aut", "cre"))
Description: An R package providing an interface to the Pan-Azimuth Web API for single-cell RNA sequencing analysis.
Expand Down
11 changes: 10 additions & 1 deletion R/api_interface.R
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
#'
#' @param url API endpoint URL
#' @param file_path Path to the file being processed
#' @return NULL
#' @return Logical indicating success (TRUE) or failure (FALSE)
#' @importFrom curl new_handle handle_setform curl_fetch_stream
#' @importFrom jsonlite fromJSON
#' @export
Expand All @@ -21,6 +21,8 @@ listen_to_progress <- function(url, file_path, ...) {

# Buffer to store partial messages
buffer <- ""
# Track whether processing succeeded
success <- NULL
Comment on lines +24 to +25
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

success is initialized to FALSE, so if the SSE stream never includes a success field, this function will return FALSE even on success. Initialize to NA and only update when a success field is received, or set success <- TRUE on normal stream completion and flip to FALSE only when an error/success flag indicates failure.

Suggested change
# Track whether processing succeeded
success <- NULL
# Track whether processing succeeded; default to TRUE and override if API sends a flag
success <- TRUE

Copilot uses AI. Check for mistakes.

# Process the SSE stream in real time
curl_fetch_stream(
Expand Down Expand Up @@ -51,6 +53,11 @@ listen_to_progress <- function(url, file_path, ...) {
tryCatch({
parsed <- fromJSON(json_message)

# Check for success flag
if (!is.null(parsed$success)) {
success <<- parsed$success
}

# Print messages based on content
if (!is.null(parsed$message)) {
cat(parsed$message, "\n")
Expand Down Expand Up @@ -78,6 +85,8 @@ listen_to_progress <- function(url, file_path, ...) {
}
}
)

return(success)
}

#' Download output file from the API
Expand Down
30 changes: 22 additions & 8 deletions R/cloud.R
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,23 @@
#'
#' @param object Seurat object to annotate
#' @param assay Name of the assay to use (default: 'RNA')
#' @param ip IP address of the cloud server (default: '34.222.135.233')
#' @param ip Hostname or IP address of the cloud server (default: 'azimuthapi.satijalab.org')
#' @param port Port number for the API (default: 5000)
#' @return Annotated Seurat object
#' @importFrom httr POST GET upload_file content status_code
#' @importFrom RCurl url.exists
#' @export
CloudAzimuth <- function(object = object, assay = 'RNA', ip = '34.222.135.233',
CloudAzimuth <- function(object = object, assay = 'RNA', ip = 'azimuthapi.satijalab.org',
port = 5000, ...) {
message("Running Pan-Human Azimuth on the cloud!")

layer_name <- 'data'
data <- LayerData(object, assay = assay, layer = layer_name)

# check if data has been normalized, just using the first 5 cells
# if data exists, check if normalized, just using the first 5 cells
# throw an error if large values, or all integer values, are detected
data_check <- data[,1:min(5,ncol(data))]
if ((max(data_check) > 15) || isTRUE(all.equal(data_check,floor(data_check)))) {
data_check <- if (!IsMatrixEmpty(data)) data[,1:min(5,ncol(data))] else NULL
if (is.null(data_check) || (max(data_check) > 15 || isTRUE(all.equal(data_check, floor(data_check))))) {
Comment on lines +20 to +21
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

IsMatrixEmpty() is not defined anywhere in this package, and it is not imported/qualified. This will error at runtime when CloudAzimuth() runs. Use a locally defined emptiness check (eg based on ncol, nrow, length, or Matrix::nnzero) or explicitly call a namespaced function that is guaranteed to exist (eg SeuratObject::...) and add the appropriate import.

Copilot uses AI. Check for mistakes.
stop("Please run NormalizeData on the data before running Azimuth")
}

Expand Down Expand Up @@ -46,13 +46,22 @@ CloudAzimuth <- function(object = object, assay = 'RNA', ip = '34.222.135.233',
object[[i]] <- srt[[i]]
}

# Copy metadata columns except QC ones
# Copy metadata columns except QC ones, matching by cell names
md_cols <- grep("nCount_RNA|nFeature_RNA",
colnames(srt@meta.data),
invert = TRUE,
value = TRUE)

# Only update metadata for cells that were processed (objects can have multiple assays w/ cell IDs formatted differently)
assay_cells <- Cells(srt)

for (i in md_cols) {
object@meta.data[, i] <- srt@meta.data[, i]
# Initialize column with NA only if it doesn't exist
if (!i %in% colnames(object@meta.data)) {
object@meta.data[, i] <- NA
Comment on lines +59 to +61
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

Initializing a new metadata column with a scalar NA creates a logical column; the subsequent assignment from srt@meta.data will be coerced into logicals (eg numerics become TRUE/FALSE), corrupting metadata types. Initialize the column using the same type as srt@meta.data[[i]] (eg rep(NA_<type>, nrow(object@meta.data))) or build the full vector via match and assign it once.

Suggested change
# Initialize column with NA only if it doesn't exist
if (!i %in% colnames(object@meta.data)) {
object@meta.data[, i] <- NA
# Initialize column with type-appropriate NA only if it doesn't exist
if (!i %in% colnames(object@meta.data)) {
template <- srt@meta.data[[i]]
n_cells <- nrow(object@meta.data)
if (is.factor(template)) {
# Preserve factor levels
na_factor <- factor(NA, levels = levels(template))
object@meta.data[, i] <- rep(na_factor, n_cells)
} else if (is.numeric(template) && !is.integer(template)) {
object@meta.data[, i] <- rep(NA_real_, n_cells)
} else if (is.integer(template)) {
object@meta.data[, i] <- rep(NA_integer_, n_cells)
} else if (is.character(template)) {
object@meta.data[, i] <- rep(NA_character_, n_cells)
} else {
# Logical or other types fall back to logical NA
object@meta.data[, i] <- rep(NA, n_cells)
}

Copilot uses AI. Check for mistakes.
}
# Update only the processed cells
object@meta.data[assay_cells, i] <- srt@meta.data[assay_cells, i]
Comment on lines +63 to +64
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

Indexing object@meta.data[assay_cells, i] will throw if any assay_cells are not present in object@meta.data rownames (which is plausible given the comment about differing cell ID formats). Update using an intersection/match of cell names so missing cells are ignored rather than erroring.

Suggested change
# Update only the processed cells
object@meta.data[assay_cells, i] <- srt@meta.data[assay_cells, i]
# Update only the processed cells that also exist in the original object metadata
common_cells <- intersect(assay_cells, rownames(object@meta.data))
if (length(common_cells) > 0) {
object@meta.data[common_cells, i] <- srt@meta.data[common_cells, i]
}

Copilot uses AI. Check for mistakes.
}

Idents(object) <- 'azimuth_label'
Expand All @@ -68,7 +77,12 @@ CloudAzimuth <- function(object = object, assay = 'RNA', ip = '34.222.135.233',
process_rds_file <- function(api_base_url, file_path, ...) {
progress_url <- paste0(api_base_url, "/process_rds")
cat("Uploading file and listening for updates...\n")
listen_to_progress(progress_url, file_path, ...)
success <- listen_to_progress(progress_url, file_path, ...)

if (isFALSE(success)) {
stop("Processing failed on the server. Please check the error messages above.")
}
Comment on lines +80 to +84
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

process_rds_file() now treats any listen_to_progress() result other than explicit TRUE as failure. Since listen_to_progress() initializes success to FALSE, if the server never emits a success field the client will always stop even when processing succeeded. Consider initializing success to NA and only stopping on an explicit FALSE (or alternatively treat stream completion as success unless an error status is received).

Copilot uses AI. Check for mistakes.

output_file_name <- gsub("\\.rds$", "_ANN.rds", basename(file_path))
download_url <- paste0(api_base_url, "/download_output?output_file=/tmp/",
output_file_name)
Expand Down