├── .Rprofile ├── config.yml ├── .gitignore ├── renv ├── .gitignore └── activate.R ├── Hrafnagud-Diagram.png ├── .lintr ├── dependencies.R ├── Hrafnagud-Dynamo.Rproj ├── entrypoint.R ├── README.md ├── plumber.R ├── utils ├── supabase_utils.R └── dynamo_utils.R └── renv.lock /.Rprofile: -------------------------------------------------------------------------------- 1 | source("renv/activate.R") 2 | -------------------------------------------------------------------------------- /config.yml: -------------------------------------------------------------------------------- 1 | default: 2 | database_utils: "supabase" 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Rproj.user 2 | .Rhistory 3 | .RData 4 | .Ruserdata 5 | -------------------------------------------------------------------------------- /renv/.gitignore: -------------------------------------------------------------------------------- 1 | library/ 2 | local/ 3 | cellar/ 4 | lock/ 5 | python/ 6 | sandbox/ 7 | staging/ 8 | -------------------------------------------------------------------------------- /Hrafnagud-Diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepanshKhurana/Hrafnagud-Dynamo/HEAD/Hrafnagud-Diagram.png -------------------------------------------------------------------------------- /.lintr: -------------------------------------------------------------------------------- 1 | linters: 2 | linters_with_defaults( 3 | line_length_linter = line_length_linter(80), 4 | object_usage_linter = NULL # Does not work with `box::use()`. 5 | ) 6 | -------------------------------------------------------------------------------- /dependencies.R: -------------------------------------------------------------------------------- 1 | library(box) 2 | library(plumber) 3 | library(googlesheets4) 4 | library(yaml) 5 | library(glue) 6 | library(dplyr) 7 | library(styler) 8 | library(lintr) 9 | library(here) 10 | library(paws.database) 11 | library(lubridate) 12 | library(stringr) 13 | library(rvest) 14 | -------------------------------------------------------------------------------- /Hrafnagud-Dynamo.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: Default 4 | SaveWorkspace: Default 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: Yes 9 | NumSpacesForTab: 2 10 | Encoding: UTF-8 11 | 12 | RnwWeave: Sweave 13 | LaTeX: pdfLaTeX 14 | 15 | AutoAppendNewline: Yes 16 | StripTrailingWhitespace: Yes 17 | -------------------------------------------------------------------------------- /entrypoint.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | plumber[ 3 | pr, 4 | pr_run, 5 | pr_set_api_spec 6 | ], 7 | here[ 8 | here 9 | ] 10 | ) 11 | 12 | add_auth <- function( 13 | api, 14 | paths = NULL 15 | ) { 16 | 17 | api[["components"]] <- list( 18 | securitySchemes = list( 19 | ApiKeyAuth = list( 20 | type = "apiKey", 21 | `in` = "header", 22 | name = "X-API-KEY", 23 | description = "Add API Key here" 24 | ) 25 | ) 26 | ) 27 | 28 | if (is.null(paths)) paths <- names(api$paths) 29 | for (path in paths) { 30 | nn <- names(api$paths[[path]]) 31 | for (p in intersect(nn, c("get", "head", "post", "put", "delete"))) { 32 | api$paths[[path]][[p]] <- c( 33 | api$paths[[path]][[p]], 34 | list(security = list(list(ApiKeyAuth = vector()))) 35 | ) 36 | } 37 | } 38 | 39 | api 40 | 41 | } 42 | 43 | pr("plumber.R") |> 44 | pr_set_api_spec(add_auth) |> 45 | pr_run( 46 | port = 8008, 47 | host = "0.0.0.0" 48 | ) 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hrafnagud / Raven God 2 | 3 | The goal for this API is to be a be-all, end-all of personal data analysis and collection. 4 | 5 | This needs AWS DynamoDb to work and thus, needs environment variables. However, the codebase is free for anyone to steal. Yes, I mean that! 🦝 6 | 7 | I especially want you to take the DynamoDb CRUD API and see what you can build from it. 8 | 9 | **Argus:** For a simple app that lets you use the CRUD API to perform operations, check out [Argus](https://github.com/DeepanshKhurana/Argus/tree/main). 10 | 11 | ## Wishlist of Potential Improvements 12 | 13 | - `widgets/` endpoints for each important table or piece of data to render things on the fly 14 | - Load balancer for the API 15 | 16 | ## System-level Diagram 17 | 18 | ![Hrafnagud](Hrafnagud-Diagram.png) 19 | 20 | ## Apps (Preview) 21 | 22 | The code for these apps is not available here but this preview is to show that Shiny is a perfectly good framework to use an API like Hrafnagud and craft wonderful looking apps. 23 | 24 | apps 25 | 26 | ## Full Story 27 | 28 | Over a year ago, I saw a problem. I wanted to track my financial growth, how everything was performing, what was my total networth across instruments, but all apps I saw were bloated at best and would surely sell my information at worst. So, I set out to understand financial calculations and metrics, became an expert in it overnight (haha!) and built myself an app called Ebenezer (named for Ebenezer Scrooge). 29 | 30 | A few days later, I wanted to plan a trip so I went to Google Trips and realised they had removed Trip Summaries. All my data was lost, and which was worse, I had no way to plan a trip. I tried to seek existing apps but again, everything was too much. All I wanted was to have a way to add my itinerary and view it later. So, I built myself an app called Livingston (named after Jonathan Livingston Seagull). 31 | 32 | A pattern was starting to emerge: bespoke apps named after literary characters which were now the mainstays of my browser window. 33 | 34 | I saw a vision: what if there was an app that showed me all the information from my life at a glance? My networth from Ebenezer. My upcoming trips from Livingston. And possible many other apps I would build for myself down the line. Then, if I wanted to dive further into a part of my life, I could hit a button and go to the app for more details. 35 | 36 | And since it was a "vision", I dubbed my project Hrafnagud or Raven God, after the Norse God Odin who sees everything, at all times. 37 | 38 | A year of experimentation later, the project is a reality, and it is a culmination of everything I have ever learned in the world of R: AWS DynamoDb, AWS S3, AWS EC2, Plumber, ShinyProxy, tidyverse, Rhino and so much more. 39 | 40 | In this talk, I will walk you through the journey of how this extremely ambitious, infinitely extensible project came to fruition. I will share my learnings for all the technologies I used, for all the challenges I faced, and not just that, I will show you little hacks like making your own Google Finance API using Google Sheets and a little bit of R + Plumber magic. 41 | 42 | What do I want to leave you with? I want to leave you inspired, and I want to leave you empowered to build applications and APIs not just for work, but for yourself, so the next time you come across an app or service that just doesn't feel right, you do not settle. 43 | 44 | And of course, I will not leave you empty handed. The open-source version of my code will be available along with the talk as well. 45 | -------------------------------------------------------------------------------- /plumber.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | plumber[...], 3 | config[ 4 | get 5 | ], 6 | dplyr[ 7 | mutate, 8 | case_when, 9 | summarise, 10 | group_by, 11 | n, 12 | select 13 | ], 14 | lubridate[ 15 | dmy, 16 | month, 17 | year 18 | ], 19 | tidyr[ 20 | complete 21 | ], 22 | purrr[ 23 | map_dbl 24 | ], 25 | checkmate[ 26 | assert_subset 27 | ], 28 | ) 29 | 30 | database_utils <- get("database_utils") 31 | 32 | assert_subset( 33 | database_utils, 34 | c( 35 | "dynamodb", 36 | "supabase" 37 | ) 38 | ) 39 | 40 | if (database_utils == "supabase") { 41 | box::use( 42 | utils/supabase_utils[ 43 | get_table_data, 44 | get_table_schema, 45 | put_table_row, 46 | delete_table_row 47 | ], 48 | ) 49 | } 50 | 51 | if (database_utils == "dynamodb") { 52 | box::use( 53 | utils/dynamo_utils[ 54 | get_table_data = get_processed_table_data, 55 | get_table_schema, 56 | put_table_row, 57 | delete_table_row 58 | ], 59 | ) 60 | } 61 | 62 | # Helper ---- 63 | 64 | #' Function to help with API Authentication 65 | #' @param res the response object from Plumber 66 | #' @param req the request object from Plumber 67 | #' @param FUN the function call if authentication succeeds 68 | #' @param ... params to pass to the function handler 69 | auth_helper <- function( 70 | res, 71 | req, 72 | FUN, #nolint: object_name_linter 73 | ... 74 | ) { 75 | req_has_key <- "HTTP_X_API_KEY" %in% names(req) 76 | key_is_valid <- req$HTTP_X_API_KEY == Sys.getenv("API_KEY") 77 | environment_not_set <- nchar(Sys.getenv("API_KEY")) <= 1 78 | if (!req_has_key || !key_is_valid || environment_not_set) { 79 | res$body <- "Unauthorized" 80 | res$status <- 401 81 | return("Missing or invalid API key, or invalid configuration!") 82 | } else { 83 | FUN(...) #nolint: object_name_linter 84 | } 85 | } 86 | 87 | # API Spec ---- 88 | 89 | #* @apiTitle Hrafnagud 90 | #* @apiDescription An all-seeing API for personal use 91 | #* @apiTag CRUD DynamoDb Utility Endpoints 92 | 93 | ## CRUD ---- 94 | 95 | ### Schema ---- 96 | 97 | #* Schema 98 | #* @param table_name:chr The table name to fetch the schema for. 99 | #* @get /schema 100 | #* @tag CRUD 101 | function( 102 | res, 103 | req, 104 | table_name 105 | ) { 106 | auth_helper( 107 | res, 108 | req, 109 | get_table_schema, 110 | table_name = table_name 111 | ) 112 | 113 | } 114 | 115 | ### Create ---- 116 | 117 | #* New row 118 | #* @param table_name:chr The table name to add the row to. 119 | #* @param input_list:[chr] The list of values to add in the row. 120 | #* @param show_old:logical Show the last values of the row? 121 | #* @put /create 122 | #* @tag CRUD 123 | function( 124 | res, 125 | req, 126 | table_name, 127 | input_list, 128 | show_old 129 | ) { 130 | auth_helper( 131 | res, 132 | req, 133 | put_table_row, 134 | table_name = table_name, 135 | input_list = as.list(input_list), 136 | show_old = as.logical(show_old) 137 | ) 138 | } 139 | 140 | ### Read ---- 141 | 142 | #* Table 143 | #* @param table_name:chr The table name to fetch data from. 144 | #* @param limit:numeric The number of rows to limit at. Use 0 for all rows. 145 | #* @get /read 146 | #* @tag CRUD 147 | function( 148 | res, 149 | req, 150 | table_name, 151 | limit 152 | ) { 153 | auth_helper( 154 | res, 155 | req, 156 | get_table_data, 157 | table_name = table_name, 158 | limit = as.numeric(limit) 159 | ) 160 | } 161 | 162 | ### Update ---- 163 | 164 | #* Update row 165 | #* @param table_name:chr The table name to modify the row in. 166 | #* @param input_list:[chr] The list of values to add in the row. 167 | #* @put /update 168 | #* @tag CRUD 169 | function( 170 | res, 171 | req, 172 | table_name, 173 | input_list, 174 | ) { 175 | auth_helper( 176 | res, 177 | req, 178 | put_table_row, 179 | table_name = table_name, 180 | input_list = as.list(input_list), 181 | is_update = TRUE 182 | ) 183 | } 184 | 185 | ### Delete ---- 186 | 187 | #* Delete row 188 | #* @param table_name:chr The table name to remove the row from. 189 | #* @param row_key:numeric The index of the row to delete. 190 | #* @delete /delete 191 | #* @tag CRUD 192 | function( 193 | res, 194 | req, 195 | table_name, 196 | row_key, 197 | ) { 198 | auth_helper( 199 | res, 200 | req, 201 | delete_table_row, 202 | table_name = table_name, 203 | id_value = as.numeric(row_key) 204 | ) 205 | } 206 | -------------------------------------------------------------------------------- /utils/supabase_utils.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | DBI[ 3 | dbConnect, 4 | dbDisconnect, 5 | dbExecute, 6 | dbGetQuery, 7 | dbQuoteLiteral 8 | ], 9 | purrr[ 10 | map2 11 | ], 12 | RPostgres[ 13 | Postgres 14 | ], 15 | glue[ 16 | glue, 17 | glue_collapse, 18 | glue_sql, 19 | glue_sql_collapse 20 | ], 21 | checkmate[ 22 | assert, 23 | assert_class, 24 | assert_string, 25 | check_string, 26 | check_logical, 27 | assert_list, 28 | check_numeric, 29 | check_list, 30 | check_class 31 | ], 32 | ) 33 | 34 | #' Helper function generate an error message 35 | generate_error <- function() { 36 | print( 37 | "Cannot connect to Supabase. Are environment variables set?" 38 | ) 39 | } 40 | 41 | #' Read Supabase credentials from environment variables 42 | #' 43 | #' @return A list of Supabase credentials. 44 | read_supabase_creds <- function() { 45 | creds <- list( 46 | host = Sys.getenv("SUPABASE_HOST"), 47 | port = 6543, 48 | dbname = Sys.getenv("SUPABASE_DBNAME"), 49 | user = Sys.getenv("SUPABASE_USER"), 50 | password = Sys.getenv("SUPABASE_PASSWORD") 51 | ) 52 | if (any(sapply(creds, nchar) == 0)) { 53 | stop(generate_error()) 54 | } else { 55 | creds 56 | } 57 | } 58 | 59 | #' Create a database connection 60 | #' 61 | #' @param supabase_creds A list of Supabase credentials. 62 | #' @return A database connection object. 63 | make_connection <- function( 64 | supabase_creds = read_supabase_creds() 65 | ) { 66 | dbConnect( 67 | Postgres(), 68 | host = supabase_creds$host, 69 | port = supabase_creds$port, 70 | dbname = supabase_creds$dbname, 71 | user = supabase_creds$user, 72 | password = supabase_creds$password 73 | ) 74 | } 75 | 76 | #' Get a list of tables in the schema 77 | #' 78 | #' @param table_schema The schema name. 79 | #' @param conn A database connection object. 80 | #' @return A vector of table names. 81 | get_table_list <- function( 82 | table_schema = "hrafnagud", 83 | conn = make_connection() 84 | ) { 85 | dbGetQuery( 86 | conn, 87 | glue_sql( 88 | " 89 | SELECT table_name 90 | FROM information_schema.tables 91 | WHERE table_schema = {table_schema} 92 | AND table_type = 'BASE TABLE'; 93 | ", 94 | .con = conn 95 | ) 96 | ) |> 97 | as.list() |> 98 | unname() |> 99 | unlist() 100 | } 101 | 102 | #' Check if the table exists 103 | #' 104 | #' @param table_name The name of the table. 105 | #' @param conn A database connection object. 106 | #' @return TRUE if the table exists, FALSE otherwise. 107 | is_valid_table <- function( 108 | table_name = NULL, 109 | conn = make_connection() 110 | ) { 111 | assert_string(table_name) 112 | table_name %in% get_table_list(conn = conn) 113 | } 114 | 115 | #' Get the latest key value for a table 116 | #' 117 | #' @param table_name The name of the table. 118 | #' @param id_column The ID column name. 119 | #' @param schema The schema name. 120 | #' @param conn A database connection object. 121 | #' @return The latest key value plus one. 122 | get_latest_key <- function( 123 | table_name = NULL, 124 | id_column = "id", 125 | schema = "hrafnagud", 126 | conn = make_connection() 127 | ) { 128 | assert( 129 | check_string(table_name), 130 | check_string(id_column), 131 | combine = "and" 132 | ) 133 | 134 | if (is_valid_table(table_name, conn)) { 135 | latest_key_query <- glue( 136 | " 137 | SELECT {id_column} 138 | FROM {schema}.{table_name} 139 | ORDER BY {id_column} DESC 140 | LIMIT 1 141 | ", 142 | .con = conn 143 | ) 144 | latest_key <- dbGetQuery(conn, latest_key_query) 145 | 146 | if (nrow(latest_key) > 0) { 147 | as.numeric(latest_key[[1]]) 148 | } else { 149 | 1 150 | } 151 | } else { 152 | stop( 153 | glue( 154 | "Table '{table_name}' does not exist!" 155 | ) 156 | ) 157 | } 158 | } 159 | 160 | #' Retrieve the schema of a table 161 | #' 162 | #' @param table_name The name of the table. 163 | #' @param schema The schema name. 164 | #' @param conn A database connection object. 165 | #' @return A data frame with table schema details. 166 | #' @export 167 | get_table_schema <- function( 168 | table_name = NULL, 169 | schema = "hrafnagud", 170 | conn = make_connection() 171 | ) { 172 | assert( 173 | check_string(table_name), 174 | check_string(schema), 175 | combine = "and" 176 | ) 177 | if (is_valid_table(table_name, conn)) { 178 | schema_query <- glue_sql( 179 | " 180 | SELECT column_name, data_type 181 | FROM information_schema.columns 182 | WHERE table_schema = {schema} 183 | AND table_name = {table_name} 184 | ", 185 | .con = conn 186 | ) 187 | dbGetQuery(conn, schema_query) 188 | } else { 189 | stop( 190 | glue( 191 | "Table '{table_name}' does not exist!" 192 | ) 193 | ) 194 | } 195 | } 196 | 197 | #' Retrieve data from a table 198 | #' 199 | #' @param table_name The name of the table. 200 | #' @param limit The number of rows to retrieve. 201 | #' @param schema The schema name. 202 | #' @param conn A database connection object. 203 | #' @return A data frame with table data. 204 | #' @export 205 | get_table_data <- function( 206 | table_name = NULL, 207 | limit = 0, 208 | schema = "hrafnagud", 209 | conn = make_connection() 210 | ) { 211 | on.exit(dbDisconnect(conn)) 212 | assert( 213 | check_string(table_name), 214 | check_numeric(limit), 215 | combine = "and" 216 | ) 217 | 218 | query_filter <- if (limit > 0) glue("LIMIT {limit}") else "" 219 | 220 | if (is_valid_table(table_name, conn)) { 221 | dbGetQuery( 222 | conn, 223 | glue( 224 | " 225 | SELECT * FROM {schema}.{table_name} 226 | {query_filter} 227 | ", 228 | .con = conn 229 | ) 230 | ) 231 | } else { 232 | stop( 233 | glue( 234 | "Table '{table_name}' does not exist!" 235 | ) 236 | ) 237 | } 238 | } 239 | 240 | #' Map SQL data types to R data types 241 | #' 242 | #' @param data_type SQL data type as a character string. 243 | #' @param type_mapping A named list mapping SQL data types to R data types. 244 | #' @return Corresponding R data type as a character string. 245 | map_sql_to_r <- function( 246 | data_type, 247 | type_mapping = list( 248 | "bigint" = as.numeric, 249 | "text" = as.character, 250 | "double precision" = as.numeric, 251 | "date" = as.Date, 252 | "timestamp with time zone" = as.POSIXct, 253 | "boolean" = as.logical 254 | ) 255 | ) { 256 | matched_type <- type_mapping[[data_type]] 257 | if (is.null(matched_type)) { 258 | stop(glue("Unrecognized data type: {data_type}")) 259 | } 260 | matched_type 261 | } 262 | 263 | #' Filter columns based on schema and operation type 264 | #' 265 | #' @param schema_info A data frame containing schema information. 266 | #' @param is_update Logical indicating whether the operation is an update. 267 | #' @return A vector of column names to include. 268 | filter_columns <- function( 269 | schema_info, 270 | is_update = FALSE 271 | ) { 272 | schema_info$column_name[ 273 | schema_info$column_name != "created_at" 274 | ] 275 | } 276 | 277 | #' Insert or update a table row 278 | #' 279 | #' @param table_name The name of the table. 280 | #' @param input_list A list of column-value pairs. 281 | #' @param is_update Whether the operation is an update. 282 | #' @param schema The schema name. 283 | #' @param conn A database connection object. 284 | #' @export 285 | put_table_row <- function( 286 | table_name = NULL, 287 | input_list = list(), 288 | is_update = FALSE, 289 | schema = "hrafnagud", 290 | conn = make_connection() 291 | ) { 292 | on.exit(dbDisconnect(conn)) 293 | assert( 294 | check_string(table_name), 295 | check_list(input_list), 296 | check_logical(is_update), 297 | combine = "and" 298 | ) 299 | 300 | if (is_valid_table(table_name, conn)) { 301 | table_schema <- get_table_schema(table_name) 302 | columns <- filter_columns(table_schema, is_update) 303 | 304 | if (!is_update) { 305 | input_list <- c( 306 | id = 1 + get_latest_key( 307 | table_name, 308 | schema = schema, 309 | conn = conn 310 | ), 311 | input_list 312 | ) 313 | } 314 | 315 | names(input_list) <- columns 316 | 317 | input_list <- lapply( 318 | seq_along(input_list), 319 | function(index) { 320 | tryCatch( 321 | expr = { 322 | FUN <- map_sql_to_r( 323 | table_schema$data_type[index] |> 324 | unlist() 325 | ) 326 | FUN( 327 | input_list[index] |> 328 | as.character() 329 | ) 330 | } 331 | ) 332 | } 333 | ) 334 | 335 | values <- lapply( 336 | input_list, 337 | function(x) dbQuoteLiteral(conn, x) 338 | ) 339 | 340 | if (is_update) { 341 | set_clause <- glue_sql_collapse( 342 | mapply( 343 | function(col, val) { 344 | glue_sql( 345 | "{`col`} = {val}", 346 | .con = conn 347 | ) 348 | }, 349 | columns, 350 | values, 351 | SIMPLIFY = FALSE 352 | ), 353 | sep = ", " 354 | ) 355 | query <- glue_sql( 356 | "UPDATE {`schema`}.{`table_name`} 357 | SET {set_clause} 358 | WHERE id = {input_list[[1]]}", 359 | .con = conn 360 | ) 361 | } else { 362 | query <- glue_sql( 363 | "INSERT INTO {`schema`}.{`table_name`} 364 | ({glue_sql_collapse(`columns`, sep = ', ')}) 365 | VALUES ({glue_sql_collapse(values, sep = ', ')})", 366 | .con = conn 367 | ) 368 | } 369 | 370 | dbExecute(conn, query) 371 | } else { 372 | stop(glue("Table '{table_name}' does not exist!")) 373 | } 374 | } 375 | 376 | #' Delete a row from a table 377 | #' 378 | #' @param table_name The name of the table. 379 | #' @param id_value The ID value of the row to delete. 380 | #' @param id_column The ID column name. 381 | #' @param schema The schema name. 382 | #' @param conn A database connection object. 383 | #' @export 384 | delete_table_row <- function( 385 | table_name = NULL, 386 | id_value = NULL, 387 | id_column = "id", 388 | schema = "hrafnagud", 389 | conn = make_connection() 390 | ) { 391 | on.exit(dbDisconnect(conn)) 392 | assert( 393 | check_string(table_name), 394 | check_numeric(id_value), 395 | check_string(id_column), 396 | combine = "and" 397 | ) 398 | 399 | if (is_valid_table(table_name, conn)) { 400 | delete_query <- glue( 401 | " 402 | DELETE FROM {schema}.{table_name} 403 | WHERE {id_column} = {id_value} 404 | " 405 | ) 406 | dbExecute(conn, delete_query) 407 | } else { 408 | stop( 409 | glue( 410 | "Table '{table_name}' does not exist!" 411 | ) 412 | ) 413 | } 414 | } 415 | -------------------------------------------------------------------------------- /utils/dynamo_utils.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | paws[ 3 | dynamodb 4 | ], 5 | checkmate[ 6 | assert, 7 | assert_string, 8 | check_string, 9 | check_logical, 10 | assert_list, 11 | check_numeric, 12 | check_list 13 | ], 14 | glue[glue], 15 | dplyr[ 16 | na_if, 17 | mutate_all, 18 | arrange, 19 | select 20 | ], 21 | methods[as], 22 | stats[setNames], 23 | utils[ 24 | tail 25 | ] 26 | ) 27 | 28 | #' Helper function generate an error message 29 | generate_error <- function() { 30 | print( 31 | "Cannot connect to DynamoDb. Are environment variables set?" 32 | ) 33 | } 34 | 35 | #' Simple function to check if table name is valid 36 | #' @param table_name Character string. Table name to validate. 37 | #' @param conn `paws::dynamodb()` object. Dependent on envvars. 38 | #' @return Logical. Whether table exists or not. 39 | #' @export 40 | is_valid_table <- function( 41 | table_name = NULL, 42 | conn = dynamodb() 43 | ) { 44 | assert_string(table_name) 45 | if (!is.null(conn)) { 46 | table_name %in% conn$list_tables()$TableNames 47 | } else { 48 | stop(generate_error()) 49 | } 50 | } 51 | 52 | #' Function to get the table data from DynamoDb 53 | #' @param table_name Character string. Table name to validate. 54 | #' @param limit Numeric. If 0, then no limit is set. 55 | #' @param conn `paws::dynamodb()` object. Dependent on envvars. 56 | #' @return `list` object with the data 57 | get_table_data <- function( 58 | table_name = NULL, 59 | limit = 0, 60 | conn = dynamodb() 61 | ) { 62 | assert( 63 | check_string(table_name), 64 | check_numeric(limit), 65 | combine = "and" 66 | ) 67 | 68 | if (is_valid_table(table_name, conn)) { 69 | conn$scan( 70 | TableName = table_name, 71 | Limit = ifelse( 72 | limit == 0, 73 | NA, 74 | limit 75 | ), 76 | ConsistentRead = TRUE 77 | ) 78 | } else { 79 | stop( 80 | glue( 81 | "Table '{table_name}' does not exist!" 82 | ) 83 | ) 84 | } 85 | } 86 | 87 | #' Function to process the object received from DynamoDb 88 | #' @param scan_data `list` object with the data 89 | #' @param null_list Character vector. Default is "NULL" and "" 90 | #' @return `data.frame` object 91 | process_table_scan <- function( 92 | scan_data = NULL, 93 | null_list = c("NULL", "") 94 | ) { 95 | assert_list(scan_data) 96 | items <- lapply(scan_data$Items, unlist) 97 | items <- lapply( 98 | items, function(x) { 99 | x[sort(names(x))] 100 | } 101 | ) |> 102 | data.frame() |> 103 | t() |> 104 | data.frame() 105 | 106 | column_names <- colnames(items) 107 | 108 | # Use the X.Type syntax to fix types 109 | 110 | items <- fix_data_types(items) 111 | 112 | # Parse the nested list 113 | 114 | colnames(items) <- lapply( 115 | strsplit( 116 | column_names, "\\." 117 | ), function(x) unlist(x)[[1]] 118 | ) |> 119 | unlist() 120 | 121 | # Find rownames & replace "NULL" with NA 122 | 123 | rownames(items) <- items$id 124 | items[ 125 | order(as.numeric(items$id)), 126 | ] |> 127 | mutate_all( 128 | ~ ifelse( 129 | . %in% null_list, 130 | NA, 131 | . 132 | ) 133 | ) 134 | } 135 | 136 | #' Wrapper to both get and process the table received from DynamoDb 137 | #' @param table_name Character string. Table name to fetch. 138 | #' @param limit Numeric. If 0, then no limit is set. 139 | #' @param conn `paws::dynamodb()` object. Dependent on envvars. 140 | #' @return `data.frame` object 141 | #' @export 142 | get_processed_table_data <- function( 143 | table_name = NULL, 144 | limit = 0, 145 | conn = dynamodb() 146 | ) { 147 | process_table_scan( 148 | get_table_data( 149 | table_name, 150 | limit, 151 | conn 152 | ) 153 | ) 154 | } 155 | 156 | #' A simple function to get the mapping list for types 157 | #' @return List. Data type map for DynamoDb and R 158 | get_type_mapping <- function() { 159 | list( 160 | S = "character", 161 | N = "numeric", 162 | B = "raw", 163 | BOOL = "logical", 164 | `NULL` = "character", 165 | M = "list", 166 | L = "list", 167 | SS = "list", 168 | NS = "list", 169 | BS = "list" 170 | ) 171 | } 172 | 173 | #' Fix data types in a dataframe 174 | #' 175 | #' @param dataframe The dataframe to be processed. 176 | #' @param type_mapping A list specifying the mapping of DynamoDb types to 177 | #' R data types. Default is `get_type_mapping()` 178 | #' @return The dataframe with corrected data types. 179 | fix_data_types <- function( 180 | dataframe, 181 | type_mapping = get_type_mapping() 182 | ) { 183 | col_names <- names(dataframe) 184 | types <- gsub(".*\\.", "", col_names) 185 | dataframe <- data.frame( 186 | lapply(seq_along(col_names), function(column) { 187 | as(dataframe[[column]], type_mapping[[types[column]]]) 188 | }) 189 | ) 190 | } 191 | 192 | #' Get the schema of a table 193 | #' 194 | #' @param table_name Character string. Table name to fetch schema for 195 | #' @return A named list with the correct data types 196 | #' @export 197 | get_table_schema <- function( 198 | table_name = NULL, 199 | conn = dynamodb() 200 | ) { 201 | assert_string(table_name) 202 | if (is_valid_table(table_name, conn)) { 203 | data_row <- get_processed_table_data(table_name, limit = 1, conn) 204 | list( 205 | classes = lapply( 206 | lapply( 207 | data_row, 208 | function(value) ifelse(!is.na(value), value, "") 209 | ), 210 | class 211 | ), 212 | sample = data_row 213 | ) 214 | } else { 215 | stop( 216 | glue( 217 | "Table '{table_name}' does not exist!" 218 | ) 219 | ) 220 | } 221 | } 222 | 223 | #' Add or update a row in a table 224 | #' 225 | #' @param table_name Character string. Table name to use 226 | #' @param input_list List. The data to enter into the row 227 | #' @param type_mapping List. A list of mapping for DynamoDb & R classes 228 | #' @param is_update Logical. Whether this is an update or a new row 229 | #' @param conn `paws::dynamodb()` object. Dependent on envvars. 230 | #' 231 | #' @export 232 | put_table_row <- function( 233 | table_name = NULL, 234 | input_list = list(), 235 | type_mapping = get_type_mapping(), 236 | is_update = FALSE, 237 | conn = dynamodb() 238 | ) { 239 | 240 | assert( 241 | check_string(table_name), 242 | check_list(type_mapping), 243 | check_list(input_list), 244 | check_logical(is_update), 245 | combine = "and" 246 | ) 247 | 248 | if (is_valid_table(table_name, conn)) { 249 | 250 | put_list <- process_table_input_row( 251 | input_list = input_list, 252 | table_name = table_name, 253 | type_mapping = type_mapping, 254 | is_update = is_update, 255 | conn = conn 256 | ) 257 | 258 | conn$put_item( 259 | TableName = table_name, 260 | Item = put_list, 261 | ReturnValues = "NONE" 262 | ) 263 | 264 | } else { 265 | stop( 266 | glue( 267 | "Table '{table_name}' does not exist!" 268 | ) 269 | ) 270 | } 271 | } 272 | 273 | #' Helper function for `put_table_row` 274 | #' 275 | #' @param table_name Character string. Table name to use 276 | #' @param input_list List. The data to enter into the row 277 | #' @param type_mapping List. A list of mapping for DynamoDb & R classes 278 | #' @param is_update Logical. Whether this is an update or a new row 279 | #' @param conn `paws::dynamodb()` object. Dependent on envvars. 280 | #' 281 | process_table_input_row <- function( 282 | table_name = NULL, 283 | input_list = NULL, 284 | type_mapping = get_type_mapping(), 285 | is_update = FALSE, 286 | conn = dynamodb() 287 | ) { 288 | 289 | schema <- get_table_schema(table_name) 290 | 291 | type_mapping <- setNames( 292 | names(type_mapping), 293 | type_mapping 294 | ) 295 | 296 | data_contract <- lapply( 297 | schema$classes, 298 | function(class) type_mapping[class][[1]] 299 | ) 300 | 301 | if (is_update) { 302 | put_condition <- length(input_list) == length(schema$classes) 303 | } else { 304 | put_condition <- length(input_list) == length(schema$classes) - 1 305 | } 306 | 307 | if (put_condition) { 308 | 309 | put_list <- do.call( 310 | c, 311 | lapply( 312 | names(data_contract), 313 | function(column_name) { 314 | 315 | db_class <- data_contract[[column_name]][[1]] 316 | key_class <- names( 317 | which( 318 | type_mapping == db_class 319 | ) 320 | ) 321 | 322 | # Auto-increment if not updating a row 323 | 324 | if (is_update) { 325 | key_value <- input_list[[ 326 | which(names(data_contract) == column_name) 327 | ]] 328 | } 329 | 330 | if (!is_update) { 331 | adjusted_contract <- data_contract[names(data_contract) != "id"] 332 | if (column_name == "id") { 333 | key_value <- as.numeric( 334 | get_latest_key(table_name, conn = conn) + 1 335 | ) 336 | } else { 337 | key_value <- input_list[[ 338 | which(names(adjusted_contract) == column_name) 339 | ]] 340 | } 341 | } 342 | 343 | list( 344 | setNames( 345 | list( 346 | tryCatch( 347 | as(key_value, key_class), 348 | error = function(e, key_value, db_class) { 349 | stop( 350 | glue( 351 | "Conversion error. {key_value} invalid for {db_class}" 352 | ) 353 | ) 354 | } 355 | ) 356 | ), 357 | db_class 358 | ) 359 | ) 360 | } 361 | ) 362 | ) 363 | names(put_list) <- names(schema$classes) 364 | put_list 365 | } else { 366 | stop( 367 | "Invalid number of values provided. Please double-check the schema." 368 | ) 369 | } 370 | } 371 | 372 | #' Get the latest key for the table 373 | #' 374 | #' @param table_name Character string. Table name to use 375 | #' @param id_column Character. The id column name. Defaults to "id" 376 | #' @param conn `paws::dynamodb()` object. Dependent on envvars. 377 | #' 378 | get_latest_key <- function( 379 | table_name = NULL, 380 | id_column = "id", 381 | conn = dynamodb() 382 | ) { 383 | if (is_valid_table(table_name, conn)) { 384 | get_processed_table_data(table_name, conn = conn) |> 385 | arrange(`id_column`, "desc") |> 386 | tail(1) |> 387 | select(`id_column`) |> 388 | as.integer() 389 | } else { 390 | stop( 391 | glue( 392 | "Table '{table_name}' does not exist!" 393 | ) 394 | ) 395 | } 396 | } 397 | 398 | #' Delete a table row (irreversible) 399 | #' 400 | #' @param table_name Character string. Table name to use 401 | #' @param id_value The value or index of the row. Defaults to NULL 402 | #' @param id_column Character. The id column name. Defaults to "id" 403 | #' @param conn `paws::dynamodb()` object. Dependent on envvars. 404 | #' 405 | #' @export 406 | delete_table_row <- function( 407 | table_name = NULL, 408 | id_value = NULL, 409 | id_column = "id", 410 | conn = dynamodb() 411 | ) { 412 | 413 | assert( 414 | check_string(table_name), 415 | check_numeric(id_value), 416 | check_string(id_column), 417 | combine = "and" 418 | ) 419 | 420 | key <- list() 421 | key[[id_column]] <- list(N = id_value) 422 | 423 | conn$delete_item( 424 | table_name, 425 | Key = key, 426 | ReturnValues = "NONE" 427 | ) 428 | } 429 | -------------------------------------------------------------------------------- /renv.lock: -------------------------------------------------------------------------------- 1 | { 2 | "R": { 3 | "Version": "4.3.2", 4 | "Repositories": [ 5 | { 6 | "Name": "CRAN", 7 | "URL": "https://cloud.r-project.org" 8 | }, 9 | { 10 | "Name": "INDIA", 11 | "URL": "https://cran.icts.res.in" 12 | } 13 | ] 14 | }, 15 | "Packages": { 16 | "R.cache": { 17 | "Package": "R.cache", 18 | "Version": "0.16.0", 19 | "Source": "Repository", 20 | "Repository": "CRAN", 21 | "Requirements": [ 22 | "R", 23 | "R.methodsS3", 24 | "R.oo", 25 | "R.utils", 26 | "digest", 27 | "utils" 28 | ], 29 | "Hash": "fe539ca3f8efb7410c3ae2cf5fe6c0f8" 30 | }, 31 | "R.methodsS3": { 32 | "Package": "R.methodsS3", 33 | "Version": "1.8.2", 34 | "Source": "Repository", 35 | "Repository": "CRAN", 36 | "Requirements": [ 37 | "R", 38 | "utils" 39 | ], 40 | "Hash": "278c286fd6e9e75d0c2e8f731ea445c8" 41 | }, 42 | "R.oo": { 43 | "Package": "R.oo", 44 | "Version": "1.25.0", 45 | "Source": "Repository", 46 | "Repository": "CRAN", 47 | "Requirements": [ 48 | "R", 49 | "R.methodsS3", 50 | "methods", 51 | "utils" 52 | ], 53 | "Hash": "a0900a114f4f0194cf4aa8cd4a700681" 54 | }, 55 | "R.utils": { 56 | "Package": "R.utils", 57 | "Version": "2.12.3", 58 | "Source": "Repository", 59 | "Repository": "RSPM", 60 | "Requirements": [ 61 | "R", 62 | "R.methodsS3", 63 | "R.oo", 64 | "methods", 65 | "tools", 66 | "utils" 67 | ], 68 | "Hash": "3dc2829b790254bfba21e60965787651" 69 | }, 70 | "R6": { 71 | "Package": "R6", 72 | "Version": "2.5.1", 73 | "Source": "Repository", 74 | "Repository": "CRAN", 75 | "Requirements": [ 76 | "R" 77 | ], 78 | "Hash": "470851b6d5d0ac559e9d01bb352b4021" 79 | }, 80 | "Rcpp": { 81 | "Package": "Rcpp", 82 | "Version": "1.0.12", 83 | "Source": "Repository", 84 | "Repository": "CRAN", 85 | "Requirements": [ 86 | "methods", 87 | "utils" 88 | ], 89 | "Hash": "5ea2700d21e038ace58269ecdbeb9ec0" 90 | }, 91 | "askpass": { 92 | "Package": "askpass", 93 | "Version": "1.2.0", 94 | "Source": "Repository", 95 | "Repository": "CRAN", 96 | "Requirements": [ 97 | "sys" 98 | ], 99 | "Hash": "cad6cf7f1d5f6e906700b9d3e718c796" 100 | }, 101 | "backports": { 102 | "Package": "backports", 103 | "Version": "1.4.1", 104 | "Source": "Repository", 105 | "Repository": "CRAN", 106 | "Requirements": [ 107 | "R" 108 | ], 109 | "Hash": "c39fbec8a30d23e721980b8afb31984c" 110 | }, 111 | "base64enc": { 112 | "Package": "base64enc", 113 | "Version": "0.1-3", 114 | "Source": "Repository", 115 | "Repository": "CRAN", 116 | "Requirements": [ 117 | "R" 118 | ], 119 | "Hash": "543776ae6848fde2f48ff3816d0628bc" 120 | }, 121 | "box": { 122 | "Package": "box", 123 | "Version": "1.1.3", 124 | "Source": "Repository", 125 | "Repository": "CRAN", 126 | "Requirements": [ 127 | "R", 128 | "tools" 129 | ], 130 | "Hash": "ce8187a260e8e3abc2294284badc3b76" 131 | }, 132 | "callr": { 133 | "Package": "callr", 134 | "Version": "3.7.3", 135 | "Source": "Repository", 136 | "Repository": "CRAN", 137 | "Requirements": [ 138 | "R", 139 | "R6", 140 | "processx", 141 | "utils" 142 | ], 143 | "Hash": "9b2191ede20fa29828139b9900922e51" 144 | }, 145 | "cellranger": { 146 | "Package": "cellranger", 147 | "Version": "1.1.0", 148 | "Source": "Repository", 149 | "Repository": "CRAN", 150 | "Requirements": [ 151 | "R", 152 | "rematch", 153 | "tibble" 154 | ], 155 | "Hash": "f61dbaec772ccd2e17705c1e872e9e7c" 156 | }, 157 | "checkmate": { 158 | "Package": "checkmate", 159 | "Version": "2.3.1", 160 | "Source": "Repository", 161 | "Repository": "CRAN", 162 | "Requirements": [ 163 | "R", 164 | "backports", 165 | "utils" 166 | ], 167 | "Hash": "c01cab1cb0f9125211a6fc99d540e315" 168 | }, 169 | "cli": { 170 | "Package": "cli", 171 | "Version": "3.6.2", 172 | "Source": "Repository", 173 | "Repository": "CRAN", 174 | "Requirements": [ 175 | "R", 176 | "utils" 177 | ], 178 | "Hash": "1216ac65ac55ec0058a6f75d7ca0fd52" 179 | }, 180 | "codetools": { 181 | "Package": "codetools", 182 | "Version": "0.2-19", 183 | "Source": "Repository", 184 | "Repository": "CRAN", 185 | "Requirements": [ 186 | "R" 187 | ], 188 | "Hash": "c089a619a7fae175d149d89164f8c7d8" 189 | }, 190 | "cpp11": { 191 | "Package": "cpp11", 192 | "Version": "0.4.7", 193 | "Source": "Repository", 194 | "Repository": "CRAN", 195 | "Requirements": [ 196 | "R" 197 | ], 198 | "Hash": "5a295d7d963cc5035284dcdbaf334f4e" 199 | }, 200 | "crayon": { 201 | "Package": "crayon", 202 | "Version": "1.5.2", 203 | "Source": "Repository", 204 | "Repository": "CRAN", 205 | "Requirements": [ 206 | "grDevices", 207 | "methods", 208 | "utils" 209 | ], 210 | "Hash": "e8a1e41acf02548751f45c718d55aa6a" 211 | }, 212 | "curl": { 213 | "Package": "curl", 214 | "Version": "5.2.0", 215 | "Source": "Repository", 216 | "Repository": "CRAN", 217 | "Requirements": [ 218 | "R" 219 | ], 220 | "Hash": "ce88d13c0b10fe88a37d9c59dba2d7f9" 221 | }, 222 | "cyclocomp": { 223 | "Package": "cyclocomp", 224 | "Version": "1.1.1", 225 | "Source": "Repository", 226 | "Repository": "CRAN", 227 | "Requirements": [ 228 | "callr", 229 | "crayon", 230 | "desc", 231 | "remotes", 232 | "withr" 233 | ], 234 | "Hash": "cdc4a473222b0112d4df0bcfbed12d44" 235 | }, 236 | "desc": { 237 | "Package": "desc", 238 | "Version": "1.4.3", 239 | "Source": "Repository", 240 | "Repository": "CRAN", 241 | "Requirements": [ 242 | "R", 243 | "R6", 244 | "cli", 245 | "utils" 246 | ], 247 | "Hash": "99b79fcbd6c4d1ce087f5c5c758b384f" 248 | }, 249 | "digest": { 250 | "Package": "digest", 251 | "Version": "0.6.34", 252 | "Source": "Repository", 253 | "Repository": "CRAN", 254 | "Requirements": [ 255 | "R", 256 | "utils" 257 | ], 258 | "Hash": "7ede2ee9ea8d3edbf1ca84c1e333ad1a" 259 | }, 260 | "dplyr": { 261 | "Package": "dplyr", 262 | "Version": "1.1.4", 263 | "Source": "Repository", 264 | "Repository": "CRAN", 265 | "Requirements": [ 266 | "R", 267 | "R6", 268 | "cli", 269 | "generics", 270 | "glue", 271 | "lifecycle", 272 | "magrittr", 273 | "methods", 274 | "pillar", 275 | "rlang", 276 | "tibble", 277 | "tidyselect", 278 | "utils", 279 | "vctrs" 280 | ], 281 | "Hash": "fedd9d00c2944ff00a0e2696ccf048ec" 282 | }, 283 | "ellipsis": { 284 | "Package": "ellipsis", 285 | "Version": "0.3.2", 286 | "Source": "Repository", 287 | "Repository": "CRAN", 288 | "Requirements": [ 289 | "R", 290 | "rlang" 291 | ], 292 | "Hash": "bb0eec2fe32e88d9e2836c2f73ea2077" 293 | }, 294 | "evaluate": { 295 | "Package": "evaluate", 296 | "Version": "0.23", 297 | "Source": "Repository", 298 | "Repository": "CRAN", 299 | "Requirements": [ 300 | "R", 301 | "methods" 302 | ], 303 | "Hash": "daf4a1246be12c1fa8c7705a0935c1a0" 304 | }, 305 | "fansi": { 306 | "Package": "fansi", 307 | "Version": "1.0.6", 308 | "Source": "Repository", 309 | "Repository": "CRAN", 310 | "Requirements": [ 311 | "R", 312 | "grDevices", 313 | "utils" 314 | ], 315 | "Hash": "962174cf2aeb5b9eea581522286a911f" 316 | }, 317 | "fastmap": { 318 | "Package": "fastmap", 319 | "Version": "1.1.1", 320 | "Source": "Repository", 321 | "Repository": "CRAN", 322 | "Hash": "f7736a18de97dea803bde0a2daaafb27" 323 | }, 324 | "fs": { 325 | "Package": "fs", 326 | "Version": "1.6.3", 327 | "Source": "Repository", 328 | "Repository": "CRAN", 329 | "Requirements": [ 330 | "R", 331 | "methods" 332 | ], 333 | "Hash": "47b5f30c720c23999b913a1a635cf0bb" 334 | }, 335 | "gargle": { 336 | "Package": "gargle", 337 | "Version": "1.5.2", 338 | "Source": "Repository", 339 | "Repository": "CRAN", 340 | "Requirements": [ 341 | "R", 342 | "cli", 343 | "fs", 344 | "glue", 345 | "httr", 346 | "jsonlite", 347 | "lifecycle", 348 | "openssl", 349 | "rappdirs", 350 | "rlang", 351 | "stats", 352 | "utils", 353 | "withr" 354 | ], 355 | "Hash": "fc0b272e5847c58cd5da9b20eedbd026" 356 | }, 357 | "generics": { 358 | "Package": "generics", 359 | "Version": "0.1.3", 360 | "Source": "Repository", 361 | "Repository": "CRAN", 362 | "Requirements": [ 363 | "R", 364 | "methods" 365 | ], 366 | "Hash": "15e9634c0fcd294799e9b2e929ed1b86" 367 | }, 368 | "glue": { 369 | "Package": "glue", 370 | "Version": "1.7.0", 371 | "Source": "Repository", 372 | "Repository": "CRAN", 373 | "Requirements": [ 374 | "R", 375 | "methods" 376 | ], 377 | "Hash": "e0b3a53876554bd45879e596cdb10a52" 378 | }, 379 | "googledrive": { 380 | "Package": "googledrive", 381 | "Version": "2.1.1", 382 | "Source": "Repository", 383 | "Repository": "CRAN", 384 | "Requirements": [ 385 | "R", 386 | "cli", 387 | "gargle", 388 | "glue", 389 | "httr", 390 | "jsonlite", 391 | "lifecycle", 392 | "magrittr", 393 | "pillar", 394 | "purrr", 395 | "rlang", 396 | "tibble", 397 | "utils", 398 | "uuid", 399 | "vctrs", 400 | "withr" 401 | ], 402 | "Hash": "e99641edef03e2a5e87f0a0b1fcc97f4" 403 | }, 404 | "googlesheets4": { 405 | "Package": "googlesheets4", 406 | "Version": "1.1.1", 407 | "Source": "Repository", 408 | "Repository": "CRAN", 409 | "Requirements": [ 410 | "R", 411 | "cellranger", 412 | "cli", 413 | "curl", 414 | "gargle", 415 | "glue", 416 | "googledrive", 417 | "httr", 418 | "ids", 419 | "lifecycle", 420 | "magrittr", 421 | "methods", 422 | "purrr", 423 | "rematch2", 424 | "rlang", 425 | "tibble", 426 | "utils", 427 | "vctrs", 428 | "withr" 429 | ], 430 | "Hash": "d6db1667059d027da730decdc214b959" 431 | }, 432 | "here": { 433 | "Package": "here", 434 | "Version": "1.0.1", 435 | "Source": "Repository", 436 | "Repository": "CRAN", 437 | "Requirements": [ 438 | "rprojroot" 439 | ], 440 | "Hash": "24b224366f9c2e7534d2344d10d59211" 441 | }, 442 | "highr": { 443 | "Package": "highr", 444 | "Version": "0.10", 445 | "Source": "Repository", 446 | "Repository": "CRAN", 447 | "Requirements": [ 448 | "R", 449 | "xfun" 450 | ], 451 | "Hash": "06230136b2d2b9ba5805e1963fa6e890" 452 | }, 453 | "httpuv": { 454 | "Package": "httpuv", 455 | "Version": "1.6.13", 456 | "Source": "Repository", 457 | "Repository": "CRAN", 458 | "Requirements": [ 459 | "R", 460 | "R6", 461 | "Rcpp", 462 | "later", 463 | "promises", 464 | "utils" 465 | ], 466 | "Hash": "d23d2879001f3d82ee9dc38a9ef53c4c" 467 | }, 468 | "httr": { 469 | "Package": "httr", 470 | "Version": "1.4.7", 471 | "Source": "Repository", 472 | "Repository": "CRAN", 473 | "Requirements": [ 474 | "R", 475 | "R6", 476 | "curl", 477 | "jsonlite", 478 | "mime", 479 | "openssl" 480 | ], 481 | "Hash": "ac107251d9d9fd72f0ca8049988f1d7f" 482 | }, 483 | "ids": { 484 | "Package": "ids", 485 | "Version": "1.0.1", 486 | "Source": "Repository", 487 | "Repository": "CRAN", 488 | "Requirements": [ 489 | "openssl", 490 | "uuid" 491 | ], 492 | "Hash": "99df65cfef20e525ed38c3d2577f7190" 493 | }, 494 | "jsonlite": { 495 | "Package": "jsonlite", 496 | "Version": "1.8.8", 497 | "Source": "Repository", 498 | "Repository": "CRAN", 499 | "Requirements": [ 500 | "methods" 501 | ], 502 | "Hash": "e1b9c55281c5adc4dd113652d9e26768" 503 | }, 504 | "knitr": { 505 | "Package": "knitr", 506 | "Version": "1.45", 507 | "Source": "Repository", 508 | "Repository": "CRAN", 509 | "Requirements": [ 510 | "R", 511 | "evaluate", 512 | "highr", 513 | "methods", 514 | "tools", 515 | "xfun", 516 | "yaml" 517 | ], 518 | "Hash": "1ec462871063897135c1bcbe0fc8f07d" 519 | }, 520 | "later": { 521 | "Package": "later", 522 | "Version": "1.3.2", 523 | "Source": "Repository", 524 | "Repository": "CRAN", 525 | "Requirements": [ 526 | "Rcpp", 527 | "rlang" 528 | ], 529 | "Hash": "a3e051d405326b8b0012377434c62b37" 530 | }, 531 | "lazyeval": { 532 | "Package": "lazyeval", 533 | "Version": "0.2.2", 534 | "Source": "Repository", 535 | "Repository": "CRAN", 536 | "Requirements": [ 537 | "R" 538 | ], 539 | "Hash": "d908914ae53b04d4c0c0fd72ecc35370" 540 | }, 541 | "lifecycle": { 542 | "Package": "lifecycle", 543 | "Version": "1.0.4", 544 | "Source": "Repository", 545 | "Repository": "CRAN", 546 | "Requirements": [ 547 | "R", 548 | "cli", 549 | "glue", 550 | "rlang" 551 | ], 552 | "Hash": "b8552d117e1b808b09a832f589b79035" 553 | }, 554 | "lintr": { 555 | "Package": "lintr", 556 | "Version": "3.1.1", 557 | "Source": "Repository", 558 | "Repository": "CRAN", 559 | "Requirements": [ 560 | "R", 561 | "backports", 562 | "codetools", 563 | "cyclocomp", 564 | "digest", 565 | "glue", 566 | "knitr", 567 | "rex", 568 | "stats", 569 | "utils", 570 | "xml2", 571 | "xmlparsedata" 572 | ], 573 | "Hash": "93e9379f4be8c0bf1862dfc7f720193e" 574 | }, 575 | "lubridate": { 576 | "Package": "lubridate", 577 | "Version": "1.9.3", 578 | "Source": "Repository", 579 | "Repository": "CRAN", 580 | "Requirements": [ 581 | "R", 582 | "generics", 583 | "methods", 584 | "timechange" 585 | ], 586 | "Hash": "680ad542fbcf801442c83a6ac5a2126c" 587 | }, 588 | "magrittr": { 589 | "Package": "magrittr", 590 | "Version": "2.0.3", 591 | "Source": "Repository", 592 | "Repository": "CRAN", 593 | "Requirements": [ 594 | "R" 595 | ], 596 | "Hash": "7ce2733a9826b3aeb1775d56fd305472" 597 | }, 598 | "mime": { 599 | "Package": "mime", 600 | "Version": "0.12", 601 | "Source": "Repository", 602 | "Repository": "CRAN", 603 | "Requirements": [ 604 | "tools" 605 | ], 606 | "Hash": "18e9c28c1d3ca1560ce30658b22ce104" 607 | }, 608 | "openssl": { 609 | "Package": "openssl", 610 | "Version": "2.1.1", 611 | "Source": "Repository", 612 | "Repository": "CRAN", 613 | "Requirements": [ 614 | "askpass" 615 | ], 616 | "Hash": "2a0dc8c6adfb6f032e4d4af82d258ab5" 617 | }, 618 | "paws": { 619 | "Package": "paws", 620 | "Version": "0.5.0", 621 | "Source": "Repository", 622 | "Repository": "CRAN", 623 | "Requirements": [ 624 | "paws.analytics", 625 | "paws.application.integration", 626 | "paws.common", 627 | "paws.compute", 628 | "paws.cost.management", 629 | "paws.customer.engagement", 630 | "paws.database", 631 | "paws.developer.tools", 632 | "paws.end.user.computing", 633 | "paws.machine.learning", 634 | "paws.management", 635 | "paws.networking", 636 | "paws.security.identity", 637 | "paws.storage" 638 | ], 639 | "Hash": "373745f3091ac32bd5f70d783d89d6db" 640 | }, 641 | "paws.analytics": { 642 | "Package": "paws.analytics", 643 | "Version": "0.5.0", 644 | "Source": "Repository", 645 | "Repository": "CRAN", 646 | "Requirements": [ 647 | "paws.common" 648 | ], 649 | "Hash": "ce1b08551d30927f07cf980fbcd624b9" 650 | }, 651 | "paws.application.integration": { 652 | "Package": "paws.application.integration", 653 | "Version": "0.5.0", 654 | "Source": "Repository", 655 | "Repository": "CRAN", 656 | "Requirements": [ 657 | "paws.common" 658 | ], 659 | "Hash": "f50fab812dd899553df4313532636028" 660 | }, 661 | "paws.common": { 662 | "Package": "paws.common", 663 | "Version": "0.7.0", 664 | "Source": "Repository", 665 | "Repository": "CRAN", 666 | "Requirements": [ 667 | "Rcpp", 668 | "base64enc", 669 | "curl", 670 | "digest", 671 | "httr", 672 | "jsonlite", 673 | "methods", 674 | "stats", 675 | "utils", 676 | "xml2" 677 | ], 678 | "Hash": "6f3f8f9f757b79f92bb92bc24e6830f6" 679 | }, 680 | "paws.compute": { 681 | "Package": "paws.compute", 682 | "Version": "0.5.0", 683 | "Source": "Repository", 684 | "Repository": "CRAN", 685 | "Requirements": [ 686 | "paws.common" 687 | ], 688 | "Hash": "31c510a7c324db340d7616f111fdbefb" 689 | }, 690 | "paws.cost.management": { 691 | "Package": "paws.cost.management", 692 | "Version": "0.5.0", 693 | "Source": "Repository", 694 | "Repository": "CRAN", 695 | "Requirements": [ 696 | "paws.common" 697 | ], 698 | "Hash": "bac681f234f9001f016aaea8e35fe03c" 699 | }, 700 | "paws.customer.engagement": { 701 | "Package": "paws.customer.engagement", 702 | "Version": "0.5.0", 703 | "Source": "Repository", 704 | "Repository": "CRAN", 705 | "Requirements": [ 706 | "paws.common" 707 | ], 708 | "Hash": "96440cd4b84c6b15a0a56d3c37771d6b" 709 | }, 710 | "paws.database": { 711 | "Package": "paws.database", 712 | "Version": "0.5.0", 713 | "Source": "Repository", 714 | "Repository": "CRAN", 715 | "Requirements": [ 716 | "paws.common" 717 | ], 718 | "Hash": "56926d954574e87f102e6a26f62471dc" 719 | }, 720 | "paws.developer.tools": { 721 | "Package": "paws.developer.tools", 722 | "Version": "0.5.0", 723 | "Source": "Repository", 724 | "Repository": "CRAN", 725 | "Requirements": [ 726 | "paws.common" 727 | ], 728 | "Hash": "9b002332f4656bb0829a47f7451a14b7" 729 | }, 730 | "paws.end.user.computing": { 731 | "Package": "paws.end.user.computing", 732 | "Version": "0.5.0", 733 | "Source": "Repository", 734 | "Repository": "CRAN", 735 | "Requirements": [ 736 | "paws.common" 737 | ], 738 | "Hash": "887c30831c3db80d11938f3d19eb3b0a" 739 | }, 740 | "paws.machine.learning": { 741 | "Package": "paws.machine.learning", 742 | "Version": "0.5.0", 743 | "Source": "Repository", 744 | "Repository": "CRAN", 745 | "Requirements": [ 746 | "paws.common" 747 | ], 748 | "Hash": "2f003f926ece4d969c6120c1782072e9" 749 | }, 750 | "paws.management": { 751 | "Package": "paws.management", 752 | "Version": "0.5.0", 753 | "Source": "Repository", 754 | "Repository": "CRAN", 755 | "Requirements": [ 756 | "paws.common" 757 | ], 758 | "Hash": "351811e503d344c0cb2162d5d0fd717e" 759 | }, 760 | "paws.networking": { 761 | "Package": "paws.networking", 762 | "Version": "0.5.0", 763 | "Source": "Repository", 764 | "Repository": "CRAN", 765 | "Requirements": [ 766 | "paws.common" 767 | ], 768 | "Hash": "53d730871228a41085d56c223eaabbcc" 769 | }, 770 | "paws.security.identity": { 771 | "Package": "paws.security.identity", 772 | "Version": "0.5.0", 773 | "Source": "Repository", 774 | "Repository": "CRAN", 775 | "Requirements": [ 776 | "paws.common" 777 | ], 778 | "Hash": "a7ed398f6e8d4153ab19626a71a49cfa" 779 | }, 780 | "paws.storage": { 781 | "Package": "paws.storage", 782 | "Version": "0.5.0", 783 | "Source": "Repository", 784 | "Repository": "CRAN", 785 | "Requirements": [ 786 | "paws.common" 787 | ], 788 | "Hash": "e4a5655c4172112449f7f501c60ed671" 789 | }, 790 | "pillar": { 791 | "Package": "pillar", 792 | "Version": "1.9.0", 793 | "Source": "Repository", 794 | "Repository": "CRAN", 795 | "Requirements": [ 796 | "cli", 797 | "fansi", 798 | "glue", 799 | "lifecycle", 800 | "rlang", 801 | "utf8", 802 | "utils", 803 | "vctrs" 804 | ], 805 | "Hash": "15da5a8412f317beeee6175fbc76f4bb" 806 | }, 807 | "pkgconfig": { 808 | "Package": "pkgconfig", 809 | "Version": "2.0.3", 810 | "Source": "Repository", 811 | "Repository": "CRAN", 812 | "Requirements": [ 813 | "utils" 814 | ], 815 | "Hash": "01f28d4278f15c76cddbea05899c5d6f" 816 | }, 817 | "plumber": { 818 | "Package": "plumber", 819 | "Version": "1.2.1", 820 | "Source": "Repository", 821 | "Repository": "CRAN", 822 | "Requirements": [ 823 | "R", 824 | "R6", 825 | "crayon", 826 | "ellipsis", 827 | "httpuv", 828 | "jsonlite", 829 | "lifecycle", 830 | "magrittr", 831 | "mime", 832 | "promises", 833 | "rlang", 834 | "sodium", 835 | "stringi", 836 | "swagger", 837 | "webutils" 838 | ], 839 | "Hash": "8b65a7a00ef8edc5ddc6fabf0aff1194" 840 | }, 841 | "processx": { 842 | "Package": "processx", 843 | "Version": "3.8.3", 844 | "Source": "Repository", 845 | "Repository": "CRAN", 846 | "Requirements": [ 847 | "R", 848 | "R6", 849 | "ps", 850 | "utils" 851 | ], 852 | "Hash": "82d48b1aec56084d9438dbf98087a7e9" 853 | }, 854 | "promises": { 855 | "Package": "promises", 856 | "Version": "1.2.1", 857 | "Source": "Repository", 858 | "Repository": "CRAN", 859 | "Requirements": [ 860 | "R6", 861 | "Rcpp", 862 | "fastmap", 863 | "later", 864 | "magrittr", 865 | "rlang", 866 | "stats" 867 | ], 868 | "Hash": "0d8a15c9d000970ada1ab21405387dee" 869 | }, 870 | "ps": { 871 | "Package": "ps", 872 | "Version": "1.7.6", 873 | "Source": "Repository", 874 | "Repository": "CRAN", 875 | "Requirements": [ 876 | "R", 877 | "utils" 878 | ], 879 | "Hash": "dd2b9319ee0656c8acf45c7f40c59de7" 880 | }, 881 | "purrr": { 882 | "Package": "purrr", 883 | "Version": "1.0.2", 884 | "Source": "Repository", 885 | "Repository": "CRAN", 886 | "Requirements": [ 887 | "R", 888 | "cli", 889 | "lifecycle", 890 | "magrittr", 891 | "rlang", 892 | "vctrs" 893 | ], 894 | "Hash": "1cba04a4e9414bdefc9dcaa99649a8dc" 895 | }, 896 | "rappdirs": { 897 | "Package": "rappdirs", 898 | "Version": "0.3.3", 899 | "Source": "Repository", 900 | "Repository": "CRAN", 901 | "Requirements": [ 902 | "R" 903 | ], 904 | "Hash": "5e3c5dc0b071b21fa128676560dbe94d" 905 | }, 906 | "rematch": { 907 | "Package": "rematch", 908 | "Version": "2.0.0", 909 | "Source": "Repository", 910 | "Repository": "CRAN", 911 | "Hash": "cbff1b666c6fa6d21202f07e2318d4f1" 912 | }, 913 | "rematch2": { 914 | "Package": "rematch2", 915 | "Version": "2.1.2", 916 | "Source": "Repository", 917 | "Repository": "CRAN", 918 | "Requirements": [ 919 | "tibble" 920 | ], 921 | "Hash": "76c9e04c712a05848ae7a23d2f170a40" 922 | }, 923 | "remotes": { 924 | "Package": "remotes", 925 | "Version": "2.4.2.1", 926 | "Source": "Repository", 927 | "Repository": "CRAN", 928 | "Requirements": [ 929 | "R", 930 | "methods", 931 | "stats", 932 | "tools", 933 | "utils" 934 | ], 935 | "Hash": "63d15047eb239f95160112bcadc4fcb9" 936 | }, 937 | "renv": { 938 | "Package": "renv", 939 | "Version": "1.0.3", 940 | "Source": "Repository", 941 | "Repository": "CRAN", 942 | "Requirements": [ 943 | "utils" 944 | ], 945 | "Hash": "41b847654f567341725473431dd0d5ab" 946 | }, 947 | "rex": { 948 | "Package": "rex", 949 | "Version": "1.2.1", 950 | "Source": "Repository", 951 | "Repository": "CRAN", 952 | "Requirements": [ 953 | "lazyeval" 954 | ], 955 | "Hash": "ae34cd56890607370665bee5bd17812f" 956 | }, 957 | "rlang": { 958 | "Package": "rlang", 959 | "Version": "1.1.3", 960 | "Source": "Repository", 961 | "Repository": "CRAN", 962 | "Requirements": [ 963 | "R", 964 | "utils" 965 | ], 966 | "Hash": "42548638fae05fd9a9b5f3f437fbbbe2" 967 | }, 968 | "rprojroot": { 969 | "Package": "rprojroot", 970 | "Version": "2.0.4", 971 | "Source": "Repository", 972 | "Repository": "CRAN", 973 | "Requirements": [ 974 | "R" 975 | ], 976 | "Hash": "4c8415e0ec1e29f3f4f6fc108bef0144" 977 | }, 978 | "rvest": { 979 | "Package": "rvest", 980 | "Version": "1.0.3", 981 | "Source": "Repository", 982 | "Repository": "CRAN", 983 | "Requirements": [ 984 | "R", 985 | "cli", 986 | "glue", 987 | "httr", 988 | "lifecycle", 989 | "magrittr", 990 | "rlang", 991 | "selectr", 992 | "tibble", 993 | "withr", 994 | "xml2" 995 | ], 996 | "Hash": "a4a5ac819a467808c60e36e92ddf195e" 997 | }, 998 | "selectr": { 999 | "Package": "selectr", 1000 | "Version": "0.4-2", 1001 | "Source": "Repository", 1002 | "Repository": "CRAN", 1003 | "Requirements": [ 1004 | "R", 1005 | "R6", 1006 | "methods", 1007 | "stringr" 1008 | ], 1009 | "Hash": "3838071b66e0c566d55cc26bd6e27bf4" 1010 | }, 1011 | "sodium": { 1012 | "Package": "sodium", 1013 | "Version": "1.3.1", 1014 | "Source": "Repository", 1015 | "Repository": "CRAN", 1016 | "Hash": "dd86d6fd2a01d4eb3777dfdee7076d56" 1017 | }, 1018 | "stringi": { 1019 | "Package": "stringi", 1020 | "Version": "1.8.3", 1021 | "Source": "Repository", 1022 | "Repository": "CRAN", 1023 | "Requirements": [ 1024 | "R", 1025 | "stats", 1026 | "tools", 1027 | "utils" 1028 | ], 1029 | "Hash": "058aebddea264f4c99401515182e656a" 1030 | }, 1031 | "stringr": { 1032 | "Package": "stringr", 1033 | "Version": "1.5.1", 1034 | "Source": "Repository", 1035 | "Repository": "CRAN", 1036 | "Requirements": [ 1037 | "R", 1038 | "cli", 1039 | "glue", 1040 | "lifecycle", 1041 | "magrittr", 1042 | "rlang", 1043 | "stringi", 1044 | "vctrs" 1045 | ], 1046 | "Hash": "960e2ae9e09656611e0b8214ad543207" 1047 | }, 1048 | "styler": { 1049 | "Package": "styler", 1050 | "Version": "1.10.2", 1051 | "Source": "Repository", 1052 | "Repository": "CRAN", 1053 | "Requirements": [ 1054 | "R", 1055 | "R.cache", 1056 | "cli", 1057 | "magrittr", 1058 | "purrr", 1059 | "rlang", 1060 | "rprojroot", 1061 | "tools", 1062 | "vctrs", 1063 | "withr" 1064 | ], 1065 | "Hash": "d61238fd44fc63c8adf4565efe8eb682" 1066 | }, 1067 | "swagger": { 1068 | "Package": "swagger", 1069 | "Version": "3.33.1", 1070 | "Source": "Repository", 1071 | "Repository": "CRAN", 1072 | "Hash": "f28d25ed70c903922254157c11b0081d" 1073 | }, 1074 | "sys": { 1075 | "Package": "sys", 1076 | "Version": "3.4.2", 1077 | "Source": "Repository", 1078 | "Repository": "CRAN", 1079 | "Hash": "3a1be13d68d47a8cd0bfd74739ca1555" 1080 | }, 1081 | "tibble": { 1082 | "Package": "tibble", 1083 | "Version": "3.2.1", 1084 | "Source": "Repository", 1085 | "Repository": "CRAN", 1086 | "Requirements": [ 1087 | "R", 1088 | "fansi", 1089 | "lifecycle", 1090 | "magrittr", 1091 | "methods", 1092 | "pillar", 1093 | "pkgconfig", 1094 | "rlang", 1095 | "utils", 1096 | "vctrs" 1097 | ], 1098 | "Hash": "a84e2cc86d07289b3b6f5069df7a004c" 1099 | }, 1100 | "tidyr": { 1101 | "Package": "tidyr", 1102 | "Version": "1.3.1", 1103 | "Source": "Repository", 1104 | "Repository": "CRAN", 1105 | "Requirements": [ 1106 | "R", 1107 | "cli", 1108 | "cpp11", 1109 | "dplyr", 1110 | "glue", 1111 | "lifecycle", 1112 | "magrittr", 1113 | "purrr", 1114 | "rlang", 1115 | "stringr", 1116 | "tibble", 1117 | "tidyselect", 1118 | "utils", 1119 | "vctrs" 1120 | ], 1121 | "Hash": "915fb7ce036c22a6a33b5a8adb712eb1" 1122 | }, 1123 | "tidyselect": { 1124 | "Package": "tidyselect", 1125 | "Version": "1.2.0", 1126 | "Source": "Repository", 1127 | "Repository": "CRAN", 1128 | "Requirements": [ 1129 | "R", 1130 | "cli", 1131 | "glue", 1132 | "lifecycle", 1133 | "rlang", 1134 | "vctrs", 1135 | "withr" 1136 | ], 1137 | "Hash": "79540e5fcd9e0435af547d885f184fd5" 1138 | }, 1139 | "timechange": { 1140 | "Package": "timechange", 1141 | "Version": "0.3.0", 1142 | "Source": "Repository", 1143 | "Repository": "CRAN", 1144 | "Requirements": [ 1145 | "R", 1146 | "cpp11" 1147 | ], 1148 | "Hash": "c5f3c201b931cd6474d17d8700ccb1c8" 1149 | }, 1150 | "utf8": { 1151 | "Package": "utf8", 1152 | "Version": "1.2.4", 1153 | "Source": "Repository", 1154 | "Repository": "CRAN", 1155 | "Requirements": [ 1156 | "R" 1157 | ], 1158 | "Hash": "62b65c52671e6665f803ff02954446e9" 1159 | }, 1160 | "uuid": { 1161 | "Package": "uuid", 1162 | "Version": "1.2-0", 1163 | "Source": "Repository", 1164 | "Repository": "CRAN", 1165 | "Requirements": [ 1166 | "R" 1167 | ], 1168 | "Hash": "303c19bfd970bece872f93a824e323d9" 1169 | }, 1170 | "vctrs": { 1171 | "Package": "vctrs", 1172 | "Version": "0.6.5", 1173 | "Source": "Repository", 1174 | "Repository": "RSPM", 1175 | "Requirements": [ 1176 | "R", 1177 | "cli", 1178 | "glue", 1179 | "lifecycle", 1180 | "rlang" 1181 | ], 1182 | "Hash": "c03fa420630029418f7e6da3667aac4a" 1183 | }, 1184 | "webutils": { 1185 | "Package": "webutils", 1186 | "Version": "1.2.0", 1187 | "Source": "Repository", 1188 | "Repository": "CRAN", 1189 | "Requirements": [ 1190 | "curl", 1191 | "jsonlite" 1192 | ], 1193 | "Hash": "6a7f2a3084c7249d2f1466d6e4cc2e84" 1194 | }, 1195 | "withr": { 1196 | "Package": "withr", 1197 | "Version": "3.0.0", 1198 | "Source": "Repository", 1199 | "Repository": "CRAN", 1200 | "Requirements": [ 1201 | "R", 1202 | "grDevices", 1203 | "graphics" 1204 | ], 1205 | "Hash": "d31b6c62c10dcf11ec530ca6b0dd5d35" 1206 | }, 1207 | "xfun": { 1208 | "Package": "xfun", 1209 | "Version": "0.41", 1210 | "Source": "Repository", 1211 | "Repository": "CRAN", 1212 | "Requirements": [ 1213 | "stats", 1214 | "tools" 1215 | ], 1216 | "Hash": "460a5e0fe46a80ef87424ad216028014" 1217 | }, 1218 | "xml2": { 1219 | "Package": "xml2", 1220 | "Version": "1.3.6", 1221 | "Source": "Repository", 1222 | "Repository": "CRAN", 1223 | "Requirements": [ 1224 | "R", 1225 | "cli", 1226 | "methods", 1227 | "rlang" 1228 | ], 1229 | "Hash": "1d0336142f4cd25d8d23cd3ba7a8fb61" 1230 | }, 1231 | "xmlparsedata": { 1232 | "Package": "xmlparsedata", 1233 | "Version": "1.0.5", 1234 | "Source": "Repository", 1235 | "Repository": "CRAN", 1236 | "Requirements": [ 1237 | "R" 1238 | ], 1239 | "Hash": "45e4bf3c46476896e821fc0a408fb4fc" 1240 | }, 1241 | "yaml": { 1242 | "Package": "yaml", 1243 | "Version": "2.3.8", 1244 | "Source": "Repository", 1245 | "Repository": "CRAN", 1246 | "Hash": "29240487a071f535f5e5d5a323b7afbd" 1247 | } 1248 | } 1249 | } 1250 | -------------------------------------------------------------------------------- /renv/activate.R: -------------------------------------------------------------------------------- 1 | 2 | local({ 3 | 4 | # the requested version of renv 5 | version <- "1.0.3" 6 | attr(version, "sha") <- NULL 7 | 8 | # the project directory 9 | project <- getwd() 10 | 11 | # use start-up diagnostics if enabled 12 | diagnostics <- Sys.getenv("RENV_STARTUP_DIAGNOSTICS", unset = "FALSE") 13 | if (diagnostics) { 14 | start <- Sys.time() 15 | profile <- tempfile("renv-startup-", fileext = ".Rprof") 16 | utils::Rprof(profile) 17 | on.exit({ 18 | utils::Rprof(NULL) 19 | elapsed <- signif(difftime(Sys.time(), start, units = "auto"), digits = 2L) 20 | writeLines(sprintf("- renv took %s to run the autoloader.", format(elapsed))) 21 | writeLines(sprintf("- Profile: %s", profile)) 22 | print(utils::summaryRprof(profile)) 23 | }, add = TRUE) 24 | } 25 | 26 | # figure out whether the autoloader is enabled 27 | enabled <- local({ 28 | 29 | # first, check config option 30 | override <- getOption("renv.config.autoloader.enabled") 31 | if (!is.null(override)) 32 | return(override) 33 | 34 | # next, check environment variables 35 | # TODO: prefer using the configuration one in the future 36 | envvars <- c( 37 | "RENV_CONFIG_AUTOLOADER_ENABLED", 38 | "RENV_AUTOLOADER_ENABLED", 39 | "RENV_ACTIVATE_PROJECT" 40 | ) 41 | 42 | for (envvar in envvars) { 43 | envval <- Sys.getenv(envvar, unset = NA) 44 | if (!is.na(envval)) 45 | return(tolower(envval) %in% c("true", "t", "1")) 46 | } 47 | 48 | # enable by default 49 | TRUE 50 | 51 | }) 52 | 53 | if (!enabled) 54 | return(FALSE) 55 | 56 | # avoid recursion 57 | if (identical(getOption("renv.autoloader.running"), TRUE)) { 58 | warning("ignoring recursive attempt to run renv autoloader") 59 | return(invisible(TRUE)) 60 | } 61 | 62 | # signal that we're loading renv during R startup 63 | options(renv.autoloader.running = TRUE) 64 | on.exit(options(renv.autoloader.running = NULL), add = TRUE) 65 | 66 | # signal that we've consented to use renv 67 | options(renv.consent = TRUE) 68 | 69 | # load the 'utils' package eagerly -- this ensures that renv shims, which 70 | # mask 'utils' packages, will come first on the search path 71 | library(utils, lib.loc = .Library) 72 | 73 | # unload renv if it's already been loaded 74 | if ("renv" %in% loadedNamespaces()) 75 | unloadNamespace("renv") 76 | 77 | # load bootstrap tools 78 | `%||%` <- function(x, y) { 79 | if (is.null(x)) y else x 80 | } 81 | 82 | catf <- function(fmt, ..., appendLF = TRUE) { 83 | 84 | quiet <- getOption("renv.bootstrap.quiet", default = FALSE) 85 | if (quiet) 86 | return(invisible()) 87 | 88 | msg <- sprintf(fmt, ...) 89 | cat(msg, file = stdout(), sep = if (appendLF) "\n" else "") 90 | 91 | invisible(msg) 92 | 93 | } 94 | 95 | header <- function(label, 96 | ..., 97 | prefix = "#", 98 | suffix = "-", 99 | n = min(getOption("width"), 78)) 100 | { 101 | label <- sprintf(label, ...) 102 | n <- max(n - nchar(label) - nchar(prefix) - 2L, 8L) 103 | if (n <= 0) 104 | return(paste(prefix, label)) 105 | 106 | tail <- paste(rep.int(suffix, n), collapse = "") 107 | paste0(prefix, " ", label, " ", tail) 108 | 109 | } 110 | 111 | startswith <- function(string, prefix) { 112 | substring(string, 1, nchar(prefix)) == prefix 113 | } 114 | 115 | bootstrap <- function(version, library) { 116 | 117 | friendly <- renv_bootstrap_version_friendly(version) 118 | section <- header(sprintf("Bootstrapping renv %s", friendly)) 119 | catf(section) 120 | 121 | # attempt to download renv 122 | catf("- Downloading renv ... ", appendLF = FALSE) 123 | withCallingHandlers( 124 | tarball <- renv_bootstrap_download(version), 125 | error = function(err) { 126 | catf("FAILED") 127 | stop("failed to download:\n", conditionMessage(err)) 128 | } 129 | ) 130 | catf("OK") 131 | on.exit(unlink(tarball), add = TRUE) 132 | 133 | # now attempt to install 134 | catf("- Installing renv ... ", appendLF = FALSE) 135 | withCallingHandlers( 136 | status <- renv_bootstrap_install(version, tarball, library), 137 | error = function(err) { 138 | catf("FAILED") 139 | stop("failed to install:\n", conditionMessage(err)) 140 | } 141 | ) 142 | catf("OK") 143 | 144 | # add empty line to break up bootstrapping from normal output 145 | catf("") 146 | 147 | return(invisible()) 148 | } 149 | 150 | renv_bootstrap_tests_running <- function() { 151 | getOption("renv.tests.running", default = FALSE) 152 | } 153 | 154 | renv_bootstrap_repos <- function() { 155 | 156 | # get CRAN repository 157 | cran <- getOption("renv.repos.cran", "https://cloud.r-project.org") 158 | 159 | # check for repos override 160 | repos <- Sys.getenv("RENV_CONFIG_REPOS_OVERRIDE", unset = NA) 161 | if (!is.na(repos)) { 162 | 163 | # check for RSPM; if set, use a fallback repository for renv 164 | rspm <- Sys.getenv("RSPM", unset = NA) 165 | if (identical(rspm, repos)) 166 | repos <- c(RSPM = rspm, CRAN = cran) 167 | 168 | return(repos) 169 | 170 | } 171 | 172 | # check for lockfile repositories 173 | repos <- tryCatch(renv_bootstrap_repos_lockfile(), error = identity) 174 | if (!inherits(repos, "error") && length(repos)) 175 | return(repos) 176 | 177 | # retrieve current repos 178 | repos <- getOption("repos") 179 | 180 | # ensure @CRAN@ entries are resolved 181 | repos[repos == "@CRAN@"] <- cran 182 | 183 | # add in renv.bootstrap.repos if set 184 | default <- c(FALLBACK = "https://cloud.r-project.org") 185 | extra <- getOption("renv.bootstrap.repos", default = default) 186 | repos <- c(repos, extra) 187 | 188 | # remove duplicates that might've snuck in 189 | dupes <- duplicated(repos) | duplicated(names(repos)) 190 | repos[!dupes] 191 | 192 | } 193 | 194 | renv_bootstrap_repos_lockfile <- function() { 195 | 196 | lockpath <- Sys.getenv("RENV_PATHS_LOCKFILE", unset = "renv.lock") 197 | if (!file.exists(lockpath)) 198 | return(NULL) 199 | 200 | lockfile <- tryCatch(renv_json_read(lockpath), error = identity) 201 | if (inherits(lockfile, "error")) { 202 | warning(lockfile) 203 | return(NULL) 204 | } 205 | 206 | repos <- lockfile$R$Repositories 207 | if (length(repos) == 0) 208 | return(NULL) 209 | 210 | keys <- vapply(repos, `[[`, "Name", FUN.VALUE = character(1)) 211 | vals <- vapply(repos, `[[`, "URL", FUN.VALUE = character(1)) 212 | names(vals) <- keys 213 | 214 | return(vals) 215 | 216 | } 217 | 218 | renv_bootstrap_download <- function(version) { 219 | 220 | sha <- attr(version, "sha", exact = TRUE) 221 | 222 | methods <- if (!is.null(sha)) { 223 | 224 | # attempting to bootstrap a development version of renv 225 | c( 226 | function() renv_bootstrap_download_tarball(sha), 227 | function() renv_bootstrap_download_github(sha) 228 | ) 229 | 230 | } else { 231 | 232 | # attempting to bootstrap a release version of renv 233 | c( 234 | function() renv_bootstrap_download_tarball(version), 235 | function() renv_bootstrap_download_cran_latest(version), 236 | function() renv_bootstrap_download_cran_archive(version) 237 | ) 238 | 239 | } 240 | 241 | for (method in methods) { 242 | path <- tryCatch(method(), error = identity) 243 | if (is.character(path) && file.exists(path)) 244 | return(path) 245 | } 246 | 247 | stop("All download methods failed") 248 | 249 | } 250 | 251 | renv_bootstrap_download_impl <- function(url, destfile) { 252 | 253 | mode <- "wb" 254 | 255 | # https://bugs.r-project.org/bugzilla/show_bug.cgi?id=17715 256 | fixup <- 257 | Sys.info()[["sysname"]] == "Windows" && 258 | substring(url, 1L, 5L) == "file:" 259 | 260 | if (fixup) 261 | mode <- "w+b" 262 | 263 | args <- list( 264 | url = url, 265 | destfile = destfile, 266 | mode = mode, 267 | quiet = TRUE 268 | ) 269 | 270 | if ("headers" %in% names(formals(utils::download.file))) 271 | args$headers <- renv_bootstrap_download_custom_headers(url) 272 | 273 | do.call(utils::download.file, args) 274 | 275 | } 276 | 277 | renv_bootstrap_download_custom_headers <- function(url) { 278 | 279 | headers <- getOption("renv.download.headers") 280 | if (is.null(headers)) 281 | return(character()) 282 | 283 | if (!is.function(headers)) 284 | stopf("'renv.download.headers' is not a function") 285 | 286 | headers <- headers(url) 287 | if (length(headers) == 0L) 288 | return(character()) 289 | 290 | if (is.list(headers)) 291 | headers <- unlist(headers, recursive = FALSE, use.names = TRUE) 292 | 293 | ok <- 294 | is.character(headers) && 295 | is.character(names(headers)) && 296 | all(nzchar(names(headers))) 297 | 298 | if (!ok) 299 | stop("invocation of 'renv.download.headers' did not return a named character vector") 300 | 301 | headers 302 | 303 | } 304 | 305 | renv_bootstrap_download_cran_latest <- function(version) { 306 | 307 | spec <- renv_bootstrap_download_cran_latest_find(version) 308 | type <- spec$type 309 | repos <- spec$repos 310 | 311 | baseurl <- utils::contrib.url(repos = repos, type = type) 312 | ext <- if (identical(type, "source")) 313 | ".tar.gz" 314 | else if (Sys.info()[["sysname"]] == "Windows") 315 | ".zip" 316 | else 317 | ".tgz" 318 | name <- sprintf("renv_%s%s", version, ext) 319 | url <- paste(baseurl, name, sep = "/") 320 | 321 | destfile <- file.path(tempdir(), name) 322 | status <- tryCatch( 323 | renv_bootstrap_download_impl(url, destfile), 324 | condition = identity 325 | ) 326 | 327 | if (inherits(status, "condition")) 328 | return(FALSE) 329 | 330 | # report success and return 331 | destfile 332 | 333 | } 334 | 335 | renv_bootstrap_download_cran_latest_find <- function(version) { 336 | 337 | # check whether binaries are supported on this system 338 | binary <- 339 | getOption("renv.bootstrap.binary", default = TRUE) && 340 | !identical(.Platform$pkgType, "source") && 341 | !identical(getOption("pkgType"), "source") && 342 | Sys.info()[["sysname"]] %in% c("Darwin", "Windows") 343 | 344 | types <- c(if (binary) "binary", "source") 345 | 346 | # iterate over types + repositories 347 | for (type in types) { 348 | for (repos in renv_bootstrap_repos()) { 349 | 350 | # retrieve package database 351 | db <- tryCatch( 352 | as.data.frame( 353 | utils::available.packages(type = type, repos = repos), 354 | stringsAsFactors = FALSE 355 | ), 356 | error = identity 357 | ) 358 | 359 | if (inherits(db, "error")) 360 | next 361 | 362 | # check for compatible entry 363 | entry <- db[db$Package %in% "renv" & db$Version %in% version, ] 364 | if (nrow(entry) == 0) 365 | next 366 | 367 | # found it; return spec to caller 368 | spec <- list(entry = entry, type = type, repos = repos) 369 | return(spec) 370 | 371 | } 372 | } 373 | 374 | # if we got here, we failed to find renv 375 | fmt <- "renv %s is not available from your declared package repositories" 376 | stop(sprintf(fmt, version)) 377 | 378 | } 379 | 380 | renv_bootstrap_download_cran_archive <- function(version) { 381 | 382 | name <- sprintf("renv_%s.tar.gz", version) 383 | repos <- renv_bootstrap_repos() 384 | urls <- file.path(repos, "src/contrib/Archive/renv", name) 385 | destfile <- file.path(tempdir(), name) 386 | 387 | for (url in urls) { 388 | 389 | status <- tryCatch( 390 | renv_bootstrap_download_impl(url, destfile), 391 | condition = identity 392 | ) 393 | 394 | if (identical(status, 0L)) 395 | return(destfile) 396 | 397 | } 398 | 399 | return(FALSE) 400 | 401 | } 402 | 403 | renv_bootstrap_download_tarball <- function(version) { 404 | 405 | # if the user has provided the path to a tarball via 406 | # an environment variable, then use it 407 | tarball <- Sys.getenv("RENV_BOOTSTRAP_TARBALL", unset = NA) 408 | if (is.na(tarball)) 409 | return() 410 | 411 | # allow directories 412 | if (dir.exists(tarball)) { 413 | name <- sprintf("renv_%s.tar.gz", version) 414 | tarball <- file.path(tarball, name) 415 | } 416 | 417 | # bail if it doesn't exist 418 | if (!file.exists(tarball)) { 419 | 420 | # let the user know we weren't able to honour their request 421 | fmt <- "- RENV_BOOTSTRAP_TARBALL is set (%s) but does not exist." 422 | msg <- sprintf(fmt, tarball) 423 | warning(msg) 424 | 425 | # bail 426 | return() 427 | 428 | } 429 | 430 | catf("- Using local tarball '%s'.", tarball) 431 | tarball 432 | 433 | } 434 | 435 | renv_bootstrap_download_github <- function(version) { 436 | 437 | enabled <- Sys.getenv("RENV_BOOTSTRAP_FROM_GITHUB", unset = "TRUE") 438 | if (!identical(enabled, "TRUE")) 439 | return(FALSE) 440 | 441 | # prepare download options 442 | pat <- Sys.getenv("GITHUB_PAT") 443 | if (nzchar(Sys.which("curl")) && nzchar(pat)) { 444 | fmt <- "--location --fail --header \"Authorization: token %s\"" 445 | extra <- sprintf(fmt, pat) 446 | saved <- options("download.file.method", "download.file.extra") 447 | options(download.file.method = "curl", download.file.extra = extra) 448 | on.exit(do.call(base::options, saved), add = TRUE) 449 | } else if (nzchar(Sys.which("wget")) && nzchar(pat)) { 450 | fmt <- "--header=\"Authorization: token %s\"" 451 | extra <- sprintf(fmt, pat) 452 | saved <- options("download.file.method", "download.file.extra") 453 | options(download.file.method = "wget", download.file.extra = extra) 454 | on.exit(do.call(base::options, saved), add = TRUE) 455 | } 456 | 457 | url <- file.path("https://api.github.com/repos/rstudio/renv/tarball", version) 458 | name <- sprintf("renv_%s.tar.gz", version) 459 | destfile <- file.path(tempdir(), name) 460 | 461 | status <- tryCatch( 462 | renv_bootstrap_download_impl(url, destfile), 463 | condition = identity 464 | ) 465 | 466 | if (!identical(status, 0L)) 467 | return(FALSE) 468 | 469 | renv_bootstrap_download_augment(destfile) 470 | 471 | return(destfile) 472 | 473 | } 474 | 475 | # Add Sha to DESCRIPTION. This is stop gap until #890, after which we 476 | # can use renv::install() to fully capture metadata. 477 | renv_bootstrap_download_augment <- function(destfile) { 478 | sha <- renv_bootstrap_git_extract_sha1_tar(destfile) 479 | if (is.null(sha)) { 480 | return() 481 | } 482 | 483 | # Untar 484 | tempdir <- tempfile("renv-github-") 485 | on.exit(unlink(tempdir, recursive = TRUE), add = TRUE) 486 | untar(destfile, exdir = tempdir) 487 | pkgdir <- dir(tempdir, full.names = TRUE)[[1]] 488 | 489 | # Modify description 490 | desc_path <- file.path(pkgdir, "DESCRIPTION") 491 | desc_lines <- readLines(desc_path) 492 | remotes_fields <- c( 493 | "RemoteType: github", 494 | "RemoteHost: api.github.com", 495 | "RemoteRepo: renv", 496 | "RemoteUsername: rstudio", 497 | "RemotePkgRef: rstudio/renv", 498 | paste("RemoteRef: ", sha), 499 | paste("RemoteSha: ", sha) 500 | ) 501 | writeLines(c(desc_lines[desc_lines != ""], remotes_fields), con = desc_path) 502 | 503 | # Re-tar 504 | local({ 505 | old <- setwd(tempdir) 506 | on.exit(setwd(old), add = TRUE) 507 | 508 | tar(destfile, compression = "gzip") 509 | }) 510 | invisible() 511 | } 512 | 513 | # Extract the commit hash from a git archive. Git archives include the SHA1 514 | # hash as the comment field of the tarball pax extended header 515 | # (see https://www.kernel.org/pub/software/scm/git/docs/git-archive.html) 516 | # For GitHub archives this should be the first header after the default one 517 | # (512 byte) header. 518 | renv_bootstrap_git_extract_sha1_tar <- function(bundle) { 519 | 520 | # open the bundle for reading 521 | # We use gzcon for everything because (from ?gzcon) 522 | # > Reading from a connection which does not supply a 'gzip' magic 523 | # > header is equivalent to reading from the original connection 524 | conn <- gzcon(file(bundle, open = "rb", raw = TRUE)) 525 | on.exit(close(conn)) 526 | 527 | # The default pax header is 512 bytes long and the first pax extended header 528 | # with the comment should be 51 bytes long 529 | # `52 comment=` (11 chars) + 40 byte SHA1 hash 530 | len <- 0x200 + 0x33 531 | res <- rawToChar(readBin(conn, "raw", n = len)[0x201:len]) 532 | 533 | if (grepl("^52 comment=", res)) { 534 | sub("52 comment=", "", res) 535 | } else { 536 | NULL 537 | } 538 | } 539 | 540 | renv_bootstrap_install <- function(version, tarball, library) { 541 | 542 | # attempt to install it into project library 543 | dir.create(library, showWarnings = FALSE, recursive = TRUE) 544 | output <- renv_bootstrap_install_impl(library, tarball) 545 | 546 | # check for successful install 547 | status <- attr(output, "status") 548 | if (is.null(status) || identical(status, 0L)) 549 | return(status) 550 | 551 | # an error occurred; report it 552 | header <- "installation of renv failed" 553 | lines <- paste(rep.int("=", nchar(header)), collapse = "") 554 | text <- paste(c(header, lines, output), collapse = "\n") 555 | stop(text) 556 | 557 | } 558 | 559 | renv_bootstrap_install_impl <- function(library, tarball) { 560 | 561 | # invoke using system2 so we can capture and report output 562 | bin <- R.home("bin") 563 | exe <- if (Sys.info()[["sysname"]] == "Windows") "R.exe" else "R" 564 | R <- file.path(bin, exe) 565 | 566 | args <- c( 567 | "--vanilla", "CMD", "INSTALL", "--no-multiarch", 568 | "-l", shQuote(path.expand(library)), 569 | shQuote(path.expand(tarball)) 570 | ) 571 | 572 | system2(R, args, stdout = TRUE, stderr = TRUE) 573 | 574 | } 575 | 576 | renv_bootstrap_platform_prefix <- function() { 577 | 578 | # construct version prefix 579 | version <- paste(R.version$major, R.version$minor, sep = ".") 580 | prefix <- paste("R", numeric_version(version)[1, 1:2], sep = "-") 581 | 582 | # include SVN revision for development versions of R 583 | # (to avoid sharing platform-specific artefacts with released versions of R) 584 | devel <- 585 | identical(R.version[["status"]], "Under development (unstable)") || 586 | identical(R.version[["nickname"]], "Unsuffered Consequences") 587 | 588 | if (devel) 589 | prefix <- paste(prefix, R.version[["svn rev"]], sep = "-r") 590 | 591 | # build list of path components 592 | components <- c(prefix, R.version$platform) 593 | 594 | # include prefix if provided by user 595 | prefix <- renv_bootstrap_platform_prefix_impl() 596 | if (!is.na(prefix) && nzchar(prefix)) 597 | components <- c(prefix, components) 598 | 599 | # build prefix 600 | paste(components, collapse = "/") 601 | 602 | } 603 | 604 | renv_bootstrap_platform_prefix_impl <- function() { 605 | 606 | # if an explicit prefix has been supplied, use it 607 | prefix <- Sys.getenv("RENV_PATHS_PREFIX", unset = NA) 608 | if (!is.na(prefix)) 609 | return(prefix) 610 | 611 | # if the user has requested an automatic prefix, generate it 612 | auto <- Sys.getenv("RENV_PATHS_PREFIX_AUTO", unset = NA) 613 | if (auto %in% c("TRUE", "True", "true", "1")) 614 | return(renv_bootstrap_platform_prefix_auto()) 615 | 616 | # empty string on failure 617 | "" 618 | 619 | } 620 | 621 | renv_bootstrap_platform_prefix_auto <- function() { 622 | 623 | prefix <- tryCatch(renv_bootstrap_platform_os(), error = identity) 624 | if (inherits(prefix, "error") || prefix %in% "unknown") { 625 | 626 | msg <- paste( 627 | "failed to infer current operating system", 628 | "please file a bug report at https://github.com/rstudio/renv/issues", 629 | sep = "; " 630 | ) 631 | 632 | warning(msg) 633 | 634 | } 635 | 636 | prefix 637 | 638 | } 639 | 640 | renv_bootstrap_platform_os <- function() { 641 | 642 | sysinfo <- Sys.info() 643 | sysname <- sysinfo[["sysname"]] 644 | 645 | # handle Windows + macOS up front 646 | if (sysname == "Windows") 647 | return("windows") 648 | else if (sysname == "Darwin") 649 | return("macos") 650 | 651 | # check for os-release files 652 | for (file in c("/etc/os-release", "/usr/lib/os-release")) 653 | if (file.exists(file)) 654 | return(renv_bootstrap_platform_os_via_os_release(file, sysinfo)) 655 | 656 | # check for redhat-release files 657 | if (file.exists("/etc/redhat-release")) 658 | return(renv_bootstrap_platform_os_via_redhat_release()) 659 | 660 | "unknown" 661 | 662 | } 663 | 664 | renv_bootstrap_platform_os_via_os_release <- function(file, sysinfo) { 665 | 666 | # read /etc/os-release 667 | release <- utils::read.table( 668 | file = file, 669 | sep = "=", 670 | quote = c("\"", "'"), 671 | col.names = c("Key", "Value"), 672 | comment.char = "#", 673 | stringsAsFactors = FALSE 674 | ) 675 | 676 | vars <- as.list(release$Value) 677 | names(vars) <- release$Key 678 | 679 | # get os name 680 | os <- tolower(sysinfo[["sysname"]]) 681 | 682 | # read id 683 | id <- "unknown" 684 | for (field in c("ID", "ID_LIKE")) { 685 | if (field %in% names(vars) && nzchar(vars[[field]])) { 686 | id <- vars[[field]] 687 | break 688 | } 689 | } 690 | 691 | # read version 692 | version <- "unknown" 693 | for (field in c("UBUNTU_CODENAME", "VERSION_CODENAME", "VERSION_ID", "BUILD_ID")) { 694 | if (field %in% names(vars) && nzchar(vars[[field]])) { 695 | version <- vars[[field]] 696 | break 697 | } 698 | } 699 | 700 | # join together 701 | paste(c(os, id, version), collapse = "-") 702 | 703 | } 704 | 705 | renv_bootstrap_platform_os_via_redhat_release <- function() { 706 | 707 | # read /etc/redhat-release 708 | contents <- readLines("/etc/redhat-release", warn = FALSE) 709 | 710 | # infer id 711 | id <- if (grepl("centos", contents, ignore.case = TRUE)) 712 | "centos" 713 | else if (grepl("redhat", contents, ignore.case = TRUE)) 714 | "redhat" 715 | else 716 | "unknown" 717 | 718 | # try to find a version component (very hacky) 719 | version <- "unknown" 720 | 721 | parts <- strsplit(contents, "[[:space:]]")[[1L]] 722 | for (part in parts) { 723 | 724 | nv <- tryCatch(numeric_version(part), error = identity) 725 | if (inherits(nv, "error")) 726 | next 727 | 728 | version <- nv[1, 1] 729 | break 730 | 731 | } 732 | 733 | paste(c("linux", id, version), collapse = "-") 734 | 735 | } 736 | 737 | renv_bootstrap_library_root_name <- function(project) { 738 | 739 | # use project name as-is if requested 740 | asis <- Sys.getenv("RENV_PATHS_LIBRARY_ROOT_ASIS", unset = "FALSE") 741 | if (asis) 742 | return(basename(project)) 743 | 744 | # otherwise, disambiguate based on project's path 745 | id <- substring(renv_bootstrap_hash_text(project), 1L, 8L) 746 | paste(basename(project), id, sep = "-") 747 | 748 | } 749 | 750 | renv_bootstrap_library_root <- function(project) { 751 | 752 | prefix <- renv_bootstrap_profile_prefix() 753 | 754 | path <- Sys.getenv("RENV_PATHS_LIBRARY", unset = NA) 755 | if (!is.na(path)) 756 | return(paste(c(path, prefix), collapse = "/")) 757 | 758 | path <- renv_bootstrap_library_root_impl(project) 759 | if (!is.null(path)) { 760 | name <- renv_bootstrap_library_root_name(project) 761 | return(paste(c(path, prefix, name), collapse = "/")) 762 | } 763 | 764 | renv_bootstrap_paths_renv("library", project = project) 765 | 766 | } 767 | 768 | renv_bootstrap_library_root_impl <- function(project) { 769 | 770 | root <- Sys.getenv("RENV_PATHS_LIBRARY_ROOT", unset = NA) 771 | if (!is.na(root)) 772 | return(root) 773 | 774 | type <- renv_bootstrap_project_type(project) 775 | if (identical(type, "package")) { 776 | userdir <- renv_bootstrap_user_dir() 777 | return(file.path(userdir, "library")) 778 | } 779 | 780 | } 781 | 782 | renv_bootstrap_validate_version <- function(version, description = NULL) { 783 | 784 | # resolve description file 785 | # 786 | # avoid passing lib.loc to `packageDescription()` below, since R will 787 | # use the loaded version of the package by default anyhow. note that 788 | # this function should only be called after 'renv' is loaded 789 | # https://github.com/rstudio/renv/issues/1625 790 | description <- description %||% packageDescription("renv") 791 | 792 | # check whether requested version 'version' matches loaded version of renv 793 | sha <- attr(version, "sha", exact = TRUE) 794 | valid <- if (!is.null(sha)) 795 | renv_bootstrap_validate_version_dev(sha, description) 796 | else 797 | renv_bootstrap_validate_version_release(version, description) 798 | 799 | if (valid) 800 | return(TRUE) 801 | 802 | # the loaded version of renv doesn't match the requested version; 803 | # give the user instructions on how to proceed 804 | remote <- if (!is.null(description[["RemoteSha"]])) { 805 | paste("rstudio/renv", description[["RemoteSha"]], sep = "@") 806 | } else { 807 | paste("renv", description[["Version"]], sep = "@") 808 | } 809 | 810 | # display both loaded version + sha if available 811 | friendly <- renv_bootstrap_version_friendly( 812 | version = description[["Version"]], 813 | sha = description[["RemoteSha"]] 814 | ) 815 | 816 | fmt <- paste( 817 | "renv %1$s was loaded from project library, but this project is configured to use renv %2$s.", 818 | "- Use `renv::record(\"%3$s\")` to record renv %1$s in the lockfile.", 819 | "- Use `renv::restore(packages = \"renv\")` to install renv %2$s into the project library.", 820 | sep = "\n" 821 | ) 822 | catf(fmt, friendly, renv_bootstrap_version_friendly(version), remote) 823 | 824 | FALSE 825 | 826 | } 827 | 828 | renv_bootstrap_validate_version_dev <- function(version, description) { 829 | expected <- description[["RemoteSha"]] 830 | is.character(expected) && startswith(expected, version) 831 | } 832 | 833 | renv_bootstrap_validate_version_release <- function(version, description) { 834 | expected <- description[["Version"]] 835 | is.character(expected) && identical(expected, version) 836 | } 837 | 838 | renv_bootstrap_hash_text <- function(text) { 839 | 840 | hashfile <- tempfile("renv-hash-") 841 | on.exit(unlink(hashfile), add = TRUE) 842 | 843 | writeLines(text, con = hashfile) 844 | tools::md5sum(hashfile) 845 | 846 | } 847 | 848 | renv_bootstrap_load <- function(project, libpath, version) { 849 | 850 | # try to load renv from the project library 851 | if (!requireNamespace("renv", lib.loc = libpath, quietly = TRUE)) 852 | return(FALSE) 853 | 854 | # warn if the version of renv loaded does not match 855 | renv_bootstrap_validate_version(version) 856 | 857 | # execute renv load hooks, if any 858 | hooks <- getHook("renv::autoload") 859 | for (hook in hooks) 860 | if (is.function(hook)) 861 | tryCatch(hook(), error = warnify) 862 | 863 | # load the project 864 | renv::load(project) 865 | 866 | TRUE 867 | 868 | } 869 | 870 | renv_bootstrap_profile_load <- function(project) { 871 | 872 | # if RENV_PROFILE is already set, just use that 873 | profile <- Sys.getenv("RENV_PROFILE", unset = NA) 874 | if (!is.na(profile) && nzchar(profile)) 875 | return(profile) 876 | 877 | # check for a profile file (nothing to do if it doesn't exist) 878 | path <- renv_bootstrap_paths_renv("profile", profile = FALSE, project = project) 879 | if (!file.exists(path)) 880 | return(NULL) 881 | 882 | # read the profile, and set it if it exists 883 | contents <- readLines(path, warn = FALSE) 884 | if (length(contents) == 0L) 885 | return(NULL) 886 | 887 | # set RENV_PROFILE 888 | profile <- contents[[1L]] 889 | if (!profile %in% c("", "default")) 890 | Sys.setenv(RENV_PROFILE = profile) 891 | 892 | profile 893 | 894 | } 895 | 896 | renv_bootstrap_profile_prefix <- function() { 897 | profile <- renv_bootstrap_profile_get() 898 | if (!is.null(profile)) 899 | return(file.path("profiles", profile, "renv")) 900 | } 901 | 902 | renv_bootstrap_profile_get <- function() { 903 | profile <- Sys.getenv("RENV_PROFILE", unset = "") 904 | renv_bootstrap_profile_normalize(profile) 905 | } 906 | 907 | renv_bootstrap_profile_set <- function(profile) { 908 | profile <- renv_bootstrap_profile_normalize(profile) 909 | if (is.null(profile)) 910 | Sys.unsetenv("RENV_PROFILE") 911 | else 912 | Sys.setenv(RENV_PROFILE = profile) 913 | } 914 | 915 | renv_bootstrap_profile_normalize <- function(profile) { 916 | 917 | if (is.null(profile) || profile %in% c("", "default")) 918 | return(NULL) 919 | 920 | profile 921 | 922 | } 923 | 924 | renv_bootstrap_path_absolute <- function(path) { 925 | 926 | substr(path, 1L, 1L) %in% c("~", "/", "\\") || ( 927 | substr(path, 1L, 1L) %in% c(letters, LETTERS) && 928 | substr(path, 2L, 3L) %in% c(":/", ":\\") 929 | ) 930 | 931 | } 932 | 933 | renv_bootstrap_paths_renv <- function(..., profile = TRUE, project = NULL) { 934 | renv <- Sys.getenv("RENV_PATHS_RENV", unset = "renv") 935 | root <- if (renv_bootstrap_path_absolute(renv)) NULL else project 936 | prefix <- if (profile) renv_bootstrap_profile_prefix() 937 | components <- c(root, renv, prefix, ...) 938 | paste(components, collapse = "/") 939 | } 940 | 941 | renv_bootstrap_project_type <- function(path) { 942 | 943 | descpath <- file.path(path, "DESCRIPTION") 944 | if (!file.exists(descpath)) 945 | return("unknown") 946 | 947 | desc <- tryCatch( 948 | read.dcf(descpath, all = TRUE), 949 | error = identity 950 | ) 951 | 952 | if (inherits(desc, "error")) 953 | return("unknown") 954 | 955 | type <- desc$Type 956 | if (!is.null(type)) 957 | return(tolower(type)) 958 | 959 | package <- desc$Package 960 | if (!is.null(package)) 961 | return("package") 962 | 963 | "unknown" 964 | 965 | } 966 | 967 | renv_bootstrap_user_dir <- function() { 968 | dir <- renv_bootstrap_user_dir_impl() 969 | path.expand(chartr("\\", "/", dir)) 970 | } 971 | 972 | renv_bootstrap_user_dir_impl <- function() { 973 | 974 | # use local override if set 975 | override <- getOption("renv.userdir.override") 976 | if (!is.null(override)) 977 | return(override) 978 | 979 | # use R_user_dir if available 980 | tools <- asNamespace("tools") 981 | if (is.function(tools$R_user_dir)) 982 | return(tools$R_user_dir("renv", "cache")) 983 | 984 | # try using our own backfill for older versions of R 985 | envvars <- c("R_USER_CACHE_DIR", "XDG_CACHE_HOME") 986 | for (envvar in envvars) { 987 | root <- Sys.getenv(envvar, unset = NA) 988 | if (!is.na(root)) 989 | return(file.path(root, "R/renv")) 990 | } 991 | 992 | # use platform-specific default fallbacks 993 | if (Sys.info()[["sysname"]] == "Windows") 994 | file.path(Sys.getenv("LOCALAPPDATA"), "R/cache/R/renv") 995 | else if (Sys.info()[["sysname"]] == "Darwin") 996 | "~/Library/Caches/org.R-project.R/R/renv" 997 | else 998 | "~/.cache/R/renv" 999 | 1000 | } 1001 | 1002 | renv_bootstrap_version_friendly <- function(version, shafmt = NULL, sha = NULL) { 1003 | sha <- sha %||% attr(version, "sha", exact = TRUE) 1004 | parts <- c(version, sprintf(shafmt %||% " [sha: %s]", substring(sha, 1L, 7L))) 1005 | paste(parts, collapse = "") 1006 | } 1007 | 1008 | renv_bootstrap_exec <- function(project, libpath, version) { 1009 | if (!renv_bootstrap_load(project, libpath, version)) 1010 | renv_bootstrap_run(version, libpath) 1011 | } 1012 | 1013 | renv_bootstrap_run <- function(version, libpath) { 1014 | 1015 | # perform bootstrap 1016 | bootstrap(version, libpath) 1017 | 1018 | # exit early if we're just testing bootstrap 1019 | if (!is.na(Sys.getenv("RENV_BOOTSTRAP_INSTALL_ONLY", unset = NA))) 1020 | return(TRUE) 1021 | 1022 | # try again to load 1023 | if (requireNamespace("renv", lib.loc = libpath, quietly = TRUE)) { 1024 | return(renv::load(project = getwd())) 1025 | } 1026 | 1027 | # failed to download or load renv; warn the user 1028 | msg <- c( 1029 | "Failed to find an renv installation: the project will not be loaded.", 1030 | "Use `renv::activate()` to re-initialize the project." 1031 | ) 1032 | 1033 | warning(paste(msg, collapse = "\n"), call. = FALSE) 1034 | 1035 | } 1036 | 1037 | renv_json_read <- function(file = NULL, text = NULL) { 1038 | 1039 | jlerr <- NULL 1040 | 1041 | # if jsonlite is loaded, use that instead 1042 | if ("jsonlite" %in% loadedNamespaces()) { 1043 | 1044 | json <- catch(renv_json_read_jsonlite(file, text)) 1045 | if (!inherits(json, "error")) 1046 | return(json) 1047 | 1048 | jlerr <- json 1049 | 1050 | } 1051 | 1052 | # otherwise, fall back to the default JSON reader 1053 | json <- catch(renv_json_read_default(file, text)) 1054 | if (!inherits(json, "error")) 1055 | return(json) 1056 | 1057 | # report an error 1058 | if (!is.null(jlerr)) 1059 | stop(jlerr) 1060 | else 1061 | stop(json) 1062 | 1063 | } 1064 | 1065 | renv_json_read_jsonlite <- function(file = NULL, text = NULL) { 1066 | text <- paste(text %||% read(file), collapse = "\n") 1067 | jsonlite::fromJSON(txt = text, simplifyVector = FALSE) 1068 | } 1069 | 1070 | renv_json_read_default <- function(file = NULL, text = NULL) { 1071 | 1072 | # find strings in the JSON 1073 | text <- paste(text %||% read(file), collapse = "\n") 1074 | pattern <- '["](?:(?:\\\\.)|(?:[^"\\\\]))*?["]' 1075 | locs <- gregexpr(pattern, text, perl = TRUE)[[1]] 1076 | 1077 | # if any are found, replace them with placeholders 1078 | replaced <- text 1079 | strings <- character() 1080 | replacements <- character() 1081 | 1082 | if (!identical(c(locs), -1L)) { 1083 | 1084 | # get the string values 1085 | starts <- locs 1086 | ends <- locs + attr(locs, "match.length") - 1L 1087 | strings <- substring(text, starts, ends) 1088 | 1089 | # only keep those requiring escaping 1090 | strings <- grep("[[\\]{}:]", strings, perl = TRUE, value = TRUE) 1091 | 1092 | # compute replacements 1093 | replacements <- sprintf('"\032%i\032"', seq_along(strings)) 1094 | 1095 | # replace the strings 1096 | mapply(function(string, replacement) { 1097 | replaced <<- sub(string, replacement, replaced, fixed = TRUE) 1098 | }, strings, replacements) 1099 | 1100 | } 1101 | 1102 | # transform the JSON into something the R parser understands 1103 | transformed <- replaced 1104 | transformed <- gsub("{}", "`names<-`(list(), character())", transformed, fixed = TRUE) 1105 | transformed <- gsub("[[{]", "list(", transformed, perl = TRUE) 1106 | transformed <- gsub("[]}]", ")", transformed, perl = TRUE) 1107 | transformed <- gsub(":", "=", transformed, fixed = TRUE) 1108 | text <- paste(transformed, collapse = "\n") 1109 | 1110 | # parse it 1111 | json <- parse(text = text, keep.source = FALSE, srcfile = NULL)[[1L]] 1112 | 1113 | # construct map between source strings, replaced strings 1114 | map <- as.character(parse(text = strings)) 1115 | names(map) <- as.character(parse(text = replacements)) 1116 | 1117 | # convert to list 1118 | map <- as.list(map) 1119 | 1120 | # remap strings in object 1121 | remapped <- renv_json_remap(json, map) 1122 | 1123 | # evaluate 1124 | eval(remapped, envir = baseenv()) 1125 | 1126 | } 1127 | 1128 | renv_json_remap <- function(json, map) { 1129 | 1130 | # fix names 1131 | if (!is.null(names(json))) { 1132 | lhs <- match(names(json), names(map), nomatch = 0L) 1133 | rhs <- match(names(map), names(json), nomatch = 0L) 1134 | names(json)[rhs] <- map[lhs] 1135 | } 1136 | 1137 | # fix values 1138 | if (is.character(json)) 1139 | return(map[[json]] %||% json) 1140 | 1141 | # handle true, false, null 1142 | if (is.name(json)) { 1143 | text <- as.character(json) 1144 | if (text == "true") 1145 | return(TRUE) 1146 | else if (text == "false") 1147 | return(FALSE) 1148 | else if (text == "null") 1149 | return(NULL) 1150 | } 1151 | 1152 | # recurse 1153 | if (is.recursive(json)) { 1154 | for (i in seq_along(json)) { 1155 | json[i] <- list(renv_json_remap(json[[i]], map)) 1156 | } 1157 | } 1158 | 1159 | json 1160 | 1161 | } 1162 | 1163 | # load the renv profile, if any 1164 | renv_bootstrap_profile_load(project) 1165 | 1166 | # construct path to library root 1167 | root <- renv_bootstrap_library_root(project) 1168 | 1169 | # construct library prefix for platform 1170 | prefix <- renv_bootstrap_platform_prefix() 1171 | 1172 | # construct full libpath 1173 | libpath <- file.path(root, prefix) 1174 | 1175 | # run bootstrap code 1176 | renv_bootstrap_exec(project, libpath, version) 1177 | 1178 | invisible() 1179 | 1180 | }) 1181 | --------------------------------------------------------------------------------