R/write-plumber.R

Defines functions vetiver_renviron_requirements.default vetiver_renviron_requirements vetiver_python_requirements.default vetiver_python_requirements file_copy_requirements write_extra_requirements choose_version glue_required_pkgs vetiver_write_plumber

Documented in vetiver_python_requirements vetiver_python_requirements.default vetiver_renviron_requirements vetiver_renviron_requirements.default vetiver_write_plumber

#' Write a deployable Plumber file for a vetiver model
#'
#' Use `vetiver_write_plumber()` to create a `plumber.R` file for a
#' [vetiver_model()] that has been versioned and stored via
#' [vetiver_pin_write()].
#'
#' @inheritParams pins::pin_read
#' @inheritParams vetiver_model
#' @param ... Other arguments passed to [vetiver_api()] such as the endpoint
#' `path` or prediction `type`.
#' @param file A path to write the Plumber file. Defaults to `plumber.R` in the
#' working directory. See [plumber::plumb()] for naming precedence rules.
#' @param rsconnect Create a Plumber file with features needed for [Posit
#' Connect](https://posit.co/products/enterprise/connect/)? Defaults to `TRUE`.
#' @param additional_pkgs Any additional R packages that need to be **attached**
#' via [library()] to run your API, as a character vector.
#'
#' @details
#' By default, this function will find and use the latest version of your
#' vetiver model; the model API (when deployed) will be linked to that specific
#' version. You can override this default behavior by choosing a specific
#' `version`.
#'
#' @return
#' The content of the `plumber.R` file, invisibly.
#'
#' @export
#'
#' @examplesIf rlang::is_installed("plumber")
#' library(pins)
#' tmp <- tempfile()
#' b <- board_temp(versioned = TRUE)
#' cars_lm <- lm(mpg ~ ., data = mtcars)
#' v <- vetiver_model(cars_lm, "cars_linear")
#' vetiver_pin_write(b, v)
#'
#' vetiver_write_plumber(b, "cars_linear", file = tmp)
#'
vetiver_write_plumber <- function(board, name, version = NULL,
                                  ...,
                                  file = "plumber.R",
                                  rsconnect = TRUE,
                                  additional_pkgs = character(0)) {

    rlang::check_installed("plumber")
    plumber_dots <- rlang::list2(...)

    if (board$versioned) {
        if (is_null(version)) {
            version <- pins::pin_versions(board, name)
            version <- choose_version(version)
        }
        pin_read <- glue('v <- vetiver_pin_read(b, "{name}", version = "{version}")')
        v <- vetiver_pin_read(board, name, version = version)
    } else {
        pin_read <- glue('v <- vetiver_pin_read(b, "{name}")')
        v <- vetiver_pin_read(board, name)
    }

    write_extra_requirements(v$model, file)

    infra_pkgs <- sort(c(infra_pkgs, additional_pkgs))
    load_infra_pkgs <- glue_collapse(glue("library({infra_pkgs})"), sep = "\n")
    load_required_pkgs <- glue_required_pkgs(v$metadata$required_pkgs, rsconnect)

    ## rlang::expr_deparse won't work for board_url, but
    ## base deparse won't work for S3 and other complex boards:
    if (inherits(board, "pins_board_url")) {
        board <- deparse(pins::board_deparse(board))
    } else {
        board <- rlang::expr_deparse(pins::board_deparse(board))
    }
    board <- glue('b <- {board}')

    if (rlang::is_empty(plumber_dots)) {
        pr_predict <- "pr %>% vetiver_api(v)"
    } else {
        pr_predict <- expr(pr %>% vetiver_api(v, !!!plumber_dots))
        pr_predict <- expr_deparse(pr_predict)
    }

    plumber <- glue("\n
         #* @plumber
         function(pr) {{
             {pr_predict}
         }}
         ")

    ret <- compact(list(
        "# Generated by the vetiver package; edit with care\n",
        load_infra_pkgs,
        load_required_pkgs,
        board,
        pin_read,
        plumber
    ))
    readr::write_lines(ret, file = file)
}

infra_pkgs <- c("pins", "plumber", "rapidoc", "vetiver")


glue_required_pkgs <- function(required_pkgs, rsconnect) {
    if (!is_null(required_pkgs) && rsconnect) {
        required_pkgs <- sort(required_pkgs)
        required_pkgs <- glue_collapse(glue("    library({required_pkgs})"),
                                       sep = "\n")
        load_required_pkgs <- glue("\n
            # Packages needed to generate model predictions
            if (FALSE) {{
            {required_pkgs}
            }}
            ")
        return(load_required_pkgs)
    }
    NULL
}

choose_version <- function(df) {
    if (has_name(df, "active")) {
        version <- vec_slice(df, df$active)
    } else if (has_name(df, "created")) {
        idx <- head(order(df$created, decreasing = TRUE), 1)
        version <- vec_slice(df, idx)
    } else {
        version <- vec_slice(df, 1)
        warn(
            c("Pinned vetiver model has no active version and no datetime on versions",
              "Do you need to check your pinned model?",
              glue('Using version {version[["version"]]}'))
        )
    }
    version[["version"]]
}

write_extra_requirements <- function(model, file) {
    model <- bundle::unbundle(model)
    path_to_py_requirements <- vetiver_python_requirements(model)
    file_copy_requirements(path_to_py_requirements, file, "requirements.txt")
    path_to_renviron_requirements <- vetiver_renviron_requirements(model)
    file_copy_requirements(path_to_renviron_requirements, file, ".Renviron")
    TRUE
}

file_copy_requirements <- function(requirements, plumber_file, new_name) {
    if (!is.null(requirements)) {
        fs::file_copy(
            requirements,
            fs::path(fs::path_dir(plumber_file), new_name),
            overwrite = TRUE
        )
    }
    requirements
}

#' Use extra files required for deployment
#'
#' Create files required for deploying an app generated via
#' [vetiver_write_plumber()], such as a Python `requirements.txt` or an
#' `.Renviron`
#'
#' @inheritParams vetiver_model
#' @export
#' @keywords internal
vetiver_python_requirements <- function(model) {
    UseMethod("vetiver_python_requirements")
}

#' @rdname vetiver_python_requirements
#' @export
vetiver_python_requirements.default <- function(model) {
    NULL
}

#' @rdname vetiver_python_requirements
#' @export
vetiver_renviron_requirements <- function(model) {
    UseMethod("vetiver_renviron_requirements")
}

#' @rdname vetiver_python_requirements
#' @export
vetiver_renviron_requirements.default <- function(model) {
    NULL
}
tidymodels/vetiver documentation built on March 25, 2024, 6 p.m.