diff --git a/.stamp/catalog.lock.lock b/.stamp/catalog.lock.lock new file mode 100644 index 0000000..e69de29 diff --git a/.stamp/catalog.qs2 b/.stamp/catalog.qs2 new file mode 100644 index 0000000..09ee6cf Binary files /dev/null and b/.stamp/catalog.qs2 differ diff --git a/NAMESPACE b/NAMESPACE index fd857a4..c6af051 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -43,6 +43,7 @@ export(log_reset) export(log_save) export(log_show) export(log_summary) +export(log_versions) export(log_warn) export(merge_branch_into) export(new_pip_release) diff --git a/R/log.R b/R/log.R index b3a30d2..081214f 100644 --- a/R/log.R +++ b/R/log.R @@ -184,132 +184,157 @@ log_init <- function(name = getOption("pipfun.log.default"), #' Save a log to disk #' -#' Saves a log stored in `.piplogenv` to a `.qs` file for persistence. +#' Saves a log stored in `.piplogenv` to disk using {stamp}, with metadata +#' and versioning support. #' #' @param name Name of the log in memory (default: #' `getOption("pipfun.log.default")`). -#' @param path `r lifecycle::badge("deprecated")` `path` is no longer supported. -#' Use `board` argument now. If value passed to `path` is not a pins board, it -#' will through an error. -#' @param board pins board -#' @param pin_name name of pin that will be used to load the log. By default it is the same as `name`. -#' @inheritDotParams pins::pin_write title description metadata tags +#' @param dir Directory where the log should be saved. +#' @param id File identifier (without extension). Defaults to `name`. +#' @param format File format (default: "qs2"). +#' @param metadata Optional named list of metadata to attach. +#' @param code Optional code object whose hash will be stored. +#' @param ... Forwarded to `stamp::st_save()`. #' -#' -#' @return Invisible `TRUE` if successful. +#' @return Invisibly, the result returned by `stamp::st_save()`. #' @export -log_save <- function(name = getOption("pipfun.log.default", "default"), - board = NULL, - pin_name = name, - path = deprecated(), - ...) { - - if (lifecycle::is_present(path)) { - lifecycle::deprecate_warn( - when = "0.3.7", - what = "log_save(path)", - with = "log_save(board)", - details = "all the logs will be saved as pins, so you need to use a pins board rather than a directory path" - ) - board <- path +log_save <- function( + name = getOption("pipfun.log.default", "default"), + dir, + id = name, + format = "qs2", + metadata = list(), + code = NULL, + ... +) { + + # ---- Validate directory ---- + if (missing(dir) || !fs::dir_exists(dir)) { + cli::cli_abort("Provided directory path does not exist: {.path {dir}}") } - if (!inherits(board, "pins_board")) { - cli::cli_abort("{.arg board} must be a pins_board class object") + # ---- Validate log ---- + if (!rlang::env_has(.piplogenv, name)) { + cli::cli_abort("Log {.field {name}} does not exist in memory.") } + log <- rlang::env_get(.piplogenv, name) - if (!exists(name, envir = .piplogenv)) { - cli::cli_abort("Log {.field {name}} does not exist in memory.") + # Restore class if dropped by serialization + if (is.data.table(log)) { + setattr(log, "class", unique(c("piplog", class(log)))) } - log <- get(name, envir = .piplogenv) - + # Final validation if (!inherits(log, "piplog")) { - cli::cli_abort("Object {.field {name}} is not a valid piplog.") + cli::cli_abort("File does not contain a valid {.cls piplog} object.") } - pins::pin_write(board = board, - x = log, - name = pin_name, - type = "qs", - versioned = TRUE, - ...) + # ---- Build stamp path ---- + file <- fs::path(dir, id, ext = format) + sp <- stamp::st_path(file, format = format) + + # ---- Save with stamp ---- + out <- stamp::st_save( + x = log, + file = sp, + metadata = c( + list( + class = "piplog", + log_name = name, + saved_at = Sys.time() + ), + metadata + ), + code = code, + format = format, + ... + ) - invisible(TRUE) + invisible(out) } -#' Load a log from a .qs file -#' -#' Loads a previously saved log into `.piplogenv`, optionally under a different -#' name. +#' Load a log from disk #' -#' @param board pins board -#' @param pin_name name of pin that will be used to load the log. By default it -#' is the same as `name`. -#' @inheritParams pins::pin_read -#' @param name `r lifecycle::badge("deprecated")` `name` has been superseded by -#' `pin_name`. It is nor inferred from filename any more. -#' @param path `r lifecycle::badge("deprecated")` `path` is no longer supported. -#' Use `board` argument now. If value passed to `path` is not a pins board, it -#' will through an error. -#' @inheritDotParams pins::pin_read +#' Loads a previously saved piplog from disk using {stamp}, optionally under a +#' different name. #' -#' @param overwrite logical: whether to override the log in `.piplogenv` with -#' the same `pin_name`. Default is FALSE. +#' @param dir Directory where the log is stored. +#' @param id File identifier (without extension). Defaults to `name`. +#' @param name Name to assign to the log in memory (default: `id`). +#' @param version Optional version identifier passed to `stamp::st_load()`. +#' Use `"available"` to list available versions. +#' @param format File format (default: "qs2"). +#' @param overwrite Logical: whether to overwrite an existing log in +#' `.piplogenv`. Default is FALSE. +#' @param verbose Logical: whether to announce loading progress. #' #' @return Invisibly returns the name of the loaded log. #' @export -log_load <- function(board, - pin_name = name, - version = NULL, - hash = NULL, - path = deprecated(), - name = deprecated(), - overwrite = FALSE, - ...) { - - if (lifecycle::is_present(path)) { - lifecycle::deprecate_warn( - when = "0.3.7", - what = "log_save(path)", - with = "log_save(board)", - details = "all the logs will be saved as pins, so you need to use a pins board rather than a directory path" - ) - board <- path +log_load <- function( + dir, + id, + name = id, + version = NULL, + format = "qs2", + overwrite = FALSE, + verbose = TRUE +) { + + # ---- Validate directory ---- + if (missing(dir) || !fs::dir_exists(dir)) { + cli::cli_abort("Artifact folder {.path {dir}} does not exist.") } - if (lifecycle::is_present(name)) { - lifecycle::deprecate_warn( - when = "0.3.7", - what = "log_save(name)", - with = "log_save(pin_name)" - ) - pin_name <- name + + # ---- Build path ---- + file <- fs::path(dir, id, ext = format) + + # ---- List available versions ---- + if (identical(version, "available")) { + vr <- stamp::st_versions(file) + if (nrow(vr) == 0) { + cli::cli_abort("No versions found in {.path {file}}.") + } + vr[, vintage := (.I - 1) * -1] + return(vr[]) } - if (!inherits(board, "pins_board")) { - cli::cli_abort("{.arg board} must be a pins_board class object") + # ---- Load log ---- + ver <- if (is.null(version)) "latest" else version + + if (verbose) { + cli::cli_alert_info( + "Loading {.path {file}} (version = {.strong {ver}})" + ) } - log <- pins::pin_read(board = board, - name = pin_name, - version = version, - hash = hash, - ...) + log <- stamp::st_load(file, version = version) + + # ---- Validate object ---- + # Restore class if dropped by serialization + if (is.data.table(log)) { + setattr(log, "class", unique(c("piplog", class(log)))) + } + + # Final validation if (!inherits(log, "piplog")) { cli::cli_abort("File does not contain a valid {.cls piplog} object.") } - if (rlang::env_has(.piplogenv, pin_name) && !overwrite) { - cli::cli_abort("A log named {.field {pin_name}} already exists in memory. Use {.code overwrite = TRUE} to replace it.") + # ---- Handle overwrite ---- + if (rlang::env_has(.piplogenv, name) && !isTRUE(overwrite)) { + cli::cli_abort( + "A log named {.field {name}} already exists in memory. + Use {.code overwrite = TRUE} to replace it." + ) } - rlang::env_poke(.piplogenv, pin_name, log) + rlang::env_poke(.piplogenv, name, log) - invisible(pin_name) + invisible(name) } #' Reset or delete a log from memory diff --git a/R/log_helpers.R b/R/log_helpers.R index 9c4d88e..1dbe0cc 100644 --- a/R/log_helpers.R +++ b/R/log_helpers.R @@ -312,3 +312,40 @@ inspect_args <- function(.env = parent.frame()) { structure(resolved, class = "inspect_args") } +#' List available versions of a saved log +#' +#' Lists all saved versions of a log stored on disk using {stamp}. +#' +#' @param dir Directory where the log is stored. +#' @param id File identifier (without extension). +#' @param format File format (default: "qs2"). +#' +#' @return A data.table of available versions. +#' @export +log_versions <- function( + dir, + id, + format = "qs2" +) { + + # ---- Validate directory ---- + if (missing(dir) || !fs::dir_exists(dir)) { + cli::cli_abort("Artifact folder {.path {dir}} does not exist.") + } + + # ---- Build file path ---- + file <- fs::path(dir, id, ext = format) + + # ---- Get versions ---- + vr <- stamp::st_versions(file) + + if (nrow(vr) == 0) { + cli::cli_abort("No versions found for {.path {file}}.") + } + + # ---- Add convenience ordering (latest = 0) ---- + vr[, vintage := (.I - 1) * -1] + + vr[] +} + diff --git a/man/log_load.Rd b/man/log_load.Rd index 74719ee..42dc742 100644 --- a/man/log_load.Rd +++ b/man/log_load.Rd @@ -2,52 +2,39 @@ % Please edit documentation in R/log.R \name{log_load} \alias{log_load} -\title{Load a log from a .qs file} +\title{Load a log from disk} \usage{ log_load( - board, - pin_name = name, + dir, + id, + name = id, version = NULL, - hash = NULL, - path = deprecated(), - name = deprecated(), + format = "qs2", overwrite = FALSE, - ... + verbose = TRUE ) } \arguments{ -\item{board}{pins board} +\item{dir}{Directory where the log is stored.} -\item{pin_name}{name of pin that will be used to load the log. By default it -is the same as \code{name}.} +\item{id}{File identifier (without extension). Defaults to \code{name}.} -\item{version}{Retrieve a specific version of a pin. Use \code{\link[pins:pin_versions]{pin_versions()}} to -find out which versions are available and when they were created.} +\item{name}{Name to assign to the log in memory (default: \code{id}).} -\item{hash}{Specify a hash to verify that you get exactly the dataset that -you expect. You can find the hash of an existing pin by looking for -\code{pin_hash} in \code{\link[pins:pin_meta]{pin_meta()}}.} +\item{version}{Optional version identifier passed to \code{stamp::st_load()}. +Use \code{"available"} to list available versions.} -\item{path}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} \code{path} is no longer supported. -Use \code{board} argument now. If value passed to \code{path} is not a pins board, it -will through an error.} +\item{format}{File format (default: "qs2").} -\item{name}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} \code{name} has been superseded by -\code{pin_name}. It is nor inferred from filename any more.} +\item{overwrite}{Logical: whether to overwrite an existing log in +\code{.piplogenv}. Default is FALSE.} -\item{overwrite}{logical: whether to override the log in \code{.piplogenv} with -the same \code{pin_name}. Default is FALSE.} - -\item{...}{ - Arguments passed on to \code{\link[pins:pin_read]{pins::pin_read}} - \describe{ - \item{\code{}}{} - }} +\item{verbose}{Logical: whether to announce loading progress.} } \value{ Invisibly returns the name of the loaded log. } \description{ -Loads a previously saved log into \code{.piplogenv}, optionally under a different -name. +Loads a previously saved piplog from disk using {stamp}, optionally under a +different name. } diff --git a/man/log_save.Rd b/man/log_save.Rd index 12a2cbb..a33d00f 100644 --- a/man/log_save.Rd +++ b/man/log_save.Rd @@ -6,9 +6,11 @@ \usage{ log_save( name = getOption("pipfun.log.default", "default"), - board = NULL, - pin_name = name, - path = deprecated(), + dir, + id = name, + format = "qs2", + metadata = list(), + code = NULL, ... ) } @@ -16,31 +18,22 @@ log_save( \item{name}{Name of the log in memory (default: \code{getOption("pipfun.log.default")}).} -\item{board}{pins board} +\item{dir}{Directory where the log should be saved.} -\item{pin_name}{name of pin that will be used to load the log. By default it is the same as \code{name}.} +\item{id}{File identifier (without extension). Defaults to \code{name}.} -\item{path}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} \code{path} is no longer supported. -Use \code{board} argument now. If value passed to \code{path} is not a pins board, it -will through an error.} +\item{format}{File format (default: "qs2").} -\item{...}{ - Arguments passed on to \code{\link[pins:pin_read]{pins::pin_write}} - \describe{ - \item{\code{title}}{A title for the pin; most important for shared boards so that -others can understand what the pin contains. If omitted, a brief -description of the contents will be automatically generated.} - \item{\code{description}}{A detailed description of the pin contents.} - \item{\code{metadata}}{A list containing additional metadata to store with the pin. -When retrieving the pin, this will be stored in the \code{user} key, to -avoid potential clashes with the metadata that pins itself uses.} - \item{\code{tags}}{A character vector of tags for the pin; most important for -discoverability on shared boards.} - }} +\item{metadata}{Optional named list of metadata to attach.} + +\item{code}{Optional code object whose hash will be stored.} + +\item{...}{Forwarded to \code{stamp::st_save()}.} } \value{ -Invisible \code{TRUE} if successful. +Invisibly, the result returned by \code{stamp::st_save()}. } \description{ -Saves a log stored in \code{.piplogenv} to a \code{.qs} file for persistence. +Saves a log stored in \code{.piplogenv} to disk using {stamp}, with metadata +and versioning support. } diff --git a/man/log_versions.Rd b/man/log_versions.Rd new file mode 100644 index 0000000..1369dcd --- /dev/null +++ b/man/log_versions.Rd @@ -0,0 +1,21 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/log_helpers.R +\name{log_versions} +\alias{log_versions} +\title{List available versions of a saved log} +\usage{ +log_versions(dir, id, format = "qs2") +} +\arguments{ +\item{dir}{Directory where the log is stored.} + +\item{id}{File identifier (without extension).} + +\item{format}{File format (default: "qs2").} +} +\value{ +A data.table of available versions. +} +\description{ +Lists all saved versions of a log stored on disk using {stamp}. +}