Skip to content
1 change: 1 addition & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export(get_from_pipenv)
export(get_github_creds)
export(get_latest_pip_release)
export(get_latest_ppp_versions)
export(get_pip_aliases)
export(get_pip_folders)
export(get_pip_releases)
export(get_pipenv)
Expand Down
281 changes: 134 additions & 147 deletions R/log.R
Original file line number Diff line number Diff line change
Expand Up @@ -182,38 +182,122 @@ log_init <- function(name = getOption("pipfun.log.default"),
}


#' Save a log to disk

#' Reset or delete a log from memory
#'
#' Saves a log stored in `.piplogenv` to disk using {stamp}, with metadata
#' and versioning support.
#' Clears a log from the internal environment. Use this to start over or free
#' memory.
#'
#' @param name Name of the log in memory (default:
#' @param name Name of the log to remove (default:
#' `getOption("pipfun.log.default")`).
#' @param dir Directory where the log should be saved.
#' @param id File identifier (without extension). Defaults to `name`.
#'
#' @return Invisibly returns TRUE if the log was removed.
#' @export
log_reset <- function(name = getOption("pipfun.log.default", "default")) {
if (!rlang::env_has(.piplogenv, name)) {
cli::cli_alert_info("Log {.field {name}} is not present.")
return(invisible(FALSE))
}

rlang::env_unbind(.piplogenv, name)
cli::cli_alert_success("Log {.field {name}} has been reset.")
invisible(TRUE)
}


#' Filter log entries
#'
#' @param name Name of the log (default: `pipfun.log.default`)
#' @param event Type of event to filter ("info", "warning", "error", etc.)
#' @param fun Optional: function name(s) to filter
#' @param after Optional: filter entries after this datetime
#' @param before Optional: filter entries before this datetime
#'
#' @return A filtered `piplog` object.
#' @export
log_filter <- function(name = getOption("pipfun.log.default"),
event = NULL,
fun = NULL,
after = NULL,
before = NULL) {

log <- name |>
log_get() |>
copy()

setDT(log)

# not elegant but works
e <- event
f <- fun

if (!is.null(event)) {
log <- log[event %in% e]
}
if (!is.null(fun)) {
log <- log[fun %in% f]
}
if (!is.null(after)) {
log <- log[time >= as.POSIXct(after)]
}
if (!is.null(before)) {
log <- log[time <= as.POSIXct(before)]
}
setattr(log, "class", c("piplog", class(log)))
return(log)
}


#' Get a particular log entries
#'
#' @param name Name of the log (default: `pipfun.log.default`)
#'
#' @return A raw `piplog` object.
#' @export
log_get <- function(name = getOption("pipfun.log.default")) {
if (!rlang::env_has(.piplogenv, name)) {
cli::cli_abort("Log {.field {name}} does not exist.")
}

log <- rlang::env_get(.piplogenv, name)

if (!inherits(log, "piplog")) {
# Restore class silently if it's just been dropped by DT ops
if (is.data.table(log)) {
setattr(log, "class", unique(c("piplog", class(log))))
} else {
cli::cli_abort(c(x = "Object {.field {name}} is not a valid piplog.",
i = "{.field {name}}'s class is {class(log)}"))
}
}
invisible(log)
}

#' Save a log to disk
#'
#' Saves a log stored in `.piplogenv` using {stamp}; `id` is the artifact path
#' (extension will be added if missing). Pass `alias` to select a stamp alias.
#'
#' @param name Name of the log in memory (default: getOption("pipfun.log.default")).
#' @param id File identifier or path (extension optional). 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 alias Optional stamp alias to select which catalog/versions to use.
#' @param ... Forwarded to `stamp::st_save()`.
#'
#' @return Invisibly, the result returned by `stamp::st_save()`.
#' @export
log_save <- function(
name = getOption("pipfun.log.default", "default"),
dir,
id = name,
format = "qs2",
metadata = list(),
code = NULL,
alias = NULL,
...
) {

# ---- Validate directory ----
if (missing(dir) || !fs::dir_exists(dir)) {
cli::cli_abort("Provided directory path does not exist: {.path {dir}}")
}

# ---- Validate log ----
# Validate log exists
if (!rlang::env_has(.piplogenv, name)) {
cli::cli_abort("Log {.field {name}} does not exist in memory.")
}
Expand All @@ -225,19 +309,19 @@ log_save <- function(
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.")
}
cli::cli_abort("Object is not a valid {.cls piplog}.")
}

# ---- Build stamp path ----
file <- fs::path(dir, id, ext = format)
sp <- stamp::st_path(file, format = format)
# Ensure extension is present like pipload::pip_write
if (is.null(fs::path_ext(id)) || identical(fs::path_ext(id), "")) {
id <- fs::path_ext_set(path = id, ext = format)
}
file <- id

# ---- Save with stamp ----
out <- stamp::st_save(
x = log,
file = sp,
file = file,
metadata = c(
list(
class = "piplog",
Expand All @@ -248,83 +332,76 @@ log_save <- function(
),
code = code,
format = format,
alias = alias,
...
)

invisible(out)
}


#' Load a log from disk
#'
#' Loads a previously saved piplog from disk using {stamp}, optionally under a
#' different name.
#' Loads a previously saved piplog using {stamp}. `id` is the artifact path
#' (extension will be added if missing). Pass `alias` to select a stamp alias.
#'
#' @param dir Directory where the log is stored.
#' @param id File identifier (without extension). Defaults to `name`.
#' @param id File identifier or path (extension optional).
#' @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 overwrite Logical: whether to overwrite an existing log in memory.
#' @param verbose Logical: whether to announce loading progress.
#' @param alias Optional stamp alias to select which catalog/versions to use.
#'
#' @return Invisibly returns the name of the loaded log.
#' @export
log_load <- function(
dir,
id,
name = id,
version = NULL,
format = "qs2",
name = id,
version = NULL,
format = "qs2",
overwrite = FALSE,
verbose = TRUE
verbose = TRUE,
alias = NULL
) {

# ---- Validate directory ----
if (missing(dir) || !fs::dir_exists(dir)) {
cli::cli_abort("Artifact folder {.path {dir}} does not exist.")
# Ensure extension is present like pipload::pip_read
if (is.null(fs::path_ext(id)) || identical(fs::path_ext(id), "")) {
id <- fs::path_ext_set(path = id, ext = format)
}
file <- id

# ---- Build path ----
file <- fs::path(dir, id, ext = format)

# ---- List available versions ----
# List available versions
if (identical(version, "available")) {
vr <- stamp::st_versions(file)
vr <- stamp::st_versions(file, alias = alias)
if (nrow(vr) == 0) {
cli::cli_abort("No versions found in {.path {file}}.")
}
vr[, vintage := (.I - 1) * -1]
return(vr[])
}

# ---- Load log ----
ver <- if (is.null(version)) "latest" else version
ver <- if (is.null(version)) "latest" else version

if (verbose) {
cli::cli_alert_info(
"Loading {.path {file}} (version = {.strong {ver}})"
)
if (is.null(alias)) {
cli::cli_alert_info("Loading {.path {file}} (version = {.strong {ver}})")
} else {
cli::cli_alert_info("Loading {.path {file}} (version = {.strong {ver}}, alias = {.val {alias}})")
}
}

# Load log, forwarding alias
log <- stamp::st_load(file, version = version, alias = alias)

log <- stamp::st_load(file, version = version)

# ---- Validate object ----
# Restore class if dropped by serialization
# Restore class if dropped
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.")
}


# ---- Handle overwrite ----
# Handle overwrite
if (rlang::env_has(.piplogenv, name) && !isTRUE(overwrite)) {
cli::cli_abort(
"A log named {.field {name}} already exists in memory.
Expand All @@ -335,94 +412,4 @@ log_load <- function(
rlang::env_poke(.piplogenv, name, log)

invisible(name)
}

#' Reset or delete a log from memory
#'
#' Clears a log from the internal environment. Use this to start over or free
#' memory.
#'
#' @param name Name of the log to remove (default:
#' `getOption("pipfun.log.default")`).
#'
#' @return Invisibly returns TRUE if the log was removed.
#' @export
log_reset <- function(name = getOption("pipfun.log.default", "default")) {
if (!rlang::env_has(.piplogenv, name)) {
cli::cli_alert_info("Log {.field {name}} is not present.")
return(invisible(FALSE))
}

rlang::env_unbind(.piplogenv, name)
cli::cli_alert_success("Log {.field {name}} has been reset.")
invisible(TRUE)
}


#' Filter log entries
#'
#' @param name Name of the log (default: `pipfun.log.default`)
#' @param event Type of event to filter ("info", "warning", "error", etc.)
#' @param fun Optional: function name(s) to filter
#' @param after Optional: filter entries after this datetime
#' @param before Optional: filter entries before this datetime
#'
#' @return A filtered `piplog` object.
#' @export
log_filter <- function(name = getOption("pipfun.log.default"),
event = NULL,
fun = NULL,
after = NULL,
before = NULL) {

log <- name |>
log_get() |>
copy()

setDT(log)

# not elegant but works
e <- event
f <- fun

if (!is.null(event)) {
log <- log[event %in% e]
}
if (!is.null(fun)) {
log <- log[fun %in% f]
}
if (!is.null(after)) {
log <- log[time >= as.POSIXct(after)]
}
if (!is.null(before)) {
log <- log[time <= as.POSIXct(before)]
}
setattr(log, "class", c("piplog", class(log)))
return(log)
}


#' Get a particular log entries
#'
#' @param name Name of the log (default: `pipfun.log.default`)
#'
#' @return A raw `piplog` object.
#' @export
log_get <- function(name = getOption("pipfun.log.default")) {
if (!rlang::env_has(.piplogenv, name)) {
cli::cli_abort("Log {.field {name}} does not exist.")
}

log <- rlang::env_get(.piplogenv, name)

if (!inherits(log, "piplog")) {
# Restore class silently if it's just been dropped by DT ops
if (is.data.table(log)) {
setattr(log, "class", unique(c("piplog", class(log))))
} else {
cli::cli_abort(c(x = "Object {.field {name}} is not a valid piplog.",
i = "{.field {name}}'s class is {class(log)}"))
}
}
invisible(log)
}
}
Loading
Loading