Skip to content

Commit

Permalink
Create records from data frames (#78)
Browse files Browse the repository at this point in the history
* Add `from_df()` method to `Registry`
* Add temporary record classes with saving
* Create a default instance with `connect(slug=NULL)`
* Add `delete()` method to Record
* Update architecture vignette
* Update development vignette

Co-authored-by: Robrecht Cannoodt <[email protected]>
  • Loading branch information
lazappi and rcannood authored Nov 18, 2024
1 parent 4e8613e commit 804cb06
Show file tree
Hide file tree
Showing 19 changed files with 545 additions and 41 deletions.
20 changes: 17 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,27 @@

## NEW FUNCTIONALITY

* Add support for more loaders (PR #81).
- Add support for more loaders (PR #81).
Currently supported: `.csv`, `.h5ad`, `.html`, `.jpg`, `.json`, `.parquet`, `.png`, `.rds`, `.svg`, `.tsv`, `.yaml`.
Planned: `.fcs`, `.h5mu`, `.zarr`.
- Add a `from_df()` method to the `Registry` class to create new artifacts from data frames (PR #78)
- Create `TemporaryRecord` classes for new artifacts before they have been saved to the database (PR #78)
- Add a `delete()` method to the `Record` class (PR #78)

## MAJOR CHANGES

- Running `connect(slug = NULL)` now connects to the default instance that is allowed to create records.
The default instance must be changed using the Lamin CLI. (PR #78)
- User setting are stored in a global option the first time `connect()` is run (PR #78)

## TESTING

- Add a test for creating artifacts from data frames (PR #78).

## DOCUMENTATION

* Updated installation instructions after **{laminr}** was released on CRAN (PR #74).
- Updated installation instructions after **{laminr}** was released on CRAN (PR #74).
- Updated the architecture vignette to include new methods and the new `TemporaryRecord` class (PR #78)
- Updated the development vignette with new functionality (PR #78)

# laminr v0.1.0

Expand Down
2 changes: 1 addition & 1 deletion NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ importFrom(purrr,map_lgl)
importFrom(purrr,modify_depth)
importFrom(purrr,pmap)
importFrom(purrr,reduce)
importFrom(purrr,set_names)
importFrom(purrr,transpose)
importFrom(purrr,walk)
importFrom(rlang,set_names)
importFrom(tools,file_ext)
58 changes: 52 additions & 6 deletions R/Instance.R
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
create_instance <- function(instance_settings) {
create_instance <- function(instance_settings, is_default = FALSE) {
super <- NULL # satisfy linter

api <- InstanceAPI$new(instance_settings = instance_settings)
Expand Down Expand Up @@ -46,19 +46,46 @@ create_instance <- function(instance_settings) {
cloneable = FALSE,
inherit = Instance,
public = list(
initialize = function(settings, api, schema) {
initialize = function(settings, api, schema, is_default, py_lamin) {
super$initialize(
settings = settings,
api = api,
schema = schema
schema = schema,
is_default = is_default,
py_lamin = py_lamin
)
}
),
active = active
)

py_lamin <- NULL
if (isTRUE(is_default)) {
check_requires("Connecting to Python", "reticulate", type = "warning")

py_lamin <- tryCatch(
reticulate::import("lamindb"),
error = function(err) {
cli::cli_warn(c(
paste(
"Failed to connect to the Python {.pkg lamindb} package,",
"you will not be able to create records"
),
"i" = "See {.run reticulate::py_config()} for more information"
))
NULL
}
)
}

# create the instance
RichInstance$new(settings = instance_settings, api = api, schema = schema)
RichInstance$new(
settings = instance_settings,
api = api,
schema = schema,
is_default = is_default,
py_lamin = py_lamin
)
}

#' @title Instance
Expand Down Expand Up @@ -103,9 +130,13 @@ Instance <- R6::R6Class( # nolint object_name_linter
#' @param settings The settings for the instance
#' @param api The API for the instance
#' @param schema The schema for the instance
initialize = function(settings, api, schema) {
#' @param is_default Logical, whether this is the default instance
#' @param py_lamin A Python `lamindb` module object
initialize = function(settings, api, schema, is_default, py_lamin) {
private$.settings <- settings
private$.api <- api
private$.is_default <- is_default
private$.py_lamin <- py_lamin

# create module classes from the schema
private$.module_classes <- map(
Expand Down Expand Up @@ -158,6 +189,12 @@ Instance <- R6::R6Class( # nolint object_name_linter
get_api = function() {
private$.api
},
#' @description Get the Python lamindb module
#'
#' @return Python lamindb module.
get_py_lamin = function() {
private$.py_lamin
},
#' @description
#' Print an `Instance`
#'
Expand Down Expand Up @@ -246,9 +283,18 @@ Instance <- R6::R6Class( # nolint object_name_linter
)
}
),
active = list(
#' @field is_default (`logical(1)`)\cr
#' Whether this is the default instance.
is_default = function() {
private$.is_default
}
),
private = list(
.settings = NULL,
.api = NULL,
.module_classes = NULL
.module_classes = NULL,
.is_default = NULL,
.py_lamin = NULL
)
)
43 changes: 43 additions & 0 deletions R/InstanceAPI.R
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,49 @@ InstanceAPI <- R6::R6Class( # nolint object_name_linter
private$process_response(response, "get record")
},
#' @description
#' Delete a record from the instance.
delete_record = function(module_name,
registry_name,
id_or_uid,
verbose = FALSE) {
user_settings <- .get_user_settings()
if (is.null(user_settings$access_token)) {
cli::cli_abort(c(
"There is no access token for the current user",
"i" = "Run {.code lamin login} and reconnect to the database in a new R session"
))
}

url <- paste0(
private$.instance_settings$api_url,
"/instances/",
private$.instance_settings$id,
"/modules/",
module_name,
"/",
registry_name,
"/",
id_or_uid,
"?schema_id=",
private$.instance_settings$schema_id
)

if (verbose) {
cli_inform("URL: {url}")
}

response <- httr::DELETE(
url,
httr::add_headers(
accept = "application/json",
`Content-Type` = "application/json",
Authorization = paste("Bearer", user_settings$access_token)
)
)

private$process_response(response, "delete record")
},
#' @description
#' Print an `API`
#'
#' @param style Logical, whether the output is styled using ANSI codes
Expand Down
116 changes: 110 additions & 6 deletions R/Record.R
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,78 @@ create_record_class <- function(instance, registry, api) {
RichRecordClass
}

#' Create a temporary record class
#'
#' @param record_class A generator for a standard record class
#'
#' @details
#' The classes generated by this function inherit from a standard record class
#' and represent the situation where a new record has being created using Python
#' `lamindb` but has not yet been saved to the database. It should behave the
#' same as the standard class but indicate to the user that it has not yet been
#' saved. Saving is performed using the Python record object stored in the R
#' temporary record. After saving, the data in the object is replaced with that
#' in the database, indications that it has not been saved are removed and
#' further saving is prevented. In examples etc. the `$save()` should usually be
#' called immediately to avoid users seeing the temporary record in it's unsaved
#' state.
#'
#' @return The temporary record class R6 generator
#' @noRd
create_temporary_record_class <- function(record_class) {
super <- NULL # Satisfy checks
self <- NULL # Satisfy checks
private <- NULL # Satisfy checks

R6::R6Class(
paste0("Temporary", record_class$classname),
cloneable = FALSE,
inherit = record_class,
public = list(
initialize = function(py_record, data) {
private$.record_class <- record_class
private$.py_record <- py_record

super$initialize(data)
},
save = function() {
if (isTRUE(private$.saved)) {
cli::cli_abort("This record has already been saved to the database")
}

private$.py_record$save()

# Replace temporary data with data saved to the database
private$.data <- private$.api$get_record(
module_name = private$.registry$module$name,
registry_name = private$.registry$name,
id_or_uid = self$uid
)

private$.saved <- TRUE
},
print = function(style = TRUE) {
if (isFALSE(private$.saved)) {
cli::cat_line(paste(
cli::bg_red(cli::col_black("TEMPORARY")),
cli::format_message(paste(
"This record has not been saved to the database.",
"Save it using {.code <object>$save()}."
))
))
}

super$print()
}
),
private = list(
.record_class = NULL,
.py_record = NULL,
.saved = FALSE
)
)
}

#' @title Record
#'
#' @description
Expand Down Expand Up @@ -98,6 +170,22 @@ Record <- R6::R6Class( # nolint object_name_linter
}
},
#' @description
#' Delete a `Record`
#'
#' @param verbose Whether to print details of the API call
#'
#' @return `TRUE` invisibly if the deletion is successful
delete = function(verbose = FALSE) {
response <- private$.api$delete_record(
module_name = private$.registry$module$name,
registry_name = private$.registry$name,
id_or_uid = self$uid,
verbose = verbose
)

invisible(TRUE)
},
#' @description
#' Print a `Record`
#'
#' @param style Logical, whether the output is styled using ANSI codes
Expand All @@ -120,12 +208,28 @@ Record <- R6::R6Class( # nolint object_name_linter
"key"
)

record_fields <- private$.api$get_record(
module_name = private$.registry$module$name,
registry_name = private$.registry$name,
id_or_uid = private$.data[["uid"]],
include_foreign_keys = TRUE
)
expected_fields <- private$.registry$get_fields() |>
discard(~ is.null(.x$column_name)) |>
map_chr("column_name")

record_fields <- map(names(expected_fields), function(.field) {
value <- tryCatch(
self[[.field]],
error = function(err) {
if (!grepl("status code 404", conditionMessage(err))) {
cli::cli_abort(conditionMessage(err))
}
NULL
}
)

if (inherits(value, "Record")) {
value <- value$id
}

value
}) |>
set_names(expected_fields)

# Get the important fields that are in the record
important_fields <- intersect(important_fields, names(record_fields))
Expand Down
Loading

0 comments on commit 804cb06

Please sign in to comment.