diff --git a/NEWS.md b/NEWS.md index 141b109e..00b41438 100644 --- a/NEWS.md +++ b/NEWS.md @@ -9,6 +9,9 @@ * `deployApp()` and `writeManifest()` receive the default value for the `image` argument from the `RSCONNECT_IMAGE` environment variable. (#1063) +* `deployTF()` can deploy a TensorFlow model to Posit Connect. Requires Posit + Connect 2024.05.0 or higher. + # rsconnect 1.2.2 * Use internally computed SHA1 sums and PKI signing when SHA1 is disabled diff --git a/R/appDependencies.R b/R/appDependencies.R index 645a4da6..8e93e294 100644 --- a/R/appDependencies.R +++ b/R/appDependencies.R @@ -136,7 +136,7 @@ appDependencies <- function(appDir = getwd(), } needsR <- function(appMetadata) { - if (appMetadata$appMode == "static") { + if (appMetadata$appMode %in% c("static", "tensorflow-saved-model")) { return(FALSE) } diff --git a/R/appMetadata.R b/R/appMetadata.R index 76fa5dae..a7fd9254 100644 --- a/R/appMetadata.R +++ b/R/appMetadata.R @@ -38,9 +38,9 @@ appMetadata <- function(appDir, appMode <- "shiny" } else { # Inference only uses top-level files - rootFiles <- appFiles[dirname(appFiles) == "."] appMode <- inferAppMode( - file.path(appDir, rootFiles), + appDir, + appFiles, usesQuarto = quarto, isShinyappsServer = isShinyappsServer ) @@ -98,10 +98,18 @@ checkAppLayout <- function(appDir, appPrimaryDoc = NULL) { # can be run/served. } -# infer the mode of the application from files in the root dir -inferAppMode <- function(absoluteRootFiles, - usesQuarto = NA, - isShinyappsServer = FALSE) { +# Infer the mode of the application from included files. Most content types +# only consider files at the directory root. TensorFlow saved models may be +# anywhere in the hierarchy. +inferAppMode <- function( + appDir, + appFiles, + usesQuarto = NA, + isShinyappsServer = FALSE) { + + rootFiles <- appFiles[dirname(appFiles) == "."] + absoluteRootFiles <- file.path(appDir, rootFiles) + absoluteAppFiles <- file.path(appDir, appFiles) matchingNames <- function(paths, pattern) { idx <- grepl(pattern, basename(paths), ignore.case = TRUE, perl = TRUE) @@ -190,6 +198,12 @@ inferAppMode <- function(absoluteRootFiles, return("quarto-static") } + # TensorFlow model files are lower in the hierarchy, not at the root. + modelFiles <- matchingNames(absoluteAppFiles, "^(saved_model.pb|saved_model.pbtxt)$") + if (length(modelFiles) > 0) { + return("tensorflow-saved-model") + } + # no renderable content "static" } diff --git a/R/deployTFModel.R b/R/deployTFModel.R index 12850dc2..faf3ad76 100644 --- a/R/deployTFModel.R +++ b/R/deployTFModel.R @@ -1,20 +1,11 @@ #' Deploy a TensorFlow saved model #' -#' This function is defunct. Posit Connect no longer supports hosting of -#' TensorFlow Model APIs. A TensorFlow model can be deployed as a -#' [Plumber API](https://tensorflow.rstudio.com/guides/deploy/plumber.html), -#' [Shiny application](https://tensorflow.rstudio.com/guides/deploy/shiny), or -#' other supported content type. +#' Deploys a directory containing a TensorFlow saved model. #' -#' @param modelDir Path to the saved model directory. Unused. -#' @param ... Unused. +#' @param ... Additional arguments to [deployApp()]. #' #' @family Deployment functions #' @export -deployTFModel <- function(modelDir, ...) { - lifecycle::deprecate_stop( - when = "1.0.0", - what = "deployTFModel()", - details = "Posit Connect no longer supports hosting of TensorFlow Model APIs." - ) +deployTFModel <- function(...) { + deployApp(appMode = "tensorflow-saved-model", ...) } diff --git a/man/deployTFModel.Rd b/man/deployTFModel.Rd index 18d83e58..1a6fcf56 100644 --- a/man/deployTFModel.Rd +++ b/man/deployTFModel.Rd @@ -4,19 +4,13 @@ \alias{deployTFModel} \title{Deploy a TensorFlow saved model} \usage{ -deployTFModel(modelDir, ...) +deployTFModel(...) } \arguments{ -\item{modelDir}{Path to the saved model directory. Unused.} - -\item{...}{Unused.} +\item{...}{Additional arguments to \code{\link[=deployApp]{deployApp()}}.} } \description{ -This function is defunct. Posit Connect no longer supports hosting of -TensorFlow Model APIs. A TensorFlow model can be deployed as a -\href{https://tensorflow.rstudio.com/guides/deploy/plumber.html}{Plumber API}, -\href{https://tensorflow.rstudio.com/guides/deploy/shiny}{Shiny application}, or -other supported content type. +Deploys a directory containing a TensorFlow saved model. } \seealso{ Other Deployment functions: diff --git a/tests/testthat/test-appMetadata.R b/tests/testthat/test-appMetadata.R index f66fd32f..6326174a 100644 --- a/tests/testthat/test-appMetadata.R +++ b/tests/testthat/test-appMetadata.R @@ -106,55 +106,69 @@ test_that("checkLayout succeeds with some common app structures", { # inferAppMode ------------------------------------------------------------ -test_that("can infer mode for APIs", { - expect_equal(inferAppMode("plumber.R"), "api") - expect_equal(inferAppMode("entrypoint.R"), "api") +test_that("can infer mode for API with plumber.R", { + dir <- local_temp_app(list("plumber.R" = "")) + paths <- list.files(dir) + expect_equal(inferAppMode(dir, paths), "api") }) -test_that("can infer mode for shiny apps", { - expect_equal(inferAppMode("app.R"), "shiny") - expect_equal(inferAppMode("server.R"), "shiny") +test_that("can infer mode for API with entrypoint.R", { + dir <- local_temp_app(list("entrypoint.R" = "")) + paths <- list.files(dir) + expect_equal(inferAppMode(dir, paths), "api") +}) + +test_that("can infer mode for shiny apps with app.R", { + dir <- local_temp_app(list("app.R" = "")) + paths <- list.files(dir) + expect_equal(inferAppMode(dir, paths), "shiny") +}) + +test_that("can infer mode for shiny apps with server.R", { + dir <- local_temp_app(list("server.R" = "")) + paths <- list.files(dir) + expect_equal(inferAppMode(dir, paths), "shiny") }) test_that("can infer mode for static rmd", { dir <- local_temp_app(list("foo.Rmd" = "")) - paths <- list.files(dir, full.names = TRUE) - expect_equal(inferAppMode(paths), "rmd-static") + paths <- list.files(dir) + expect_equal(inferAppMode(dir, paths), "rmd-static") }) test_that("can infer mode for rmd as static quarto with guidance", { dir <- local_temp_app(list("foo.Rmd" = "")) - paths <- list.files(dir, full.names = TRUE) - expect_equal(inferAppMode(paths, usesQuarto = TRUE), "quarto-static") + paths <- list.files(dir) + expect_equal(inferAppMode(dir, paths, usesQuarto = TRUE), "quarto-static") }) test_that("can infer mode for rmd as shiny quarto with guidance", { # Static R Markdown treated as rmd-shiny for shinyapps targets dir <- local_temp_app(list("foo.Rmd" = "")) - paths <- list.files(dir, full.names = TRUE) - expect_equal(inferAppMode(paths, isShinyappsServer = TRUE), "rmd-shiny") + paths <- list.files(dir) + expect_equal(inferAppMode(dir, paths, isShinyappsServer = TRUE), "rmd-shiny") }) test_that("can infer mode for static quarto", { dir <- local_temp_app(list("foo.qmd" = "")) - paths <- list.files(dir, full.names = TRUE) - expect_equal(inferAppMode(paths), "quarto-static") + paths <- list.files(dir) + expect_equal(inferAppMode(dir, paths), "quarto-static") dir <- local_temp_app(list("_quarto.yml" = "", "foo.qmd" = "")) - paths <- list.files(dir, full.names = TRUE) - expect_equal(inferAppMode(paths), "quarto-static") + paths <- list.files(dir) + expect_equal(inferAppMode(dir, paths), "quarto-static") dir <- local_temp_app(list("_quarto.yml" = "", "foo.rmd" = "")) - paths <- list.files(dir, full.names = TRUE) - expect_equal(inferAppMode(paths), "quarto-static") + paths <- list.files(dir) + expect_equal(inferAppMode(dir, paths), "quarto-static") dir <- local_temp_app(list("_quarto.yml" = "", "foo.r" = "")) - paths <- list.files(dir, full.names = TRUE) - expect_equal(inferAppMode(paths), "quarto-static") + paths <- list.files(dir) + expect_equal(inferAppMode(dir, paths), "quarto-static") dir <- local_temp_app(list("foo.r" = "")) - paths <- list.files(dir, full.names = TRUE) - expect_equal(inferAppMode(paths), "quarto-static") + paths <- list.files(dir) + expect_equal(inferAppMode(dir, paths), "quarto-static") }) test_that("can infer mode for shiny rmd docs", { @@ -163,26 +177,26 @@ test_that("can infer mode for shiny rmd docs", { } dir <- local_temp_app(list("index.Rmd" = yaml_runtime("shiny"))) - paths <- list.files(dir, full.names = TRUE) - expect_equal(inferAppMode(paths), "rmd-shiny") + paths <- list.files(dir) + expect_equal(inferAppMode(dir, paths), "rmd-shiny") dir <- local_temp_app(list("index.Rmd" = yaml_runtime("shinyrmd"))) - paths <- list.files(dir, full.names = TRUE) - expect_equal(inferAppMode(paths), "rmd-shiny") + paths <- list.files(dir) + expect_equal(inferAppMode(dir, paths), "rmd-shiny") dir <- local_temp_app(list("index.Rmd" = yaml_runtime("shiny_prerendered"))) - paths <- list.files(dir, full.names = TRUE) - expect_equal(inferAppMode(paths), "rmd-shiny") + paths <- list.files(dir) + expect_equal(inferAppMode(dir, paths), "rmd-shiny") # can pair server.R with shiny runtime dir <- local_temp_app(list("index.Rmd" = yaml_runtime("shiny"), "server.R" = "")) - paths <- list.files(dir, full.names = TRUE) - expect_equal(inferAppMode(paths), "rmd-shiny") + paths <- list.files(dir) + expect_equal(inferAppMode(dir, paths), "rmd-shiny") # Beats static rmarkdowns dir <- local_temp_app(list("index.Rmd" = yaml_runtime("shiny"), "foo.Rmd" = "")) - paths <- list.files(dir, full.names = TRUE) - expect_equal(inferAppMode(paths), "rmd-shiny") + paths <- list.files(dir) + expect_equal(inferAppMode(dir, paths), "rmd-shiny") }) test_that("can infer mode for shiny qmd docs", { @@ -191,21 +205,21 @@ test_that("can infer mode for shiny qmd docs", { } dir <- local_temp_app(list("index.Qmd" = yaml_runtime("shiny"))) - paths <- list.files(dir, full.names = TRUE) - expect_equal(inferAppMode(paths), "quarto-shiny") + paths <- list.files(dir) + expect_equal(inferAppMode(dir, paths), "quarto-shiny") # Can force Rmd to use quarto dir <- local_temp_app(list("index.Rmd" = yaml_runtime("shiny"))) - paths <- list.files(dir, full.names = TRUE) - expect_equal(inferAppMode(paths, usesQuarto = TRUE), "quarto-shiny") + paths <- list.files(dir) + expect_equal(inferAppMode(dir, paths, usesQuarto = TRUE), "quarto-shiny") # Prefers quarto if both present dir <- local_temp_app(list( "index.Qmd" = yaml_runtime("shiny"), "index.Rmd" = yaml_runtime("shiny") )) - paths <- list.files(dir, full.names = TRUE) - expect_equal(inferAppMode(paths), "quarto-shiny") + paths <- list.files(dir) + expect_equal(inferAppMode(dir, paths), "quarto-shiny") }) test_that("Shiny R Markdown files are detected correctly", { @@ -223,7 +237,9 @@ test_that("shiny metadata process correctly", { }) test_that("otherwise, fallsback to static deploy", { - expect_equal(inferAppMode(c("a.html", "b.html")), "static") + dir <- local_temp_app(list("a.html" = "", "b.html" = "")) + paths <- list.files(dir) + expect_equal(inferAppMode(dir, paths), "static") }) # inferAppPrimaryDoc ------------------------------------------------------ diff --git a/tests/testthat/test-writeManifest.R b/tests/testthat/test-writeManifest.R index 29ce0f80..b9ac440b 100644 --- a/tests/testthat/test-writeManifest.R +++ b/tests/testthat/test-writeManifest.R @@ -239,6 +239,18 @@ test_that("environment.image is not set when image is not provided", { expect_null(manifest$environment) }) +test_that("TensorFlow models are identified", { + skip_on_cran() + + app_dir <- local_temp_app(list( + "1/saved_model.pb" = "fake-saved-model" + )) + manifest <- makeManifest(app_dir) + expect_equal(manifest$metadata$appmode, "tensorflow-saved-model") + expect_null(manifest$packages) + expect_named(manifest$files, c("1/saved_model.pb")) +}) + test_that("environment.image is set when image is provided", { skip_on_cran()