From a555bf20155f6b74e4eb2b277168dc47b9b5f8b6 Mon Sep 17 00:00:00 2001 From: Luke Zappia Date: Fri, 20 Dec 2024 11:47:15 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=B8=20Improve=20the=20user=20experienc?= =?UTF-8?q?e=20for=20setting=20up=20Python=20&=20reticulate=20(#129)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ Add install_lamindb() function Installs lamindb into a new or existing environment * ✨ Add .onLoad() function Sets the default Python environment if reticulate is not already connected * ➕ Move reticulate to Imports * ✨ Add lamin_connect() function * 📝 Add setup vignette Adjust instructions in other documentation * ✨ Add lamin_login() function * 👷 Use R setup functions for CI * 📝 Roxygenise * 🚨 Fix lint * 💚 Fix missing shell in CI * 🐛 Fix lamin_login function name * 🐛 Remove argument check in lamin_login() * 📝 Fix \dontru{} in example * ➕ Install s3fs on CI * 📝 Adjust setup vignette title * Update R-CMD-check.yaml * 💚 Install laminr in check action * 💚 Use pak to install tiledbsoma on GHA * 💚 Use reticulate to install Python 3.12 on macOS * 💚 Correctly install Python 3.12 on macOS not Linux * 🐛 Fix logic in lamin_login() * 🐛 Adjust settings directory path for Windows * 📝 Update CHANGELOG * 📝 Remove login with user from README * 📝 Update development vignette * 📝 Fix version in CHANGELOG --------- Co-authored-by: Robrecht Cannoodt --- .github/workflows/R-CMD-check.yaml | 40 +++++---- .github/workflows/pkgdown.yaml | 21 ++--- CHANGELOG.md | 16 +++- DESCRIPTION | 4 +- NAMESPACE | 3 + R/connect.R | 77 +++++++++++++++++ R/install.R | 44 ++++++++++ R/settings_store.R | 7 +- R/zzz.R | 3 + README.md | 30 +++++-- man/install_lamindb.Rd | 47 ++++++++++ man/lamin_connect.Rd | 20 +++++ man/lamin_login.Rd | 22 +++++ vignettes/development.qmd | 8 ++ vignettes/laminr.Rmd | 14 +-- vignettes/setup.Rmd | 133 +++++++++++++++++++++++++++++ 16 files changed, 443 insertions(+), 46 deletions(-) create mode 100644 R/install.R create mode 100644 R/zzz.R create mode 100644 man/install_lamindb.Rd create mode 100644 man/lamin_connect.Rd create mode 100644 man/lamin_login.Rd create mode 100644 vignettes/setup.Rmd diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index bcbb739..4758e2b 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -18,15 +18,9 @@ jobs: fail-fast: false matrix: config: - # note: we use python 3.12 on mac os x so we can install scipy 1.13 from a wheel - - { os: macos-latest, r: "release", python: "3.12" } + - { os: macos-latest, r: "release", python: "3.x" } - { os: windows-latest, r: "release", python: "3.x" } - - { - os: ubuntu-latest, - r: "devel", - http-user-agent: "release", - python: "3.x", - } + - { os: ubuntu-latest, r: "devel", http-user-agent: "release", python: "3.x" } - { os: ubuntu-latest, r: "release", python: "3.x" } - { os: ubuntu-latest, r: "oldrel-1", python: "3.9" } @@ -52,7 +46,7 @@ jobs: # manually installing openblas as a workaround for issue # https://github.com/laminlabs/laminr/issues/57 - - name: Install OpenBLAS + - name: Install OpenBLAS on macOS if: runner.os == 'macOS' run: | brew install openblas @@ -62,36 +56,46 @@ jobs: - uses: r-lib/actions/setup-r-dependencies@v2 with: - extra-packages: any::rcmdcheck + extra-packages: any::rcmdcheck, local::. needs: check - name: Install {tiledbsoma} if: runner.os == 'Linux' run: | options(repos = c("https://chanzuckerberg.r-universe.dev", getOption("repos"))) - install.packages("tiledbsoma") + pak::pkg_install("tiledbsoma") shell: Rscript {0} - - name: Install lamindb + - name: Install Python 3.12 on macOS + # We use python 3.12 on mac os x so we can install scipy 1.13 from a wheel + if: runner.os == 'macOS' run: | - pip install 'lamindb[aws]>=0.77.2' + reticulate::install_python(version = "3.12") + shell: Rscript {0} - # Make sure IPython is installed -- - # Workaround for laminlabs/laminhub-public#29 - pip install ipython + - name: Setup Python environment + run: | + laminr::install_lamindb(extra_packages = c("s3fs")) + shell: Rscript {0} - name: Log in to Lamin run: | - lamin login + reticulate::use_virtualenv("r-lamindb") + laminr::lamin_login() + shell: Rscript {0} - name: Set lamindata as default instance run: | - lamin connect laminlabs/lamindata + reticulate::use_virtualenv("r-lamindb") + laminr::lamin_connect("laminlabs/lamindata") + shell: Rscript {0} - name: Check whether we can import lamindb and connect to the default instance run: | + reticulate::use_virtualenv("r-lamindb") reticulate::py_config() reticulate::import("lamindb") + laminr::connect() shell: Rscript {0} - name: Check diff --git a/.github/workflows/pkgdown.yaml b/.github/workflows/pkgdown.yaml index 43393ba..1625eb6 100644 --- a/.github/workflows/pkgdown.yaml +++ b/.github/workflows/pkgdown.yaml @@ -49,31 +49,32 @@ jobs: run: | if (!requireNamespace("tiledbsoma", quietly = TRUE)) { options(repos = c("https://chanzuckerberg.r-universe.dev", getOption("repos"))) - install.packages("tiledbsoma") + pak::pkg_install("tiledbsoma") } else { message("Package 'tiledbsoma' already installed") } shell: Rscript {0} - - name: Install lamindb + - name: Setup Python environment run: | - # install bionty and wetlab to avoid warnings about missing dependencies - pip install 'lamindb[aws,bionty,wetlab]>=0.77.2' - - # Make sure IPython is installed -- - # Workaround for laminlabs/laminhub-public#29 - pip install ipython + laminr::install_lamindb(extra_packages = c("s3fs")) + shell: Rscript {0} - name: Log in to Lamin run: | - lamin login + reticulate::use_virtualenv("r-lamindb") + laminr::lamin_login() + shell: Rscript {0} - name: Set lamindata as default instance run: | - lamin connect laminlabs/lamindata + reticulate::use_virtualenv("r-lamindb") + laminr::lamin_connect("laminlabs/lamindata") + shell: Rscript {0} - name: Check whether we can import lamindb and connect to the default instance run: | + reticulate::use_virtualenv("r-lamindb") reticulate::py_config() reticulate::import("lamindb") laminr::connect() diff --git a/CHANGELOG.md b/CHANGELOG.md index 08adfc9..e8f9571 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,21 @@ +# laminr v0.3.1 + +## NEW FUNCTIONALITY + +- Add a `install_lamindb()` function to help with setting up the default Python environment +- Add `lamin_login()` and `lamin_connect()` functions to allow access to CLI functionality from R + +## DOCUMENTATION + +- Add a set up vignette and update other documentation with instructions for how to set up a Python environment # laminr v0.3.0 This release contains mostly UX improvements: -* Support for interacting with private LaminDB instances -* Support for interacting with TileDB-SOMA / CELLxGENE Census -* Improved UX for tracking and finishing runs +- Support for interacting with private LaminDB instances +- Support for interacting with TileDB-SOMA / CELLxGENE Census +- Improved UX for tracking and finishing runs ## NEW FUNCTIONALITY diff --git a/DESCRIPTION b/DESCRIPTION index 239ea4e..064b417 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -23,6 +23,7 @@ Imports: purrr, R.utils, R6, + reticulate, rlang, tibble Suggests: @@ -31,7 +32,6 @@ Suggests: nanoparquet, quarto, readr, - reticulate, rstudioapi, rsvg, s3 (>= 1.1.0), @@ -39,7 +39,7 @@ Suggests: testthat (>= 3.0.0), withr, yaml -VignetteBuilder: +VignetteBuilder: quarto Config/testthat/edition: 3 Encoding: UTF-8 diff --git a/NAMESPACE b/NAMESPACE index 6ea9514..0fcdca4 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,6 +1,9 @@ # Generated by roxygen2: do not edit by hand export(connect) +export(install_lamindb) +export(lamin_connect) +export(lamin_login) importFrom(R6,R6Class) importFrom(cli,cli_abort) importFrom(cli,cli_inform) diff --git a/R/connect.R b/R/connect.R index 378a2cd..4ebd054 100644 --- a/R/connect.R +++ b/R/connect.R @@ -166,3 +166,80 @@ connect <- function(slug = NULL) { InstanceSettings$new(content) } + +#' Set the default LaminDB instance +#' +#' Set the default LaminDB instance by calling `lamin connect` on the command +#' line +#' +#' @param slug Slug giving the instance to connect to ("/") +#' +#' @export +#' +#' @examples +#' \dontrun{ +#' lamin_connect("laminlabs/cellxgene") +#' } +lamin_connect <- function(slug) { + current_default <- getOption("LAMINR_DEFAULT_INSTANCE") + if (!is.null(current_default)) { + cli::cli_abort(c( + "There is already a default instance connected ({.field {current_default}})", + "x" = "{.code lamin connect} will not be run" + )) + } + + # Set the default environment if not set + reticulate::use_virtualenv("r-lamindb", required = FALSE) + if (!reticulate::py_available()) { + # Force reticulate to connect to Python + py_config <- reticulate::py_config() # nolint object_usage_linter + } + + system2("lamin", paste("connect", slug)) +} + +#' Login to LaminDB +#' +#' Login as a LaminDB user +#' +#' @param user Handle for the user to login as +#' @param api_key API key for a user +#' +#' @details +#' Setting `user` will run `lamin login `. Setting `api_key` will set the +#' `LAMIN_API_KEY` environment variable tempoarily with `withr::with_envvar()` +#' and run `lamin login`. If neither `user` or `api_key` are set `lamin login` +#' will be run if `LAMIN_API_KEY` is set. +#' +#' @export +lamin_login <- function(user = NULL, api_key = NULL) { + current_default <- getOption("LAMINR_DEFAULT_INSTANCE") + if (!is.null(current_default)) { + cli::cli_abort(c( + "There is already a default instance connected ({.field {current_default}})", + "x" = "{.code lamin login} will not be run" + )) + } + + # Set the default environment if not set + reticulate::use_virtualenv("r-lamindb", required = FALSE) + if (!reticulate::py_available()) { + # Force reticulate to connect to Python + py_config <- reticulate::py_config() # nolint object_usage_linter + } + + if (!is.null(user)) { + system2("lamin", paste("login", user)) + } else if (!is.null(api_key)) { + withr::with_envvar(c("LAMIN_API_KEY" = api_key), { + system2("lamin", "login") + }) + } else { + if (Sys.getenv("LAMIN_API_KEY") == "") { + cli::cli_abort("{.arg LAMIN_API_KEY} is not set") + } + + system2("lamin", "login") + } +} diff --git a/R/install.R b/R/install.R new file mode 100644 index 0000000..feaef7a --- /dev/null +++ b/R/install.R @@ -0,0 +1,44 @@ +#' Install LaminDB +#' +#' Create a Python environment containing **lamindb** or install **lamindb** +#' into an existing environment. +#' +#' @param ... Additional arguments passed to `reticulate::py_install()` +#' @param envname String giving the name of the environment to install packages +#' into +#' @param extra_packages A vector giving the names of additional Python packages +#' to install +#' @param new_env Whether to remove any existing `virtualenv` with the same name +#' before creating a new one with the requested packages +#' +#' @return The result of `reticulate::py_install()` +#' @export +#' +#' @details +#' See `vignette("setup", package = "laminr")` for further details on setting up +#' a Python environment +#' +#' @examples +#' \dontrun{ +#' install_lamindb() +#' +#' # Add additional packages to the environment +#' install_lamindb(extra_packages = c("bionty", "wetlab")) +#' +#' # Install into a different environment +#' install_lamindb(envvname = "your-env") +#' } +install_lamindb <- function(..., envname = "r-lamindb", extra_packages = NULL, + new_env = identical(envname, "r-lamindb")) { + + if (new_env && reticulate::virtualenv_exists(envname)) { + reticulate::virtualenv_remove(envname) + } + + packages <- unique(c( + "lamindb", + "ipython", + extra_packages + )) + reticulate::py_install(packages = packages, envname = envname, ...) +} diff --git a/R/settings_store.R b/R/settings_store.R index 5289427..8a3e5a9 100644 --- a/R/settings_store.R +++ b/R/settings_store.R @@ -7,7 +7,12 @@ if (settings_dir != "") { file.path(settings_dir, ".lamin") } else { - file.path(Sys.getenv("HOME"), ".lamin") + if (.Platform$OS.type == "windows") { + home_dir <- paste0(Sys.getenv("HOMEDRIVE"), Sys.getenv("HOMEPATH")) + } else { + home_dir <- Sys.getenv("HOME") + } + file.path(home_dir, ".lamin") } } diff --git a/R/zzz.R b/R/zzz.R new file mode 100644 index 0000000..e9fdba0 --- /dev/null +++ b/R/zzz.R @@ -0,0 +1,3 @@ +.onLoad <- function(libname, pkgname) { + reticulate::use_virtualenv("r-lamindb", required = FALSE) +} diff --git a/README.md b/README.md index ccb84a8..1ba4336 100644 --- a/README.md +++ b/README.md @@ -26,12 +26,6 @@ Get started with **{laminr}** by installing the package from CRAN: install.packages("laminr") ``` -You will also need to install the `lamindb` Python package: - -```bash -pip install 'lamindb[aws]>=0.77.2' -``` - ### Additional packages Some functionality requires additional packages. To install all of these use: @@ -51,6 +45,30 @@ This will also install these package for the following tasks: If you choose not to install all packages now you will be prompted to do so whenever one is required. +## Setting up + +Before loading **{laminr}** for the first time you should: + +1. Set up a Python environment + +```r +laminr::install_lamindb() +``` + +2. Log in + +```r +laminr::lamin_login(api_key = "your_api_key") +``` + +3. Set a default instance + +```r +laminr::lamin_connect("/") +``` + +See the [setup vignette](https://laminr.lamin.ai/articles/setup.html) for more information (`vignette("setup", package = "laminr")`). + ## Getting started The best way to get started with **{laminr}** is to explore the package vignettes (available at [laminr.lamin.ai](https://laminr.lamin.ai)): diff --git a/man/install_lamindb.Rd b/man/install_lamindb.Rd new file mode 100644 index 0000000..61cc4ea --- /dev/null +++ b/man/install_lamindb.Rd @@ -0,0 +1,47 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/install.R +\name{install_lamindb} +\alias{install_lamindb} +\title{Install LaminDB} +\usage{ +install_lamindb( + ..., + envname = "r-lamindb", + extra_packages = NULL, + new_env = identical(envname, "r-lamindb") +) +} +\arguments{ +\item{...}{Additional arguments passed to \code{reticulate::py_install()}} + +\item{envname}{String giving the name of the environment to install packages +into} + +\item{extra_packages}{A vector giving the names of additional Python packages +to install} + +\item{new_env}{Whether to remove any existing \code{virtualenv} with the same name +before creating a new one with the requested packages} +} +\value{ +The result of \code{reticulate::py_install()} +} +\description{ +Create a Python environment containing \strong{lamindb} or install \strong{lamindb} +into an existing environment. +} +\details{ +See \code{vignette("setup", package = "laminr")} for further details on setting up +a Python environment +} +\examples{ +\dontrun{ +install_lamindb() + +# Add additional packages to the environment +install_lamindb(extra_packages = c("bionty", "wetlab")) + +# Install into a different environment +install_lamindb(envvname = "your-env") +} +} diff --git a/man/lamin_connect.Rd b/man/lamin_connect.Rd new file mode 100644 index 0000000..e498f65 --- /dev/null +++ b/man/lamin_connect.Rd @@ -0,0 +1,20 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/connect.R +\name{lamin_connect} +\alias{lamin_connect} +\title{Set the default LaminDB instance} +\usage{ +lamin_connect(slug) +} +\arguments{ +\item{slug}{Slug giving the instance to connect to ("\if{html}{\out{}}/\if{html}{\out{}}")} +} +\description{ +Set the default LaminDB instance by calling \verb{lamin connect} on the command +line +} +\examples{ +\dontrun{ +lamin_connect("laminlabs/cellxgene") +} +} diff --git a/man/lamin_login.Rd b/man/lamin_login.Rd new file mode 100644 index 0000000..ecda265 --- /dev/null +++ b/man/lamin_login.Rd @@ -0,0 +1,22 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/connect.R +\name{lamin_login} +\alias{lamin_login} +\title{Login to LaminDB} +\usage{ +lamin_login(user = NULL, api_key = NULL) +} +\arguments{ +\item{user}{Handle for the user to login as} + +\item{api_key}{API key for a user} +} +\description{ +Login as a LaminDB user +} +\details{ +Setting \code{user} will run \verb{lamin login }. Setting \code{api_key} will set the +\code{LAMIN_API_KEY} environment variable tempoarily with \code{withr::with_envvar()} +and run \verb{lamin login}. If neither \code{user} or \code{api_key} are set \verb{lamin login} +will be run if \code{LAMIN_API_KEY} is set. +} diff --git a/vignettes/development.qmd b/vignettes/development.qmd index 1cb3841..3854cfe 100644 --- a/vignettes/development.qmd +++ b/vignettes/development.qmd @@ -14,6 +14,12 @@ This document outlines the features of the **{laminr}** package and the roadmap ## Features +### Setup + +* [x] Create a default Python instance for **{laminr}** (`install_lamindb()`) +* [x] Login to LaminDB from R (`lamin_login()`) +* [x] Set a default instance from R (`lamin_connect()`) + ### Connect to an instance * [x] Connect to a LaminDB instance (`connect()`). @@ -146,6 +152,8 @@ A first version of the package that allows users to: * Introduce data curation features (validation, standardization, annotation). * Enhance support for bionty registries and ontology interactions. * Connect to TileDB-SOMA artifacts. +* Allow users to install **lamindb** and manage a Python environment +* Login and set a default instance from R ### Future versions diff --git a/vignettes/laminr.Rmd b/vignettes/laminr.Rmd index 3712b1b..f160af2 100644 --- a/vignettes/laminr.Rmd +++ b/vignettes/laminr.Rmd @@ -28,21 +28,23 @@ Install **{laminr}** from CRAN: install.packages("laminr", dependencies = TRUE) ``` -Install `lamindb` from PyPI: +Set up the Python environment: -```bash -pip install 'lamindb[aws]>=0.77.2' +```r +laminr::install_lamindb() ``` -Connect to a LaminDB instance on the command line: +Set the default LaminDB instance: -```shell -lamin connect / +```r +laminr::lamin_connect("/") ``` This instance acts as the default instance for everything that follows. Any data and tracking information will be added to it. +See `vignette("setup", package = "laminr")` for more details. + # Start your analysis Load **{laminr}** to get started. diff --git a/vignettes/setup.Rmd b/vignettes/setup.Rmd new file mode 100644 index 0000000..ec04dad --- /dev/null +++ b/vignettes/setup.Rmd @@ -0,0 +1,133 @@ +--- +title: "Setting up laminr" +output: rmarkdown::html_vignette +vignette: > + %\VignetteIndexEntry{Setting up laminr} + %\VignetteEngine{knitr::rmarkdown} + %\VignetteEncoding{UTF-8} +--- + +```{r, include = FALSE} +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>" +) +``` + +# Introduction + +This vignette provides more detailed instructions for how to set up **{laminr}**. + +# Installing **{laminr}** + +The **{laminr}** package can be installed from CRAN: + +```r +install.packages("laminr") +``` + +## Additional packages + +Some functionality in **{laminr}** requires additional packages. +To install all suggested dependencies use: + +```r +install.packages("laminr", dependencies = TRUE) +``` + +If you choose not to install all additional packages you will be prompted to do so as needed. + +# Installing Python **lamindb** + +Using **{laminr}** requires that the Python **lamindb** package is available. + +## Using the included Python environment + +The recommended way to use **{laminr}** is to connect to the included `r-lamindb` Python environment. +This can be created using: + +```r +laminr::install_lamindb() +``` + +This should be run before the first time you load the **{laminr}** library. +The **{reticulate}** package will then be told to use this environment when **{laminr}** is loaded. + +### Adding additional packages + +If you want to add additional packages to the environment, these can be specified using the `extra_packages` argument. +For example, to add the **bionty** package: + +```r +laminr::install_lamindb(extra_packages = "bionty") +``` + +Python packages providing additional registries that may be used in your instance: + +- **bionty** - Basic biological entities, coupled to public ontologies +- **cellregistry** - A registry for single cells +- **clinicore** - Basic clinical entities +- **findrefs** - Store references to studies, reports, papers, blog posts, preprints +- **omop** - OMOP Common Data Model +- **ourprojects** - Manage projects and teams +- **wetlab** - Basic wetlab entities + +Other Python packages you may want to install: + +- **s3fs** - Accessing artifacts stored on Amazon S3 + +Installing additional Python packages also be useful if you want to use another R package which expects a particular Python module to be installed. + +## Using another environment + +In some cases you may prefer to use another Python environment. +This may be the case if you use another R package which requires a Python module or if you are managing your own Python environment. + +Specifying another Python environment to use should be done ***before*** loading **{laminr}** using one of these methods: + +- Loading another R package that sets the Python environment +- Using `reticulate::use_virtualenv()`, `reticulate::use_condaenv()` or `reticulate::use_python()` +- Setting the `RETICULATE_PYTHON` or `RETICULATE_PYTHON_ENV` environment variables + +Details of the current Python environment can be viewed using `reticulate::py_config()`. +For more information about setting the active Python environment see `vignette("versions", package = "reticulate")` (also [available here](https://rstudio.github.io/reticulate/articles/versions.html)). + +### Adding **lamindb** to another environment + +To install **lamindb** into another environment, set the `envname` argument: + +```r +laminr::install_lamindb(envname = "your-env") +``` + +Be aware that there may be dependency conflicts with packages already installed in the environment and installing **lamindb** may cause package versions to change. + +# Logging in to LaminDB + +If you have not used LaminDB before you will need to login to access public instances. +To do this you will need a user API key which you can get by logging in to [LaminHub](https://lamin.ai/dashboard) and going to your user settings. + +You can then login with: + +```r +laminr::lamin_login(api_key = "your_api_key") +``` + +## Switching users + +If you have already given an API key for a user you can log in as them by giving the user name: + +```r +laminr::lamin_login(user = "user_handle") +``` + +# Setting a default instance + +Using `connect(slug = NULL)` will connect to the current default LaminDB instance. +The default instance can be set using: + +``` +laminr::lamin_connect("/") +``` + +Note that this must be called before attempting to connect to a default instance and cannot be changed once the default instance is connected.