Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create records from data frames #78

Merged
merged 30 commits into from
Nov 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
57d0f2d
Handle loading CSV and TSV files
lazappi Nov 12, 2024
a06ad13
Add from_df() method to Registry
lazappi Nov 12, 2024
9513d17
Modify Record printing to avoid API calls
lazappi Nov 13, 2024
4d2351f
Recursively create records in Registry$from_df()
lazappi Nov 13, 2024
d27dd95
Add temporary record classes with saving
lazappi Nov 14, 2024
433e278
Overwrite data after saving temporary record
lazappi Nov 14, 2024
ccba121
Create a default instance with connect(slug=NULL)
lazappi Nov 14, 2024
d0bfe5a
Adjust check_requires to output warnings
lazappi Nov 14, 2024
ca4eca9
Attempt to load Python lamin in connect()
lazappi Nov 14, 2024
3deebfd
Add reading for Parquet files
lazappi Nov 14, 2024
cc8cd0a
Document and pass checks
lazappi Nov 14, 2024
2bddcfe
Add get_temporary_record_class() to Registry
lazappi Nov 14, 2024
d8a841d
Remove new file loaders
lazappi Nov 14, 2024
b9b9315
Remove importing reading functions
lazappi Nov 14, 2024
6dc941d
Style package
lazappi Nov 14, 2024
134cc68
Remove broken docs link
lazappi Nov 14, 2024
39f8832
Store user settings in option
lazappi Nov 15, 2024
55e7222
Add delete() method to Record
lazappi Nov 15, 2024
c2aa3d3
Update architecture vignette
lazappi Nov 15, 2024
d079cb3
Update development vignette
lazappi Nov 15, 2024
e6a2792
Roxygenise
lazappi Nov 15, 2024
62540e6
Add test for Artifact$from_df()
lazappi Nov 15, 2024
8d7a5e2
Fix incorrect function call in test
lazappi Nov 15, 2024
7072330
Merge branch 'main' into add-dataframe-artifacts
rcannood Nov 15, 2024
ad09309
Update CHANGELOG
lazappi Nov 18, 2024
0c27537
Error in API$delete_record() if null access token
lazappi Nov 18, 2024
fea16e1
Move importing lamindb to create_instance
lazappi Nov 18, 2024
3dbf7bc
Make Python lamin getter and function not field
lazappi Nov 18, 2024
25ce8fd
Add features to README
lazappi Nov 18, 2024
81274cb
Pass checks and style
lazappi Nov 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()
lazappi marked this conversation as resolved.
Show resolved Hide resolved
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