From 4f6bccd078cbfcff763336489d99059cc71f679d Mon Sep 17 00:00:00 2001 From: Anagha Shenoy <90294056+anashen@users.noreply.github.com> Date: Thu, 16 Jul 2026 22:48:08 -0400 Subject: [PATCH 1/6] Switch to HTTP requests by default with legacy fallback --- R/cloud.R | 64 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 60 insertions(+), 4 deletions(-) diff --git a/R/cloud.R b/R/cloud.R index c411c81..92ca93f 100644 --- a/R/cloud.R +++ b/R/cloud.R @@ -2,21 +2,59 @@ #' #' @param object Seurat object to annotate #' @param assay Name of the assay to use (default: 'RNA') -#' @param ip Hostname or IP address of the cloud server (default: 'azimuthapi.satijalab.org') -#' @param port Port number for the API (default: 5000) +#' @param ip Server hostname (default: 'azimuthapi.satijalab.org') +#' @param port Server port (default: NULL) +#' @param scheme Server URL scheme (default: 'https'). #' @param ... Additional arguments for the API to pass to the model (see ANNotate function for details) #' @return Annotated Seurat object +#' @details +#' Use `CloudAzimuth(object)` for the public cloud service. The `ip`, `port`, and `scheme` +#' arguments should not be set manually by users; the default values follow standard use. #' @importFrom httr POST GET upload_file content status_code #' @importFrom RCurl url.exists #' @importFrom SeuratObject LayerData Idents IsMatrixEmpty CreateAssay5Object CreateSeuratObject Cells Idents<- #' @concept annotation #' @export CloudAzimuth <- function(object = object, assay = 'RNA', ip = 'azimuthapi.satijalab.org', - port = 5000, ...) { + port = NULL, scheme = NULL, ...) { cli::cli_h1("Running Pan-human Azimuth on the cloud") - api_base_url <- paste0('http://', ip, ":", port) + if (grepl("^https?://", ip)) { + stop("`ip` should not include 'http://' or 'https://'. Use `scheme` to choose HTTP or HTTPS.") + } + + # the default scheme and port depend on whether the user is connecting to the production server or a custom host + # defaults preserve the official HTTPS path and the temporary legacy HTTP path + if (identical(ip, "azimuthapi.satijalab.org")) { + if (is.null(scheme) && is.null(port)) { + scheme <- "https" + } else if (is.null(scheme) && isTRUE(port == 5000)) { + scheme <- "http" + } + } else { + if (is.null(scheme)) { + scheme <- "http" + } + if (is.null(port) && identical(scheme, "http")) { + port <- 5000 + } + } + + # restrict the official host to supported public entrypoints + if (identical(ip, "azimuthapi.satijalab.org")) { + https <- identical(scheme, "https") && is.null(port) + http_5000 <- identical(scheme, "http") && isTRUE(port == 5000) + allowed_official_access <- https || http_5000 + if (isTRUE(http_5000)) { + cli::cli_alert_warning("Using HTTP connection to the AzimuthAPI server. This is not recommended for security reasons and will be deprecated in the future. Please use HTTPS instead.") + } else if (isFALSE(allowed_official_access)) { + stop("For the AzimuthAPI server, use either (default) HTTPS with no port or (legacy) HTTP on port 5000.") + } + } + + api_base_url <- build_cloud_api_base_url(ip = ip, port = port, scheme = scheme) + update <- check_api_version(api_base_url) if (isTRUE(update)) { @@ -134,3 +172,21 @@ process_rds_file <- function(api_base_url, file_path, ...) { cli::cli_alert_success("Annotation complete. Output saved to: {save_path}") } +#' Build base URL for the cloud API +#' +#' @param ip Hostname or IP address of the cloud server +#' @param port Port number for the API, or `NULL` to omit it +#' @param scheme URL scheme to use +#' @return Character containing the base API URL to connect to +#' @noRd +build_cloud_api_base_url <- function(ip, port = NULL, scheme = "https") { + if (!is.character(scheme) || length(scheme) != 1 || is.na(scheme) || !scheme %in% c("http", "https")) { + stop("`scheme` must be either 'http' or 'https'.") + } + + if (is.null(port) || identical(port, "")) { + return(paste0(scheme, "://", ip)) + } + + paste0(scheme, "://", ip, ":", port) +} From 5f640e672f06bc03aefa5f7c72064fc07b527170 Mon Sep 17 00:00:00 2001 From: Anagha Shenoy <90294056+anashen@users.noreply.github.com> Date: Thu, 16 Jul 2026 22:48:44 -0400 Subject: [PATCH 2/6] Add tests for HTTPS / HTTP legacy --- tests/testthat/test_interface.R | 106 +++++++++++++++++++++++++++++++- 1 file changed, 105 insertions(+), 1 deletion(-) diff --git a/tests/testthat/test_interface.R b/tests/testthat/test_interface.R index 771942f..818c1d5 100644 --- a/tests/testthat/test_interface.R +++ b/tests/testthat/test_interface.R @@ -23,13 +23,117 @@ test_that("CloudAzimuth returns an annotated Seurat object", { .package = "AzimuthAPI" ) - annotated <- CloudAzimuth(query, ip = "azimuthapi.satijalab.org", port = 5000) + annotated <- CloudAzimuth(query, ip = "azimuthapi.satijalab.org") expect_s4_class(annotated, "Seurat") expect_true(all(c("final_level_labels", "azimuth_label") %in% colnames(annotated@meta.data))) expect_identical(unname(annotated@meta.data$azimuth_label), rep(c("T cell", "B cell"), length.out = ncol(query))) }) +test_that("CloudAzimuth builds HTTPS and legacy URLs correctly", { + expect_identical( + build_cloud_api_base_url("azimuthapi.satijalab.org"), + "https://azimuthapi.satijalab.org" + ) + + expect_identical( + build_cloud_api_base_url("localhost", port = 5000, scheme = "http"), + "http://localhost:5000" + ) + + expect_identical( + build_cloud_api_base_url("azimuthapi.satijalab.org", scheme = "http", port = 5000), + "http://azimuthapi.satijalab.org:5000" + ) + + expect_error( + build_cloud_api_base_url("azimuthapi.satijalab.org", scheme = "ftp"), + "`scheme` must be either 'http' or 'https'.", + fixed = TRUE + ) + +}) + +test_that("CloudAzimuth requires ip without scheme prefix", { + expect_error( + CloudAzimuth(object = NULL, ip = "https://example.org"), + "`ip` should not include 'http://' or 'https://'.", + fixed = TRUE + ) +}) + +test_that("CloudAzimuth defaults to official HTTPS endpoint", { + query <- make_test_object() + expected <- query + expected@meta.data$final_level_labels <- rep(c("T cell", "B cell"), length.out = ncol(expected)) + expected@meta.data$azimuth_label <- expected@meta.data$final_level_labels + api_base_urls <- character() + + testthat::local_mocked_bindings( + check_api_version = function(url) { + api_base_urls <<- c(api_base_urls, url) + invisible(url) + }, + process_rds_file = function(url, file_path, ...) { + api_base_urls <<- c(api_base_urls, url) + saveRDS(expected, file = sub("\\.rds$", "_ANN.rds", file_path)) + invisible(NULL) + }, + .package = "AzimuthAPI" + ) + + CloudAzimuth(query, ip = "azimuthapi.satijalab.org") + expect_true(all(api_base_urls[1:2] == "https://azimuthapi.satijalab.org")) +}) + +test_that("CloudAzimuth preserves alternate-host HTTP defaults", { + query <- make_test_object() + expected <- query + expected@meta.data$final_level_labels <- rep(c("T cell", "B cell"), length.out = ncol(expected)) + expected@meta.data$azimuth_label <- expected@meta.data$final_level_labels + api_base_urls <- character() + + testthat::local_mocked_bindings( + check_api_version = function(url) { + api_base_urls <<- c(api_base_urls, url) + invisible(url) + }, + process_rds_file = function(url, file_path, ...) { + api_base_urls <<- c(api_base_urls, url) + saveRDS(expected, file = sub("\\.rds$", "_ANN.rds", file_path)) + invisible(NULL) + }, + .package = "AzimuthAPI" + ) + + CloudAzimuth(query, ip = "localhost") + expect_true(all(api_base_urls[1:2] == "http://localhost:5000")) +}) + +test_that("CloudAzimuth preserves legacy official HTTP when port 5000 is explicit", { + query <- make_test_object() + expected <- query + expected@meta.data$final_level_labels <- rep(c("T cell", "B cell"), length.out = ncol(expected)) + expected@meta.data$azimuth_label <- expected@meta.data$final_level_labels + api_base_urls <- character() + + testthat::local_mocked_bindings( + check_api_version = function(url) { + api_base_urls <<- c(api_base_urls, url) + invisible(url) + }, + process_rds_file = function(url, file_path, ...) { + api_base_urls <<- c(api_base_urls, url) + saveRDS(expected, file = sub("\\.rds$", "_ANN.rds", file_path)) + invisible(NULL) + }, + .package = "AzimuthAPI" + ) + + CloudAzimuth(query, ip = "azimuthapi.satijalab.org", port = 5000) + expect_true(all(api_base_urls[1:2] == "http://azimuthapi.satijalab.org:5000")) +}) + test_that("ANNotate returns an annotated Seurat object", { skip_if_not_installed("Seurat") skip_if_not_installed("reticulate") From 6557094643f9f35220ebcd22e0e620a526015e94 Mon Sep 17 00:00:00 2001 From: Anagha Shenoy <90294056+anashen@users.noreply.github.com> Date: Thu, 16 Jul 2026 22:49:21 -0400 Subject: [PATCH 3/6] Use model version v1 as default --- R/annotation.R | 4 ++-- R/argument_parser.R | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/R/annotation.R b/R/annotation.R index 7d9a04b..4da713e 100644 --- a/R/annotation.R +++ b/R/annotation.R @@ -28,7 +28,7 @@ #' @param process_obj Whether to process the object #' @param cutoff_abs Absolute cutoff for label filtering #' @param cutoff_frac Fractional cutoff for label filtering -#' @param model_version Version of the model to use +#' @param model_version Version of the model to use (default: 'v1') #' @param assay Assay to use for annotation #' #' @importFrom SeuratObject Idents<- @@ -61,7 +61,7 @@ ANNotate <- function( spread = 1.0, verbose = TRUE, init = "spectral", - model_version = "v0", + model_version = "v1", process_obj = TRUE, cutoff_abs = 5, cutoff_frac = 0.001, diff --git a/R/argument_parser.R b/R/argument_parser.R index ce2926f..a53ed91 100644 --- a/R/argument_parser.R +++ b/R/argument_parser.R @@ -161,8 +161,8 @@ parse_annotate_args <- function() { parser$add_argument( "--model_version", - default = "v0", - help = "Version of the model to use (default: 'v0')", + default = "v1", + help = "Version of the model to use (default: 'v1')", type = "character" ) From 95e244f25655c928d44ee4b7cc937ece9bbb7837 Mon Sep 17 00:00:00 2001 From: Anagha Shenoy <90294056+anashen@users.noreply.github.com> Date: Thu, 16 Jul 2026 22:49:34 -0400 Subject: [PATCH 4/6] Update documentation --- man/ANNotate.Rd | 4 ++-- man/CloudAzimuth.Rd | 13 ++++++++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/man/ANNotate.Rd b/man/ANNotate.Rd index 5c3e758..fb02272 100644 --- a/man/ANNotate.Rd +++ b/man/ANNotate.Rd @@ -26,7 +26,7 @@ ANNotate( spread = 1, verbose = TRUE, init = "spectral", - model_version = "v0", + model_version = "v1", process_obj = TRUE, cutoff_abs = 5, cutoff_frac = 0.001, @@ -76,7 +76,7 @@ ANNotate( \item{init}{Initialization method for UMAP} -\item{model_version}{Version of the model to use} +\item{model_version}{Version of the model to use (default: 'v1')} \item{process_obj}{Whether to process the object} diff --git a/man/CloudAzimuth.Rd b/man/CloudAzimuth.Rd index 7bcf3c9..2bd6e50 100644 --- a/man/CloudAzimuth.Rd +++ b/man/CloudAzimuth.Rd @@ -8,7 +8,8 @@ CloudAzimuth( object = object, assay = "RNA", ip = "azimuthapi.satijalab.org", - port = 5000, + port = NULL, + scheme = NULL, ... ) } @@ -17,9 +18,11 @@ CloudAzimuth( \item{assay}{Name of the assay to use (default: 'RNA')} -\item{ip}{Hostname or IP address of the cloud server (default: 'azimuthapi.satijalab.org')} +\item{ip}{Server hostname (default: 'azimuthapi.satijalab.org')} -\item{port}{Port number for the API (default: 5000)} +\item{port}{Server port (default: NULL)} + +\item{scheme}{Server URL scheme (default: 'https').} \item{...}{Additional arguments for the API to pass to the model (see ANNotate function for details)} } @@ -29,4 +32,8 @@ Annotated Seurat object \description{ Run Pan-human Azimuth annotation on the cloud } +\details{ +Use \code{CloudAzimuth(object)} for the public cloud service. The \code{ip}, \code{port}, and \code{scheme} +arguments should not be set manually by users; the default values follow standard use. +} \concept{annotation} From a9937b2951ff150e063829a66c589063b485d651 Mon Sep 17 00:00:00 2001 From: Anagha Shenoy <90294056+anashen@users.noreply.github.com> Date: Thu, 16 Jul 2026 22:49:48 -0400 Subject: [PATCH 5/6] Bump version --- DESCRIPTION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index b2872f7..96b94f6 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: AzimuthAPI Title: Pan-Azimuth Web API Interface -Version: 0.9.0 +Version: 1.0.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. From c755197e58df08049ce7c3cd26b3099d10ea0858 Mon Sep 17 00:00:00 2001 From: Anagha Shenoy <90294056+anashen@users.noreply.github.com> Date: Thu, 16 Jul 2026 22:49:59 -0400 Subject: [PATCH 6/6] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 32b4c19..aafa94e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # AzimuthAPI -## Version 0.9.0 +## Version 1.0.0 An R package providing an interface to the Pan-human Azimuth neural network, enabling users to run cell type annotation on single-cell and spatial transcriptomics data. @@ -15,7 +15,7 @@ Two options for annotation are available: ## Installation ```r -# Install devtools if not already installed +# Install remotes if not already installed install.packages("remotes") # Install AzimuthAPI from GitHub