Использование предустановленных наборов параметров с пакетом config в R

Для корректного применения программного кода в разных случаях использования, например,

необходимо использовать отличные для каждой вышеперечисленных целей предустановочные (конфигурационных) значения. Поэтому для подобных целей, а также для отдельного хранения паролей и иных констант, которые следует поберечь от чужих глаз, целесообразно загружать данные из текстовых файлов форматов YAML или JSON. Первый имеет формат применяемый для задания начальных параметров в R Markdown и R Notebook файлах (*.Rmd).

В составе R/RStudio имеется пакет config, разработанный J. J. Allaire в компании RStudio, Inc. Его грамотное использование способно существенно облегчить работу с различными наборами предустановленных значений. Он загружает и манипулирует вариантами констант, сохраненных обычно в файл config.yml в текущей или родительской папке, если не указано иное. По крайней мере, один по умолчанию (англ. default).

Структура файла config.yml

Обычно применяют три набора значений:

  • Для среды ОБУЧЕНИЯ не задаватьте НИЧЕГО, это тоже, что дать опцию ‘default’ в .Renviron, Renviron.site , например, R_CONFIG_ACTIVE = 'default'.

  • Для среды ТЕСТИРОВАНИЯ задайте установку ‘test’ в Renviron.site / Rprofile.site, например, R_CONFIG_ACTIVE = 'test'.

  • Для среды РАЗВЕРТЫВАНИЯ задайте установку ‘production’ в Renviron.site / Rprofile.site, например, R_CONFIG_ACTIVE = 'production'.

Образец файла config.yml с подобной структурой приведен ниже:

# R for travis: see documentation at https://docs.travis-ci.com/user/languages/r
    
default:
  date    : !expr as.Date('2020-05-01')
  seed    : 2020
  debug   : ON
  datawarehouse : 
    driver: 'PostgreSQL' 
    dbname: 'gpdb'
    host  : '176.54.3.21'
    port  : 4321
    user  : 'user'
    pwd   : [0x70, 0x61, 0x73, 0x73]
    # pwd   : !expr rstudioapi::askForPassword('Password for PostgreSQL')
    # pwd   : !expr Sys.getenv('PGPWD')  # value from .Renviron, .Rprofile, Renviron.site or Rprofile.site files
    # keyring - Access the System Credential Store from R: https://www.r-bloggers.com/how-to-avoid-publishing-credentials-in-your-code
    options: '-c search_path=ANL'
  comments: >
    Для среды ОБУЧЕНИЯ не задаватьте НИЧЕГО, это тоже, что дать опцию 'default' в Renviron.site / Rprofile.site, например, R_CONFIG_ACTIVE = 'default' .
    Для среды ТЕСТИРОВАНИЯ дайте установку 'test' в Renviron.site / Rprofile.site, например, R_CONFIG_ACTIVE = 'test' .
    Для среды РАЗВЕРТЫВАНИЯ дайте установку 'production' в Renviron.site / Rprofile.site, например, R_CONFIG_ACTIVE = 'production' .
    Смотри информацию о "R Startup" (https://stat.ethz.ch/R-manual/R-devel/library/base/html/Startup.html) по этим файлам.
  server  : 'localhost:7600'
  
test:
  debug   : OFF

production:
  inherits: test
  debug   : !expr Sys.getenv('ENABLE_DEBUG') == '1'
  # server  : !expr config::get(value = 'server', file = '/etc/server-config.yml')
  server  : 'production.***.kz:7651'
  

Если компания применяет RStudio Connect - платформу для коллективной работе с базами данных на Lunix, которые, кроме всего прочего, имеют Professional Drivers, то тогда наряду с опцией default следует задавать набор предустановленных значений для rsconnect.

Как загрузить набор значений из config.yml файла

Обычно загрузка YAML файла производится из текущей папки, но можно задавать как другие папки, так и иные имена файлов, желательно с расширением *.yml.

Зачастую отдельные константы, помещаемые в config.yml файл, берутся из других файлов:

  • .Renviron;

  • .Rprofile;

  • Renviron.site и

  • Rprofile.site.

Подробности о .Renviron и им подобных файлах смотрите в материале “R Startup”. А вот файлы .Rprofile или Rprofile.site не рекомендую, так как считанные из них значения становятся переменными в R. Их можно легко считывать в R, тогда как загруженные из .Renviron остаются переменными окружения (англ. environment variables) и для их извлечения нужны определенные манипуляции.

Учтите, что данные из вышеперечисленных файлов считываются в начале сеанса R, а для файлов с переменными окружения (.Renviron и Renviron.site) остаются неизменными во время всего сеанса R.

При этом файлы Renviron.site и Rprofile.site используются непосредство в RStudio Server и относятся к конкретной версии R, поэтому они могут быть в папках каждой из версий программы. Особенности использования стратегии управления RStudio Server описаны в R for Enterprise: Understanding R’s Startup.

При необходимости в YAML файле можно непосредственно выполнить код R предварив его командой !expr.

# Use Configuration Values from YAML File `config.yml`

library('config')           # Manage Environment Specific Configuration Values
## 
## Attaching package: 'config'
## The following objects are masked from 'package:base':
## 
##     get, merge
# set the active configuration globally via .Renviron or .Rprofile
# Sys.setenv(R_CONFIG_ACTIVE = 'production')   # For 'production' settings
# see https://support.rstudio.com/hc/en-us/articles/360047157094-Managing-R-with-Rprofile-Renviron-Rprofile-site-Renviron-site-rsession-conf-and-repos-conf

try_Config_Get <- function(value = NULL, config = Sys.getenv('R_CONFIG_ACTIVE', 'default'),
  file = Sys.getenv('R_CONFIG_FILE', 'config.yml'), use_parent = TRUE) {
    
      out <- 
        tryCatch(
            expr = {
                result <- suppressWarnings( config::get(value, config, file, use_parent) )
                return( result )
            },   # end of `expr`
            error = function(e){
                message('Unsuccessfully executed the config::get() call!!!')
                print(e)

                return( NULL )
            },   # end of `error`
            finally = {
                message('All done in `config::get()`.')
            }   # end of `finally`
      )
      
      return( out )
      
      }  # End of function `try_Config_Get()`

# Load a Default Set of Configuration Values from YAML File `config.yml` 
config <- try_Config_Get()
## All done in `config::get()`.
# config <- suppressWarnings( config::get() ); str(config)   # from the parent directory of Project on ` file = 'config.yml' `

if ( exists('config') & is.null(config) == FALSE ) {
  writeLines('The Specific Configuration Values:')
  str(config)
  writeLines('The end of  Specific Configuration Values. \n')
  
  # Show Configuration comments from YAML File `config.yml` on the Configuration Sets of Values (Sections)
  # writeLines(config$comments)
}
## The Specific Configuration Values:
## List of 6
##  $ datawarehouse:List of 7
##   ..$ driver : chr "PostgreSQL"
##   ..$ dbname : chr "gpdb"
##   ..$ host   : chr "176.54.3.21"
##   ..$ port   : int 4321
##   ..$ user   : chr "user"
##   ..$ pwd    : int [1:4] 112 97 115 115
##   ..$ options: chr "-c search_path=ANL"
##  $ date         : Date[1:1], format: "2020-05-01"
##  $ seed         : int 2020
##  $ debug        : logi TRUE
##  $ comments     : chr "Для среды ОБУЧЕНИЯ не задаватьте НИЧЕГО, это тоже, что дать опцию 'default' в Renviron.site / Rprofile.site, на"| __truncated__
##  $ server       : chr "localhost:7600"
##  - attr(*, "config")= chr "default"
##  - attr(*, "file")= chr "C:\\Users\\arodionov\\Documents\\Projects\\20.01_SocialPortraitOfAFamily\\plumber-api\\config.yml"
## The end of  Specific Configuration Values.

Обратите внимание, что критически важную операцию загрузки config.yml файла вызывают в виде функции обработки ошибок в момент выполнения под именем try_Config_Get() для того, чтобы при необходимости перехватывать управление после некорректной попытки считать YAML файла с переданными опциями.

Как хранить в config.yml конфиденциальные данные

Очевидно, что конфиденциальные сведения важно хранить в зашифрованном виде, например, в шестнадцатиричном коде или закодированной по специальному ключю. Хотя порой такие данные хранятся как константы в конфигурационном файле .Renviron, но тогда они остаются неизменными во время всего сеанса R.

Для считывания пароля либо иной секретной информации нельзя применять какие-либо промежуточные переменные, поскольку содержимое таких переменных может быть перехвачено посторонними агентами. Поэтому такие сведения надлежит непосредственно брать из источника хранения.

# I. Show the Password from YAML File `config.yml` after converting from Hexadecimal codes
#  paste0('0x', as.hexmode(utf8ToInt('password')), collapse = ', ') or by MS Excel  ="0x" & DEC2HEX(CODE(A1)) & ","
writeLines('\n Convert password from comnfig.yml file of the Hexadecimal Codes:')
## 
##  Convert password from comnfig.yml file of the Hexadecimal Codes:
( suppressWarnings( readLines(rawConnection(as.raw(config$datawarehouse$pwd))) ) )
## [1] "pass"
close( rawConnection(as.raw(config$datawarehouse$pwd)) )  # Close Raw Connection with a password

# II. Check the activation of a particular Configuration Set of Value (Section) 'default' from YAML File
writeLines("\n You can check whether a particular configuration 'default' is active using the `config::is_active()`:")
## 
##  You can check whether a particular configuration 'default' is active using the `config::is_active()`:
config::is_active('default')
## [1] TRUE
# III. Check the activation of a particular Configuration Set of Value (Section) 'test' from YAML File
writeLines("\n You can check whether a particular configuration 'test' is active using the `config::is_active()`:")
## 
##  You can check whether a particular configuration 'test' is active using the `config::is_active()`:
config::is_active('test')
## [1] FALSE
# IV. Check the activation of a particular Configuration Set of Value (Section) 'production' from YAML File
writeLines("\n You can check whether a particular configuration 'production' is active using the `config::is_active()`:")
## 
##  You can check whether a particular configuration 'production' is active using the `config::is_active()`:
config::is_active('production')
## [1] FALSE
# V. Read a single configuration value (will return 'debug' = FALSE from a particular Configuration Values 'production')
writeLines( sprintf('\n You can Read a single configuration value from a loaded Configuration Sets of Values:\n debug = %s ', suppressWarnings( config::get('debug') ) ) )
## 
##  You can Read a single configuration value from a loaded Configuration Sets of Values:
##  debug = TRUE

Очевидно, что из возможных вариантов наборов предустановленных значений (например, default, test и production) загружен только один - default, который предусматривается для среды ОБУЧЕНИЯ (разработки моделей).

Как применять набор конфигурационных значений (на примере работы с Базой данных)

Теперь проверим как работает набор конфигурационных значений на примере получения списка таблиц из базы данных PostgreSQL, к которой подключаются по параметрам, считанных из config.yml файла, приведенного выше. Будет использован набор предустановленных значений для Обучения т.е. из секции default YAML файла.

library('DBI')              # R Database Interface

# install.packages('RPostgreSQL')
library('RPostgreSQL')    # R Interface to the 'PostgreSQL' Database System

writeLines( sprintf('Current version of package for DBMS PostgreSQL: %s', packageVersion('RPostgreSQL')) )
## Current version of package for DBMS PostgreSQL: 0.6.2
try_dbConnect_PostgreSQL <- function(drv) {   # https://stackoverflow.com/questions/12193779/how-to-write-trycatch-in-r
    
      out <- 
        tryCatch(
            expr = {
                con <- DBI::dbConnect(
                  drv      = drv,
                  dbname   = config[['datawarehouse']][['dbname']],
                  host     = config[['datawarehouse']][['host']],
                  port     = config[['datawarehouse']][['port']],
                  user     = config[['datawarehouse']][['user']],
                  password = suppressWarnings( readLines(rawConnection(as.raw(config[['datawarehouse']][['pwd']]))) ),  # rstudioapi::askForPassword('Password!')
                  options  = config[['datawarehouse']][['options']]
                  )
                close( rawConnection(as.raw(config[['datawarehouse']][['pwd']])) )
                message('Successfully executed the DBI::dbConnect(x) call.')
                
                return( con )
            },   # end of `expr`
            error = function(e){
                close( rawConnection(as.raw(config[['datawarehouse']][['pwd']])) )
                message('Unsuccessfully executed the DBI::dbConnect(x) call!!!')
                print(e)

                return( NULL )
            },   # end of `error`
            warning = function(w){
                close( rawConnection(as.raw(config[['datawarehouse']][['pwd']])) )
                message('Unsuccessfully executed the DBI::dbConnect(x) call!')
                print(w)
                
                return( con )
            },   # end of `warning`
            finally = {
                message('All done in `dbConnect_PostgreSQL()`.')
            }   # end of `finally`
      )
      
      return( out )
      
      }  # End of function `try_dbConnect_PostgreSQL()`

try_dbListTables_PostgreSQL <- function(con) {
    
      out <- 
        tryCatch(
            expr = {
                result <- DBI::dbListTables( con )
                return( result )
            },   # end of `expr`
            error = function(e){
                message('Unsuccessfully executed the DBI::dbListTables(con) call!!!')
                print(e)

                return( NULL )
            },   # end of `error`
            warning = function(w){
                message('Unsuccessfully executed the DBI::dbListTables(con) call!')
                print(w)
                return( NULL )
            },   # end of `warning`
            finally = {
                message('All done in `dbConnect_dbListTables()`.')
            }   # end of `finally`
      )
      
      return( out )
      
      }  # End of function `try_dbListTables_PostgreSQL()`

if ( exists('config') & is.null(config) == FALSE & config[['datawarehouse']][['driver']] == 'PostgreSQL' ) {

  writeLines('\nTo test for whether a configuration «default» is active you should use the `config::is_active()` function:')
  config::is_active('default')
  
  drv <- DBI::dbDriver(config[['datawarehouse']][['driver']])
  
  # Get all the tables from connection
  writeLines('The Description of Driver for DBMS PostgreSQL:')
  summary(drv)

  con <- try_dbConnect_PostgreSQL( drv )
  
  if ( is.null(con) == FALSE & RPostgreSQL::isPostgresqlIdCurrent(con) == TRUE ) { # Check Connection to PostgreSQL

    writeLines('\nThe Description of Session for DBMS PostgreSQL:')
    summary(con)
    
    writeLines('\nThe Description of Meta-Data in DBMS PostgreSQL:')
    out <- DBI::dbGetInfo(con)
    print( out )
    
    writeLines('\nThe Description of Data Type to DBMS PostgreSQL:')
    out <- DBI::dbDataType(con, 'ANY')
    print( out )
    
    browser()
    
    writeLines('\nThe Table list of Session`s User to DBMS PostgreSQL:')
    out <- try_dbListTables_PostgreSQL( con )
    print( out[grep('contracts_fpd_', out)] )
    
  } else {
    
    writeLines( sprintf('Unable to connect to Database `%s`!', config[['datawarehouse']][['dbname']]) )
    
  }     # end if ( is.null(con) == FALSE & RPostgreSQL::isPostgresqlIdCurrent(con) == TRUE )
  
}
## 
## To test for whether a configuration «default» is active you should use the `config::is_active()` function:
## The Description of Driver for DBMS PostgreSQL:
## <PostgreSQLDriver:(9664)> 
##   Driver name:  PostgreSQL 
##   Max  connections: 16 
##   Conn. processed: 0 
##   Default records per fetch: 500 
##   Open connections: 0
## Successfully executed the DBI::dbConnect(x) call.
## All done in `dbConnect_PostgreSQL()`.
## 
## The Description of Session for DBMS PostgreSQL:
## <PostgreSQLConnection:(9664,0)> 
##   User: user 
##   Host: 176.54.3.21 
##   Dbname: gpdb 
##   No resultSet available
## 
## The Description of Meta-Data in DBMS PostgreSQL:
## $host
## [1] "176.54.3.21"
## 
## $port
## [1] "4321"
## 
## $user
## [1] "user"
## 
## $dbname
## [1] "gpdb"
## 
## $serverVersion
## [1] "9.4.24"
## 
## $protocolVersion
## [1] 3
## 
## $backendPId
## [1] 16682
## 
## $rsId
## list()
## 
## 
## The Description of Data Type to DBMS PostgreSQL:
## [1] "text"
## Called from: eval(expr, envir, enclos)
## debug на <text>#107: writeLines("\nThe Table list of Session`s User to DBMS PostgreSQL:")
## 
## The Table list of Session`s User to DBMS PostgreSQL:
## debug на <text>#108: out <- try_dbListTables_PostgreSQL(con)
## All done in `dbConnect_dbListTables()`.
## debug на <text>#109: print(out[grep("contracts_fpd_", out)])
## [1] "contracts_fpd_spd_fiz"   "contracts_fpd_spd_ipjur"

Обратите внимание, что критически важные операции открытия пользователю сеанса с Базой данных PostgreSQL и извлечения списка таблиц у пользователя во время этого сеанса вызываются в виде функций обработки ошибок в момент выполнения под именами try_dbConnect_PostgreSQL() и try_dbListTables_PostgreSQL() соответственно.

Как напрямую выполнять SQL команды в сеансе с Базой данных.

Теперь обратимся к таблице contracts_fpd_spd_fiz Базы данных и извлечем из нее небольшую выборку, где поле providerid имеет значения 321 или 320.

# Declare Subset of table

library('glue')           # Interpreted String Literals 
  
types <- c(320L, 321L)
Provider_Status <- glue::glue_sql('{types*}', .con = con)

Следующий фрагмент объявлен как SQL chunck {sql ...}, поэтому необходимо указать в заголовке (то есть в фигурных скобках chunck) как connection = con, так и output.var = 'trials'. Кавычки обрамляющие выходную переменную trials также важны.

-- Query to PostgreSQL's Table with options
-- connection = 'con' in this chunk
-- output.var = 'trials' in this chunk

SELECT * FROM contracts_fpd_spd_fiz WHERE providerid IN (?Provider_Status) LIMIT 20;

Кстати, если сеанс связи с Базой данных будет прерван, то выполнение вышеуказанного SQL кода выдаст ошибку:

Error in (function (sql, outputFile, options) :

The 'connection' option must be a valid DBI connection.

Failed to execute SQL chunk

Теперь опубликуем часть выборки, извлеченную из таблицы contracts_fpd_spd_fiz Базы данных и переданную в выходную переменную trials.

head( trials ) 
##   creditinfoid contractid providerid  startdate fpd spd
## 1         7944       3183        321 2019-12-12   1  NA
## 2         8135       3184        320 2019-12-12  NA   1
## 3         4908       9392        321 2020-01-29  NA   1
## 4         2293       9425        321 2020-01-29   1  NA
## 5         2779       1817        320 2019-12-13   1  NA
## 6         9468       8154        321 2019-12-15   1  NA

Наконец, ставим точку в этом сеансе связи с Базой данных и отключаем Драйвера Базы данных.

if ( exists('config') & is.null(config) == FALSE & config[['datawarehouse']][['driver']] == 'PostgreSQL' ) {

  if ( is.null(con) == FALSE & RPostgreSQL::isPostgresqlIdCurrent(con) == TRUE ) { # Check Connection to PostgreSQL

    # Close the connection
    writeLines('\nClose the connection to DBMS PostgreSQL:')
    out <- DBI::dbDisconnect(con)
    print( out )
  
  }     # end if ( is.null(con) == FALSE & RPostgreSQL::isPostgresqlIdCurrent(con) == TRUE )
  
  writeLines('Unload the Driver for DBMS PostgreSQL:')
  DBI::dbUnloadDriver(drv)

} # end if ( config[['datawarehouse']][['driver']] == 'PostgreSQL')
## debug на <text>#4: if (is.null(con) == FALSE & RPostgreSQL::isPostgresqlIdCurrent(con) == 
##     TRUE) {
##     writeLines("\nClose the connection to DBMS PostgreSQL:")
##     out <- DBI::dbDisconnect(con)
##     print(out)
## }
## debug на <text>#7: writeLines("\nClose the connection to DBMS PostgreSQL:")
## 
## Close the connection to DBMS PostgreSQL:
## debug на <text>#8: out <- DBI::dbDisconnect(con)
## debug на <text>#9: print(out)
## [1] TRUE
## debug на <text>#13: writeLines("Unload the Driver for DBMS PostgreSQL:")
## Unload the Driver for DBMS PostgreSQL:
## debug на <text>#14: DBI::dbUnloadDriver(drv)
## [1] TRUE

Завершение сессии

# The End of Session

devtools::session_info()
## - Session info -------------------------------------------------------------------------------------------------------
##  setting  value                       
##  version  R version 4.0.2 (2020-06-22)
##  os       Windows 10 x64              
##  system   x86_64, mingw32             
##  ui       RTerm                       
##  language (EN)                        
##  collate  Russian_Russia.1251         
##  ctype    Russian_Russia.1251         
##  tz       Asia/Dhaka                  
##  date     2020-08-14                  
## 
## - Packages -----------------------------------------------------------------------------------------------------------
##  package     * version date       lib source        
##  assertthat    0.2.1   2019-03-21 [1] CRAN (R 4.0.0)
##  backports     1.1.8   2020-06-17 [1] CRAN (R 4.0.2)
##  callr         3.4.3   2020-03-28 [1] CRAN (R 4.0.0)
##  cli           2.0.2   2020-02-28 [1] CRAN (R 4.0.0)
##  config      * 0.3     2018-03-27 [1] CRAN (R 4.0.0)
##  crayon        1.3.4   2017-09-16 [1] CRAN (R 4.0.0)
##  DBI         * 1.1.0   2019-12-15 [1] CRAN (R 4.0.0)
##  desc          1.2.0   2018-05-01 [1] CRAN (R 4.0.0)
##  devtools      2.3.0   2020-04-10 [1] CRAN (R 4.0.0)
##  digest        0.6.25  2020-02-23 [1] CRAN (R 4.0.0)
##  ellipsis      0.3.1   2020-05-15 [1] CRAN (R 4.0.0)
##  evaluate      0.14    2019-05-28 [1] CRAN (R 4.0.0)
##  fansi         0.4.1   2020-01-08 [1] CRAN (R 4.0.0)
##  fs            1.5.0   2020-07-31 [1] CRAN (R 4.0.2)
##  glue        * 1.4.1   2020-05-13 [1] CRAN (R 4.0.0)
##  htmltools     0.5.0   2020-06-16 [1] CRAN (R 4.0.0)
##  knitr         1.29    2020-06-23 [1] CRAN (R 4.0.2)
##  magrittr      1.5     2014-11-22 [1] CRAN (R 4.0.0)
##  memoise       1.1.0   2017-04-21 [1] CRAN (R 4.0.0)
##  pkgbuild      1.1.0   2020-07-13 [1] CRAN (R 4.0.2)
##  pkgload       1.1.0   2020-05-29 [1] CRAN (R 4.0.0)
##  prettyunits   1.1.1   2020-01-24 [1] CRAN (R 4.0.0)
##  processx      3.4.3   2020-07-05 [1] CRAN (R 4.0.2)
##  ps            1.3.3   2020-05-08 [1] CRAN (R 4.0.0)
##  R6            2.4.1   2019-11-12 [1] CRAN (R 4.0.0)
##  remotes       2.1.1   2020-02-15 [1] CRAN (R 4.0.0)
##  rlang         0.4.7   2020-07-09 [1] CRAN (R 4.0.2)
##  rmarkdown     2.3     2020-06-18 [1] CRAN (R 4.0.0)
##  RPostgreSQL * 0.6-2   2017-06-24 [1] CRAN (R 4.0.2)
##  rprojroot     1.3-2   2018-01-03 [1] CRAN (R 4.0.0)
##  sessioninfo   1.1.1   2018-11-05 [1] CRAN (R 4.0.0)
##  stringi       1.4.6   2020-02-17 [1] CRAN (R 4.0.0)
##  stringr       1.4.0   2019-02-10 [1] CRAN (R 4.0.0)
##  testthat      2.3.2   2020-03-02 [1] CRAN (R 4.0.0)
##  usethis       1.6.1   2020-04-29 [1] CRAN (R 4.0.0)
##  withr         2.2.0   2020-04-20 [1] CRAN (R 4.0.0)
##  xfun          0.16    2020-07-24 [1] CRAN (R 4.0.2)
##  yaml          2.2.1   2020-02-01 [1] CRAN (R 4.0.0)
## 
## [1] C:/R/R-4.0.2/library
Sys.time()
## [1] "2020-08-14 13:07:05 +06"