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

Installable modules #77

Closed
wants to merge 29 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
f4b9291
Automatic results upload after execution for CdmExecutionSettings
azimov Feb 28, 2023
e45354f
doc strings
azimov Feb 28, 2023
70e1196
doc strings
azimov Feb 28, 2023
1e9a045
Fixed bad type check
azimov Feb 28, 2023
fed5682
missing comma
azimov Feb 28, 2023
d9fa6d7
Revert "missing comma"
azimov Feb 28, 2023
ed42a8c
missing comma
azimov Feb 28, 2023
321d371
missing comma correction
azimov Feb 28, 2023
dec5dd0
whitespace
azimov Feb 28, 2023
800a27f
logic
azimov Feb 28, 2023
a2d6003
expermient with tar_env
azimov Mar 1, 2023
ec789b2
expermient with tar_env removed
azimov Mar 1, 2023
589423e
experiment with tar_env
azimov Mar 1, 2023
bd713ae
experiment with tar_env
azimov Mar 1, 2023
a7e8728
targets tar_script for readability
azimov Mar 1, 2023
2ee618e
Result upload where module functions for specs don't exist handled
azimov Mar 2, 2023
67223ec
Result schema creation and improve readability of code executed in re…
azimov Mar 4, 2023
12b7902
Module execution environment for code readability
azimov Mar 4, 2023
630b09a
Renamed module exec file
azimov Mar 4, 2023
f80849e
Example file
azimov Mar 4, 2023
79c1ed7
Merge branch 'develop' into results-upload
azimov Mar 4, 2023
825e0a6
Removed code
azimov Mar 4, 2023
4a97546
Merge branch 'develop-azimov' into results-upload
azimov Mar 7, 2023
825a81b
missing doc files
azimov Mar 9, 2023
dfc4e5f
outline
azimov Mar 10, 2023
5d1e45f
Merge branch 'develop' into installable-modules
azimov May 12, 2023
de53edf
Started module installer code
azimov May 12, 2023
6d287df
Tests and tidy up
azimov May 12, 2023
cb9c6d9
Merge branch 'develop' into installable-modules
azimov Aug 18, 2023
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
2 changes: 2 additions & 0 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ Imports:
renv (>= 0.15.5),
ParallelLogger (>= 3.1.0),
dplyr,
tidyr,
fs,
checkmate,
keyring,
rlang,
Expand Down
66 changes: 66 additions & 0 deletions R/Module.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Copyright 2023 Observational Health Data Sciences and Informatics
#
# This file is part of Strategus
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


#' Load a StrategusModule s3 class
#' @export
#' @description
#'
#' Returns a list of module info for a given module
#'
loadModule <- function(moduleName = null, version = NULL, folder = NULL, instantiate = FALSE) {
available <- getAvailableModules()
module <- list(

)
class(module) <- "StrategusModule"
return(module)
}


#' Module exec
#' @description
#'
#' Execute ariibtrary code inside a strategus module.
#' This is mainly intended for moudle developers that wish to debug problems.
#' However, it can be used, for example, to execute code with multiple different
#' versions of the same package inside an R session.
#'
#' In order to return data in to an R object you will need to write data to disk and read it back
#' Naturally, this will be much slower than if calling inside a session.
#'
#' @param job Optionally run this task inside an R studio job. This is useful if you
#' want to spawn lots of independent processes. No handling of what happens
#' when this occurs is handled with this function call
#'
#' @param useLocalStrategusLibrary On some systems finding the base Strategus package can fail.
#' By setting this to true the execution of the code will first load the
#' calling Strategus library. This is only needed if you are calling other
#' strategus functions
#'
moduleExec <- function(module, code, substituteVars = list(), useLocalStrategusLibrary = FALSE, job = job) {
checkmate::expect_class(module, "StrategusModule")

tempFile <- withModuleRenv(code,
module$moduleFolder,
injectVars = list(),
tempScriptFile = tempfile(fileext = ".R"),
useLocalStrategusLibrary = useLocalStrategusLibrary,
job = job,
processName = paste(module$moduleFolder, "_renv_run"))

unlink(tempFile)
}
147 changes: 147 additions & 0 deletions R/ModuleInstaller.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# Copyright 2023 Observational Health Data Sciences and Informatics
#
# This file is part of Strategus
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


checkModuleDependencies <- function(dependencies, moduleSet) {

for (dep in dependencies) {
dp <- strsplit(dep, split = " ")[[1]]

if (!dp[1] %in% moduleSet$moduleName) {
stop("Dependent module not installed - ", dp)
}

# TODO: Check version
}
}


#' List currently downloaded module cache
#' @description
#' This returns the set of currently installed in the specifed system path
#'
#' @export
getAvailableModules <- function(installedModulesPath = Sys.getenv("STRATEGUS_INSTALLED_MODULES")) {

if (installedModulesPath == "") {
warning("No system module path set, set STRATEGUS_INSTALLED_MODULES in your .renviron to enable global modules")
}

modulesRdsFile <- file.path(installedModulesPath, "installedModules.rds")

if (!file.exists(modulesRdsFile)) {
return(data.frame(moduleName = c(),
modulePath = c(),
dependencies = c(),
version = c()))
}

return(readRDS(modulesRdsFile))
}

#' Install a module into the cache from local directory
#' @description
#'
#' Modules can be installed but not instantiated. To be instantiated is to have the renv initialised with all required
#' packages installed. This is a required step for running code inside a module but not for creating study configuration
#' files.
#'
#'
installLocalModule <- function(pathToModule,
overwrite = FALSE,
instantiate = FALSE,
installedModulesPath = Sys.getenv("STRATEGUS_INSTALLED_MODULES")) {
# Validate required module files are present
requiredFiles <- c(
"MetaData.json",
"Main.R",
"SettingsFunctions.R",
"renv.lock"
)

checkmate::assertFileExists(file.path(pathToModule, requiredFiles))

if (!dir.exists(installedModulesPath)) {
dir.create(installedModulesPath)
}
# lock rds object
installLockFile <- file.path(installedModulesPath, "install_module.lock")
if (file.exists(installLockFile)) {
ts <- readLines(installLockFile)
stop("Cannot install module operation is in progress or ", installLockFile, "must be manually deleted.\n", ts)
}

# Save a timestamp inside a file to inform when last lock was
writeLines(timestamp(quiet = TRUE), con = installLockFile)
on.exit(unlink(installLockFile, force = TRUE))
currentModules <- getAvailableModules(installedModulesPath = installedModulesPath)
metaData <- getModuleMetaData(pathToModule)

if (nrow(currentModules)) {
if (!overwrite) {
installCount <- currentModules %>%
dplyr::filter(.data$moduleName == metaData$Name,
.data$version == metaData$Version) %>%
dplyr::count() %>%
dplyr::pull()

if (installCount != 0)
stop("Module of same name and version already exists in cache. Use overwrite = TRUE to reinstall")
}
# Check dependencies - any dependent modules must be installed in the cache
checkModuleDependencies(metaData$Dependencies, currentModules)
} else if (length(metaData$Dependencies)) {
stop("Dependent modules not installed - ", metaData$Dependencies)
}
installPath <- file.path(installedModulesPath, metaData$Name, metaData$Version)

if (dir.exists(installPath))
unlink(installPath, recursive = TRUE, force = TRUE)

iRow <- tibble::tibble(moduleName = c(metaData$Name),
modulePath = c(installPath),
dependencies = paste(metaData$Dependencies, collapse = ";"),
version = c(metaData$Version))

fs::dir_copy(pathToModule, installPath, overwrite = TRUE)

currentModules <- dplyr::bind_rows(currentModules, iRow)
# Write to install path
modulesRdsFile <- file.path(installedModulesPath, "installedModules.rds")
# save rds
saveRDS(currentModules %>% dplyr::distinct(), modulesRdsFile)

message(paste("Module", iRow$moduleName, "Installed"))
invisible(NULL)
}

#' Install a module from github
#'
#'
installGithubModule <- function(repoPath, ref = "main", ...) {
# Download tarball
tfile <- tempfile(fileext = ".tar.gz")
on.exit(unlink(tfile))

download.file(url = paste0("http://github.com/", repoPath, "/tarball/", ref), destfile = tfile)
tdir <- tempfile()
on.exit(unlink(tdir, recursive = TRUE), add = TRUE)

utils::untar(tfile, exdir = tdir)
finalDir <- file.path(tdir, list.files(tdir)[1])

installLocalModule(finalDir, ...)
}
2 changes: 2 additions & 0 deletions R/ModuleInstantiation.R
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ getModuleMetaData <- function(moduleFolder) {
stop(sprintf("Meta-data JSON not found in '%s'.", moduleFolder))
}
metaData <- ParallelLogger::loadSettingsFromJson(jsonFileName)
checkmate::assertNames(names(metaData), must.include = c("Name", "Version", "Dependencies"))

return(metaData)
}

Expand Down
17 changes: 17 additions & 0 deletions man/runResultsUpload.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 22 additions & 0 deletions tests/testthat/test-ModuleInstaller.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
test_that("Modules install from github", {
installedModulesPath <- tempfile()
unlink(installedModulesPath, recursive = TRUE)
on.exit(unlink(installedModulesPath, recursive = TRUE))

installGithubModule("OHDSI/CohortGeneratorModule", installedModulesPath = installedModulesPath)
checkmate::expect_directory_exists(file.path(installedModulesPath, "/CohortGeneratorModule"))
expect_error(installGithubModule("OHDSI/CohortGeneratorModule", installedModulesPath = installedModulesPath))

checkmate::expect_data_frame(getAvailableModules(installedModulesPath))

installGithubModule("OHDSI/CohortGeneratorModule", installedModulesPath = installedModulesPath, overwrite = TRUE)
installGithubModule("OHDSI/CohortMethodModule", installedModulesPath = installedModulesPath)
checkmate::expect_directory_exists(file.path(installedModulesPath, "CohortGeneratorModule"))

# Should not be able to install if lock file is present
installLockFile <- file.path(installedModulesPath, "install_module.lock")
writeLines(timestamp(quiet = TRUE), con = installLockFile)

expect_error(installGithubModule("OHDSI/CohortGeneratorModule", installedModulesPath = installedModulesPath, overwrite = TRUE))
unlink(installLockFile)
})