├── .Rbuildignore ├── .github └── workflows │ ├── check.yaml │ ├── cla.yaml │ ├── docs.yaml │ ├── post-release.yaml │ ├── release.yaml │ └── scheduled.yaml ├── .gitignore ├── .lintr ├── .pre-commit-config.yaml ├── DESCRIPTION ├── NAMESPACE ├── NEWS.md ├── R ├── run_sas.R ├── sas_engine.R ├── sascfg.R ├── sasr-package.R ├── utils.R └── zzz.R ├── README.md ├── _pkgdown.yml ├── design ├── fev_mmrm.Rmd ├── fev_mmrm.r └── test.r ├── inst ├── WORDLIST └── example.Rmd ├── man ├── df2sd.Rd ├── dot-onLoad.Rd ├── dot-sasr_env.Rd ├── figures │ └── sasr-logo.svg ├── get_sas_cfg.Rd ├── get_sas_session.Rd ├── install_saspy.Rd ├── run_sas.Rd ├── sas_engine.Rd ├── sas_session.Rd ├── sas_session_ssh.Rd ├── sascfg.Rd ├── saspy.Rd ├── sasr-package.Rd ├── sd2df.Rd ├── validate_data.Rd └── validate_sascfg.Rd ├── revdep └── .gitignore ├── sasr.Rproj ├── staged_dependencies.yaml ├── tests ├── testthat.R └── testthat │ ├── _snaps │ ├── rmarkdown.md │ └── run_sas.md │ ├── helper-dummy_session.R │ ├── test-rmarkdown.R │ ├── test-run_sas.R │ ├── test-sascfg.R │ ├── test-session.R │ └── test-utils.R └── vignettes └── introduction.Rmd /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^renv$ 2 | ^renv\.lock$ 3 | CODE_OF_CONDUCT.md 4 | SECURITY.md 5 | ^.*\.Rproj$ 6 | ^\.Rproj\.user$ 7 | ^_pkgdown\.yml$ 8 | ^vignettes/hello\.Rmd$ 9 | ^docs$ 10 | ^\.github$ 11 | README.* 12 | ^\.lintr$ 13 | ^staged_dependencies\.yaml$ 14 | coverage.* 15 | ^\.pre-commit-config\.yaml$ 16 | ^codemeta\.json$ 17 | init\.sh 18 | workflows\.md 19 | images 20 | __pycache__ 21 | design 22 | sascfg_personal\.py 23 | ^pkgdown$ 24 | ^.revdeprefs\.yaml$ 25 | ^revdep$ 26 | ^\.covrignore$ 27 | -------------------------------------------------------------------------------- /.github/workflows/check.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Check 🛠 3 | 4 | on: 5 | pull_request: 6 | types: 7 | - opened 8 | - synchronize 9 | - reopened 10 | - ready_for_review 11 | branches: 12 | - main 13 | push: 14 | branches: 15 | - main 16 | workflow_dispatch: 17 | 18 | jobs: 19 | audit: 20 | name: Audit Dependencies 🕵️‍♂️ 21 | uses: insightsengineering/r.pkg.template/.github/workflows/audit.yaml@main 22 | r-cmd: 23 | name: R CMD Check 🧬 24 | uses: insightsengineering/r.pkg.template/.github/workflows/build-check-install.yaml@main 25 | secrets: 26 | REPO_GITHUB_TOKEN: ${{ secrets.REPO_GITHUB_TOKEN }} 27 | with: 28 | additional-r-cmd-check-params: --as-cran 29 | coverage: 30 | name: Coverage 📔 31 | uses: insightsengineering/r.pkg.template/.github/workflows/test-coverage.yaml@main 32 | secrets: 33 | REPO_GITHUB_TOKEN: ${{ secrets.REPO_GITHUB_TOKEN }} 34 | with: 35 | additional-env-vars: | 36 | NOT_CRAN=true 37 | linter: 38 | if: github.event_name != 'push' 39 | name: SuperLinter 🦸‍♀️ 40 | uses: insightsengineering/r.pkg.template/.github/workflows/linter.yaml@main 41 | roxygen: 42 | name: Roxygen 🅾 43 | uses: insightsengineering/r.pkg.template/.github/workflows/roxygen.yaml@main 44 | secrets: 45 | REPO_GITHUB_TOKEN: ${{ secrets.REPO_GITHUB_TOKEN }} 46 | with: 47 | auto-update: true 48 | gitleaks: 49 | name: gitleaks 💧 50 | uses: insightsengineering/r.pkg.template/.github/workflows/gitleaks.yaml@main 51 | spelling: 52 | name: Spell Check 🆎 53 | uses: insightsengineering/r.pkg.template/.github/workflows/spelling.yaml@main 54 | secrets: 55 | REPO_GITHUB_TOKEN: ${{ secrets.REPO_GITHUB_TOKEN }} 56 | links: 57 | if: github.event_name != 'push' 58 | name: Check URLs 🌐 59 | uses: insightsengineering/r.pkg.template/.github/workflows/links.yaml@main 60 | vbump: 61 | name: Version Bump 🤜🤛 62 | if: github.event_name == 'push' 63 | uses: insightsengineering/r.pkg.template/.github/workflows/version-bump.yaml@main 64 | secrets: 65 | REPO_GITHUB_TOKEN: ${{ secrets.REPO_GITHUB_TOKEN }} 66 | version: 67 | name: Version Check 🏁 68 | uses: insightsengineering/r.pkg.template/.github/workflows/version.yaml@main 69 | licenses: 70 | name: License Check 🃏 71 | uses: insightsengineering/r.pkg.template/.github/workflows/licenses.yaml@main 72 | style: 73 | if: github.event_name != 'push' 74 | name: Style Check 👗 75 | uses: insightsengineering/r.pkg.template/.github/workflows/style.yaml@main 76 | with: 77 | auto-update: true 78 | secrets: 79 | REPO_GITHUB_TOKEN: ${{ secrets.REPO_GITHUB_TOKEN }} 80 | grammar: 81 | if: github.event_name != 'push' 82 | name: Grammar Check 🔤 83 | uses: insightsengineering/r.pkg.template/.github/workflows/grammar.yaml@main 84 | -------------------------------------------------------------------------------- /.github/workflows/cla.yaml: -------------------------------------------------------------------------------- 1 | name: CLA 🔏 2 | 3 | on: 4 | issue_comment: 5 | types: 6 | - created 7 | # For PRs that originate from forks 8 | pull_request_target: 9 | types: 10 | - opened 11 | - closed 12 | - synchronize 13 | 14 | jobs: 15 | CLA: 16 | name: CLA 📝 17 | uses: insightsengineering/.github/.github/workflows/cla.yaml@main 18 | secrets: inherit 19 | -------------------------------------------------------------------------------- /.github/workflows/docs.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Docs 📚 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | paths: 9 | - "inst/templates/**" 10 | - "_pkgdown.*" 11 | - DESCRIPTION 12 | - "**.md" 13 | - "**.Rmd" 14 | - "man/**" 15 | - "LICENSE.*" 16 | - NAMESPACE 17 | pull_request: 18 | types: 19 | - opened 20 | - synchronize 21 | - reopened 22 | - ready_for_review 23 | branches: 24 | - main 25 | paths: 26 | - "inst/templates/**" 27 | - "_pkgdown.*" 28 | - DESCRIPTION 29 | - "**.md" 30 | - "**.Rmd" 31 | - "man/**" 32 | - "LICENSE.*" 33 | - NAMESPACE 34 | workflow_dispatch: 35 | 36 | jobs: 37 | docs: 38 | name: Pkgdown Docs 📚 39 | uses: insightsengineering/r.pkg.template/.github/workflows/pkgdown.yaml@main 40 | secrets: 41 | REPO_GITHUB_TOKEN: ${{ secrets.REPO_GITHUB_TOKEN }} 42 | with: 43 | default-landing-page: latest-tag 44 | -------------------------------------------------------------------------------- /.github/workflows/post-release.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Post release ✨ 3 | 4 | on: 5 | release: 6 | types: ["released"] 7 | 8 | jobs: 9 | vbump: 10 | name: Version Bump 🤜🤛 11 | uses: insightsengineering/r.pkg.template/.github/workflows/version-bump.yaml@main 12 | secrets: 13 | REPO_GITHUB_TOKEN: ${{ secrets.REPO_GITHUB_TOKEN }} 14 | with: 15 | vbump-after-release: true 16 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Release 🎈 3 | 4 | on: 5 | push: 6 | tags: 7 | - "v*" 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | name: Build package 🎁 13 | needs: release 14 | uses: insightsengineering/r.pkg.template/.github/workflows/build-check-install.yaml@main 15 | secrets: 16 | REPO_GITHUB_TOKEN: ${{ secrets.REPO_GITHUB_TOKEN }} 17 | with: 18 | skip-r-cmd-check: true 19 | skip-r-cmd-install: true 20 | docs: 21 | name: Pkgdown Docs 📚 22 | needs: release 23 | uses: insightsengineering/r.pkg.template/.github/workflows/pkgdown.yaml@main 24 | secrets: 25 | REPO_GITHUB_TOKEN: ${{ secrets.REPO_GITHUB_TOKEN }} 26 | with: 27 | default-landing-page: latest-tag 28 | validation: 29 | name: R Package Validation report 📃 30 | needs: release 31 | uses: insightsengineering/r.pkg.template/.github/workflows/validation.yaml@main 32 | secrets: 33 | REPO_GITHUB_TOKEN: ${{ secrets.REPO_GITHUB_TOKEN }} 34 | release: 35 | name: Create release 🎉 36 | uses: insightsengineering/r.pkg.template/.github/workflows/release.yaml@main 37 | secrets: 38 | REPO_GITHUB_TOKEN: ${{ secrets.REPO_GITHUB_TOKEN }} 39 | wasm: 40 | name: Build WASM packages 🧑‍🏭 41 | needs: release 42 | uses: insightsengineering/r.pkg.template/.github/workflows/wasm.yaml@main 43 | -------------------------------------------------------------------------------- /.github/workflows/scheduled.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Scheduled 🕰️ 3 | 4 | on: 5 | schedule: 6 | - cron: '45 3 * * 0' 7 | workflow_dispatch: 8 | inputs: 9 | chosen-workflow: 10 | description: | 11 | Select which workflow you'd like to run 12 | required: true 13 | type: choice 14 | default: rhub 15 | options: 16 | - rhub 17 | - dependency-test 18 | - branch-cleanup 19 | - revdepcheck 20 | 21 | jobs: 22 | dependency-test: 23 | if: > 24 | github.event_name == 'schedule' || ( 25 | github.event_name == 'workflow_dispatch' && 26 | inputs.chosen-workflow == 'dependency-test' 27 | ) 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | test-strategy: ["min_cohort", "min_isolated", "release", "max"] 32 | uses: insightsengineering/r.pkg.template/.github/workflows/verdepcheck.yaml@main 33 | name: Dependency Test - ${{ matrix.test-strategy }} 🔢 34 | secrets: 35 | REPO_GITHUB_TOKEN: ${{ secrets.REPO_GITHUB_TOKEN }} 36 | GCHAT_WEBHOOK: ${{ secrets.GCHAT_WEBHOOK }} 37 | with: 38 | strategy: ${{ matrix.test-strategy }} 39 | additional-env-vars: | 40 | PKG_SYSREQS_DRY_RUN=true 41 | branch-cleanup: 42 | if: > 43 | github.event_name == 'schedule' || ( 44 | github.event_name == 'workflow_dispatch' && 45 | inputs.chosen-workflow == 'branch-cleanup' 46 | ) 47 | name: Branch Cleanup 🧹 48 | uses: insightsengineering/r.pkg.template/.github/workflows/branch-cleanup.yaml@main 49 | secrets: 50 | REPO_GITHUB_TOKEN: ${{ secrets.REPO_GITHUB_TOKEN }} 51 | revdepcheck: 52 | if: > 53 | github.event_name == 'schedule' || ( 54 | github.event_name == 'workflow_dispatch' && 55 | inputs.chosen-workflow == 'revdepcheck' 56 | ) 57 | name: revdepcheck ↩️ 58 | uses: insightsengineering/r.pkg.template/.github/workflows/revdepcheck.yaml@main 59 | rhub: 60 | if: > 61 | github.event_name == 'schedule' || ( 62 | github.event_name == 'workflow_dispatch' && 63 | inputs.chosen-workflow == 'rhub' 64 | ) 65 | name: R-hub 🌐 66 | uses: insightsengineering/r.pkg.template/.github/workflows/rhub.yaml@main 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .httr-oauth 3 | .project 4 | .RData 5 | .Rhistory 6 | .Rprofile 7 | .Rproj.user 8 | .Ruserdata 9 | .settings/** 10 | *.html 11 | *.Rcheck 12 | *.rprof 13 | *.sas.txt 14 | *~ 15 | /.project 16 | devel/* 17 | doc 18 | docs 19 | inst/outputs/* 20 | logs 21 | Meta 22 | packrat/lib*/ 23 | temp 24 | temp_w 25 | templates/ 26 | tmp.* 27 | vignettes/*.html 28 | vignettes/*.md 29 | vignettes/*.R 30 | coverage.* 31 | .vscode/ 32 | node_modules/ 33 | package-lock.json 34 | package.json 35 | __pycache__ 36 | sascfg_personal.py 37 | tests/testthat/_snaps/**/*.new.md 38 | tests/testthat/_snaps/**/*.new.svg 39 | -------------------------------------------------------------------------------- /.lintr: -------------------------------------------------------------------------------- 1 | linters: linters_with_defaults( 2 | line_length_linter = line_length_linter(120), 3 | cyclocomp_linter = NULL, 4 | object_usage_linter = NULL 5 | ) 6 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # All available hooks: https://pre-commit.com/hooks.html 3 | # R specific hooks: https://github.com/lorenzwalthert/precommit 4 | repos: 5 | - repo: https://github.com/lorenzwalthert/precommit 6 | rev: v0.4.3.9007 7 | hooks: 8 | - id: style-files 9 | args: [--style_pkg=styler, --style_fun=tidyverse_style] 10 | - id: roxygenize 11 | additional_dependencies: 12 | - plumber 13 | - shiny 14 | # codemeta must be above use-tidy-description when both are used 15 | # - id: codemeta-description-updated 16 | - id: use-tidy-description 17 | - id: spell-check 18 | exclude: > 19 | (?x)^( 20 | data/.*| 21 | (.*/|)\.Rprofile| 22 | (.*/|)\.Renviron| 23 | (.*/|)\.gitignore| 24 | (.*/|)NAMESPACE| 25 | (.*/|)DESCRIPTION| 26 | (.*/|)WORDLIST| 27 | (.*/|)LICENSE| 28 | (.*/|)\.Rbuildignore| 29 | (.*/|)\.lintr| 30 | (.*/|)_pkgdown.y[a]?ml| 31 | (.*/|)\.covrignore| 32 | (.*/|)staged_dependencies.y[a]?ml| 33 | (.*/|)\.pre-commit-.*| 34 | \.github/.*| 35 | .*\.[rR]| 36 | .*\.Rproj| 37 | .*\.py| 38 | .*\.feather| 39 | .*\.rds| 40 | .*\.Rds| 41 | .*\.sh| 42 | .*\.RData| 43 | renv.lock 44 | )$ 45 | - id: lintr 46 | - id: readme-rmd-rendered 47 | - id: parsable-R 48 | - id: no-browser-statement 49 | - id: deps-in-desc 50 | - repo: https://github.com/pre-commit/mirrors-prettier 51 | rev: v4.0.0-alpha.8 52 | hooks: 53 | - id: prettier 54 | - repo: https://github.com/pre-commit/pre-commit-hooks 55 | rev: v5.0.0 56 | hooks: 57 | - id: check-added-large-files 58 | args: ["--maxkb=200"] 59 | - id: end-of-file-fixer 60 | exclude: '\.Rd' 61 | - id: trailing-whitespace 62 | exclude: '\.Rd' 63 | - id: check-yaml 64 | - id: no-commit-to-branch 65 | - id: mixed-line-ending 66 | args: ["--fix=lf"] 67 | - id: detect-aws-credentials 68 | args: ["--allow-missing-credentials"] 69 | - id: detect-private-key 70 | - id: forbid-new-submodules 71 | - id: check-symlinks 72 | - repo: local 73 | hooks: 74 | - id: forbid-to-commit 75 | name: Don't commit common R artifacts 76 | entry: Cannot commit .Rhistory, .RData, .Rds or .rds. 77 | language: fail 78 | files: '\.Rhistory|\.RData|\.Rds|\.rds$' 79 | # `exclude: ` to allow committing specific files. 80 | - repo: https://github.com/igorshubovych/markdownlint-cli 81 | rev: v0.44.0 82 | hooks: 83 | - id: markdownlint 84 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Type: Package 2 | Package: sasr 3 | Title: 'SAS' Interface 4 | Version: 0.1.4.9000 5 | Date: 2025-03-30 6 | Authors@R: c( 7 | person("Liming", "Li", , "clark.liming@gmail.com", role = c("aut", "cre")), 8 | person("Daniel", "Sabanes Bove", , "daniel.sabanesbove@gmail.com", role = "aut"), 9 | person("Isaac", "Gravestock", , "isaac.gravestock@roche.com", role = "aut"), 10 | person("F. Hoffmann-La Roche AG", role = c("cph", "fnd")) 11 | ) 12 | Description: Provides a 'SAS' interface, through 13 | 'SASPy'() and 14 | 'reticulate'(). This package 15 | helps you create 'SAS' sessions, execute 'SAS' code in remote 'SAS' 16 | servers, retrieve execution results and log, and exchange datasets 17 | between 'SAS' and 'R'. It also helps you to install 'SASPy' and 18 | create a configuration file for the connection. Please review the 19 | 'SASPy' license file as instructed so that you comply with its 20 | separate and independent license. 21 | License: Apache License 2.0 22 | URL: https://github.com/insightsengineering/sasr/, 23 | https://insightsengineering.github.io/sasr/latest-tag/ 24 | BugReports: https://github.com/insightsengineering/sasr/issues 25 | Depends: 26 | R (>= 3.6) 27 | Imports: 28 | checkmate, 29 | lifecycle, 30 | reticulate 31 | Suggests: 32 | knitr, 33 | mockery, 34 | rmarkdown, 35 | testthat (>= 3.0.0) 36 | VignetteBuilder: 37 | knitr 38 | biocViews: 39 | Config/testthat/edition: 3 40 | Encoding: UTF-8 41 | Language: en-US 42 | LazyData: true 43 | Roxygen: list(markdown = TRUE) 44 | RoxygenNote: 7.3.2 45 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | export(df2sd) 4 | export(get_sas_cfg) 5 | export(get_sas_session) 6 | export(install_saspy) 7 | export(run_sas) 8 | export(sas_session) 9 | export(sascfg) 10 | export(sd2df) 11 | import(checkmate) 12 | import(reticulate) 13 | importFrom(lifecycle,deprecate_warn) 14 | importFrom(utils,askYesNo) 15 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | # sasr 0.1.4.9000 2 | 3 | # sasr 0.1.4 4 | 5 | * Add a basic SAS engine for knitr. 6 | * Update `sas_session` to allow finer control over the sas session. 7 | 8 | # sasr 0.1.2 9 | 10 | * First CRAN version of the package. 11 | * The package facilitates the execution of SAS code from R. 12 | 13 | ### New features 14 | * Create SAS sessions. 15 | * Execute SAS code in SAS sessions. 16 | * Retrieve execution logs and results from SAS sessions. 17 | * Transfer datasets between SAS and R. 18 | -------------------------------------------------------------------------------- /R/run_sas.R: -------------------------------------------------------------------------------- 1 | #' Run SAS code with SAS Session 2 | #' 3 | #' @description `r lifecycle::badge("experimental")` 4 | #' Run SAS code with a SAS session. 5 | #' 6 | #' @param sas_code (`character`)\cr sas code to be executed. 7 | #' @param results (`character`)\cr sas code execution results type. 8 | #' @param sas_session (`saspy.sasbase.SASsession`) SAS session. 9 | #' 10 | #' @return Named list with following elements: 11 | #' - `LOG`: `string` of SAS execution log. 12 | #' - `LST`: `string` of SAS execution result, in html or txt format. 13 | #' 14 | #' @export 15 | #' @details `run_sas` will run sas code through SAS session. 16 | #' The results is a named list of `LST` and `LOG`. 17 | #' The result part will be stored in `LST`, and log will be stored in `LOG`. 18 | #' If `results` argument is "TEXT", then results are in text format; 19 | #' if `results` argument is "HTML", then results are in html format. 20 | #' 21 | run_sas <- function(sas_code, results = c("TEXT", "HTML"), sas_session = get_sas_session()) { 22 | assert_string(sas_code) 23 | results <- match.arg(results) 24 | sas_session$submit(code = sas_code, results = results) 25 | } 26 | #' Transfer data.frame to SAS 27 | #' 28 | #' @description `r lifecycle::badge("experimental")` 29 | #' Transfer `data.frame` object from R environment to SAS. 30 | #' 31 | #' @param df (`data.frame`)\cr data frame to be transferred. 32 | #' @param table (`character`)\cr table name in SAS. 33 | #' @param libref (`character`)\cr library name in SAS. 34 | #' @param sas_session (`saspy.sasbase.SASsession`) SAS session. 35 | #' @param ... additional arguments for `saspy.sasbase.SASsession.df2sd` 36 | #' 37 | #' @return "saspy.sasdata.SASdata" object. 38 | #' @export 39 | df2sd <- function(df, table = "_df", libref = "", ..., sas_session = get_sas_session()) { 40 | df <- validate_data(df) 41 | sas_session$df2sd(df, table = table, libref = libref, ...) 42 | } 43 | 44 | #' Transfer SAS Data to R 45 | #' 46 | #' @description `r lifecycle::badge("experimental")` 47 | #' Transfer the table in SAS session to R. 48 | #' 49 | #' @param table (`character`)\cr table name in SAS. 50 | #' @param libref (`character`)\cr library name in SAS. 51 | #' @param sas_session (`saspy.sasbase.SASsession`) SAS session. 52 | #' @param ... additional arguments for `saspy.sasbase.SASsession.sd2df` 53 | #' 54 | #' @return `data.frame` object. 55 | #' @export 56 | sd2df <- function(table, libref = "", ..., sas_session = get_sas_session()) { 57 | sas_session$sd2df(table = table, libref = libref, ...) 58 | } 59 | -------------------------------------------------------------------------------- /R/sas_engine.R: -------------------------------------------------------------------------------- 1 | #' SAS engine function 2 | #' @param options See knitr documentation on engines. 3 | sas_engine <- function(options) { 4 | if (!requireNamespace("knitr", quietly = TRUE)) { 5 | stop("Please install knitr to use the SAS engine.") 6 | } 7 | if (options$eval) { 8 | ret <- sasr::run_sas(paste0(options$code, collapse = "\n"), results = "HTML") 9 | if (identical(ret$LST, "")) { 10 | stop(ret$LOG) 11 | } else { 12 | output <- ret$LST 13 | output <- gsub("", "", output) 14 | } 15 | } else { 16 | output <- NULL 17 | } 18 | options$results <- "asis" 19 | knitr::engine_output(options, code = options$code, out = output) 20 | } 21 | -------------------------------------------------------------------------------- /R/sascfg.R: -------------------------------------------------------------------------------- 1 | #' Create SAS Session Configuration File 2 | #' 3 | #' @description `r lifecycle::badge("experimental")` 4 | #' Create SAS session configuration file based on argument. 5 | #' 6 | #' @param name (`character`)\cr name of the configuration. 7 | #' @param host (`character`)\cr host name of remote server. 8 | #' @param saspath (`character`)\cr SAS executable path on remote server. 9 | #' @param ssh (`character`)\cr executable path of ssh. 10 | #' @param encoding (`character`)\cr encoding of the SAS session. 11 | #' @param ... additional arguments. 12 | #' @param sascfg (`character`)\cr target file of configuration. 13 | #' @param options (`list`)\cr additional list of arguments to pass to `ssh` command. 14 | #' 15 | #' @return No return value. 16 | #' 17 | #' @export 18 | #' @details 19 | #' `host` and `saspath` are required to connect to remote SAS server. Other arguments can follow default. 20 | #' If transferring datasets is needed and the client(running sasr) is not reachable from the server, 21 | #' then tunnelling is required. 22 | #' Use `tunnel = `, `rtunnel = ` to specify tunnels and reverse tunnels. 23 | #' The values should be length 1 integer. 24 | sascfg <- function(name = "default", host, saspath, ssh = system("which ssh", intern = TRUE), 25 | encoding = "latin1", options = list("-fullstimer"), ..., sascfg = "sascfg_personal.py") { 26 | lst <- list(host = host, saspath = saspath, ssh = ssh, encoding = encoding, options = options) 27 | lst <- c(lst, list(...)) 28 | f <- file(sascfg, "w") 29 | writeLines(sprintf("SAS_config_names=['%s']", name), con = f) 30 | writeLines(sprintf("%s=%s", name, toString(r_to_py(lst))), f) 31 | close(f) 32 | invisible() 33 | } 34 | -------------------------------------------------------------------------------- /R/sasr-package.R: -------------------------------------------------------------------------------- 1 | #' `sasr` Package 2 | #' 3 | #' `sasr` provides interface to SAS through `saspy` and `reticulate` in R. 4 | #' 5 | #' @aliases sasr-package 6 | "_PACKAGE" 7 | #' 8 | #' @import reticulate 9 | #' @import checkmate 10 | #' @importFrom lifecycle deprecate_warn 11 | #' @importFrom utils askYesNo 12 | NULL 13 | -------------------------------------------------------------------------------- /R/utils.R: -------------------------------------------------------------------------------- 1 | #' Install `saspy` Module 2 | #' 3 | #' @description `r lifecycle::badge("experimental")` 4 | #' Install `saspy` module in `reticulate`. 5 | #' 6 | #' @param method (`character`)\cr method to install `saspy`. 7 | #' @param conda (`character`)\cr path to `conda` executable. 8 | #' 9 | #' @return No return value. 10 | #' @export 11 | install_saspy <- function(method = "auto", conda = "auto") { 12 | msg <- paste0( 13 | "Before installing saspy, please read and confirm that you will ", 14 | "comply to the lincense of saspy:\n", 15 | "https://github.com/sassoftware/saspy/blob/main/LICENSE.md\n", 16 | "I have read the license and confirm that I will comply to the license:" 17 | ) 18 | accept <- askYesNo(msg) 19 | if (!identical(accept, TRUE)) { 20 | stop("Installation of saspy cancelled.") 21 | } 22 | reticulate::py_install("saspy", method = method, conda = conda) 23 | } 24 | 25 | #' Validate and Process `data.frame` for SAS 26 | #' 27 | #' @description `r lifecycle::badge("experimental")` 28 | #' Validate if data contains validate variable names in SAS, and 29 | #' remove possible row names. 30 | #' 31 | #' @param data (`data.frame`)\cr data.frame to be checked. 32 | #' 33 | #' @keywords internal 34 | #' 35 | #' @details 36 | #' In SAS, the variable names should be consist of letters, numbers and underscore. 37 | #' Other characters are not allowed. 38 | #' In addition, in SAS, row names(index) are not allowed. 39 | #' 40 | #' @return `data.frame`\cr 41 | validate_data <- function(data) { 42 | nms <- colnames(data) 43 | nms_check <- grepl("^[a-zA-Z_]+(\\w+)?$", nms) 44 | if (!all(nms_check)) { 45 | stop( 46 | "SAS data frame must only contain letters, numbers and underscore, and must not start with numbers!\n", 47 | toString(nms[!nms_check]), 48 | " contains illegal characters that is not allowed.\n" 49 | ) 50 | } 51 | if (!identical(row.names(data), as.character(seq_len(nrow(data))))) { 52 | warning( 53 | "row.names is not supported in SAS and will be dropped, ", 54 | "Please consider create a column to hold the row names.", 55 | call. = FALSE 56 | ) 57 | row.names(data) <- NULL 58 | } 59 | return(data) 60 | } 61 | 62 | #' Validate SAS Configuration File Exist 63 | #' 64 | #' @description `r lifecycle::badge("experimental")` 65 | #' Validate if SAS configuration file exist. 66 | #' 67 | #' @param sascfg (`character`)\cr file path of configuration. 68 | #' 69 | #' @keywords internal 70 | #' 71 | #' @details 72 | #' Currently, only the file existence check is conducted and the rest 73 | #' is checked at python side. 74 | validate_sascfg <- function(sascfg) { 75 | if (is.null(sascfg)) { 76 | warning( 77 | "No SAS configuration file specified. By default the configuration file under ", 78 | " saspy installation path will be used. This is usually not a real accessible SAS configuration." 79 | ) 80 | return() 81 | } 82 | if (!file.exists(sascfg)) { 83 | stop( 84 | sascfg, 85 | " must exist to establish a connection.\n", 86 | "Use `sascfg()` to create this file and modify its content accordingly!" 87 | ) 88 | } 89 | } 90 | 91 | #' Get the Last or Default SAS Session 92 | #' 93 | #' @description `r lifecycle::badge("experimental")` 94 | #' Obtain the last session or default session. 95 | #' 96 | #' @details this function is designed to facilitate the R users programming practice 97 | #' of function oriented programming instead of object oriented programmings. 98 | #' 99 | #' @return A new SAS session if there are no previous SAS session, or the last SAS session created. 100 | #' @export 101 | get_sas_session <- function() { 102 | if (is.null(.sasr_env$.sas_session)) { 103 | .sasr_env$.sas_session <- sas_session(sascfg = get_sas_cfg()) 104 | } 105 | if (is.null(.sasr_env$.sas_session)) { 106 | stop( 107 | "SAS session not established! Please review the python part and update ", 108 | getOption("sascfg"), 109 | " accordingly.\n", 110 | "You can also set the default sas cofiguration file using `options(sascfg = )` ", 111 | "to allow other files to be used, ", 112 | "or use `sas_session(sascfg =)` to choose the configuration file manually.\n" 113 | ) 114 | } 115 | return(.sasr_env$.sas_session) 116 | } 117 | 118 | #' Create SAS Session Based on Configuration File 119 | #' 120 | #' @description `r lifecycle::badge("experimental")` 121 | #' Create a SAS session. 122 | #' 123 | #' @param sascfg (`string`)\cr SAS session configuration. 124 | #' @param ... additional arguments passed to `saspy.SASsession()`. 125 | #' Can override the configuration file. 126 | #' 127 | #' @return SAS session. 128 | #' @export 129 | sas_session <- function(sascfg = get_sas_cfg(), ...) { 130 | validate_sascfg(sascfg) 131 | session <- saspy$SASsession(cfgfile = sascfg, ...) 132 | .sasr_env$.sas_session <- session 133 | return(session) 134 | } 135 | 136 | #' Create SAS Session Based on Configuration File 137 | #' @inherit sas_session 138 | #' @description `r lifecycle::badge("deprecated")` 139 | sas_session_ssh <- function(sascfg = get_sas_cfg(), ...) { 140 | lifecycle::deprecate_warn( 141 | when = "0.1.3", 142 | what = "sas_session_ssh()", 143 | details = "Please use `sas_session` instead" 144 | ) 145 | sas_session(sascfg = sascfg, ...) 146 | } 147 | 148 | #' Obtain the SAS Configuration File 149 | #' 150 | #' @description `r lifecycle::badge("experimental")` 151 | #' Obtain the file path of the SAS configuration file. 152 | #' 153 | #' @details Obtain the default sas configuration file. By default, it will search 154 | #' the `sascfg_personal.py` file under current directory. If it does not exist, it will 155 | #' search this file under home directory. If this file does not exist, NULL will be returned. 156 | #' 157 | #' @return The file path of default SAS configuration file, or NULL if not found. 158 | #' 159 | #' @export 160 | get_sas_cfg <- function() { 161 | default_cfg <- getOption("sascfg", "sascfg_personal.py") 162 | if (file.exists(default_cfg)) { 163 | return(default_cfg) 164 | } 165 | if (file.exists(file.path("~", default_cfg))) { 166 | return(file.path("~", default_cfg)) 167 | } 168 | return(NULL) 169 | } 170 | -------------------------------------------------------------------------------- /R/zzz.R: -------------------------------------------------------------------------------- 1 | #' saspy package 2 | #' 3 | #' @keywords internal 4 | saspy <- NULL 5 | 6 | #' sasr Environment 7 | #' 8 | #' @keywords internal 9 | .sasr_env <- new.env() 10 | 11 | #' onLoad Function 12 | #' 13 | #' @keywords internal 14 | .onLoad <- function(libname, pkgname) { 15 | options("sascfg" = "sascfg_personal.py") 16 | saspy <<- import("saspy", delay_load = TRUE) 17 | if (requireNamespace("knitr", quietly = TRUE)) { 18 | knitr::knit_engines$set(sas = sas_engine) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sasr 2 | 3 | 4 | 5 | 6 | [![Code Coverage](https://raw.githubusercontent.com/insightsengineering/sasr/_xml_coverage_reports/data/main/badge.svg)](https://raw.githubusercontent.com/insightsengineering/sasr/_xml_coverage_reports/data/main/coverage.xml) 7 | 8 | 9 | This package provides interface to SAS through `saspy` and `reticulate`. 10 | 11 | ## Prerequisites 12 | 13 | To use `sasr`, you need to make sure you have the following 14 | 15 | 1. An SAS server that is accessible from the machine that you want to run `sasr` on 16 | 1. The machine that you want to run `sasr` has Python and Java 17 | 18 | ## Installation 19 | 20 | To install `sasr`, please use the following command 21 | 22 | ```{r} 23 | remotes::install_github(repo = 'insightsengineering/sasr') 24 | ``` 25 | 26 | Reticulate will be installed automatically, but Python package `saspy` will not. 27 | 28 | If you do not have Python, you can use the following code to install Python, or it can be installed automatically after you call some python related stuffs. 29 | 30 | ```{r} 31 | library(reticulate) 32 | install_python() 33 | ``` 34 | 35 | To install `saspy`, use the following code 36 | 37 | ```{r} 38 | library(sasr) 39 | install_saspy() 40 | ``` 41 | 42 | After the installation completes, you are ready to use `sasr` package. 43 | 44 | ### Short Example 45 | 46 | ```{r} 47 | library(sasr) 48 | df2sd(mtcars, "mt") 49 | result <- run_sas(" 50 | proc freq data = mt; 51 | run; 52 | ") 53 | 54 | cat(result$LOG) 55 | 56 | cat(result$LST) 57 | ``` 58 | 59 | ## FAQ 60 | 61 | Q: Why use `saspy` instead of using `ssh` tunnels? 62 | 63 | A: Although we can use `ssh` tunnels to transfer data and 64 | execute SAS commands, there are many restrictions: it only 65 | supports `ssh` connection. Using `saspy`, the official Python 66 | interface to SAS, we can enable all connection types, without 67 | reinventing the wheel, e.g. we can also connect to a local SAS 68 | installation with the same syntax, or connect to a remote SAS 69 | Viya through `http`. In addition, SAS sessions in `saspy` will 70 | not end until you terminate it (or encounter net work issues), 71 | it will be nice to execute multiple SAS code one by one, not 72 | necessarily putting them in one script and execute the whole 73 | script at once. Also, with the update of `saspy` over time, 74 | `sasr` will be easily extensible, to include functionalities 75 | other than transferring data and executing SAS code. 76 | -------------------------------------------------------------------------------- /_pkgdown.yml: -------------------------------------------------------------------------------- 1 | url: https://insightsengineering.github.io/sasr 2 | 3 | template: 4 | bootstrap: 5 5 | bootswatch: cerulean 6 | includes: 7 | in_header: | 8 | 9 | 10 | 16 | 17 | navbar: 18 | right: 19 | - icon: fa-github 20 | href: https://github.com/insightsengineering/sasr 21 | -------------------------------------------------------------------------------- /design/fev_mmrm.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "sasr in Rmarkdown" 3 | output: "html_document" 4 | --- 5 | 6 | # run sas code 7 | 8 | ```{r} 9 | df2sd(fev_data, "fev") 10 | 11 | a <- run_sas(" 12 | PROC MIXED DATA = fev cl method=reml; 13 | CLASS RACE(ref = 'Asian') AVISIT(ref = 'VIS4') SEX(ref = 'Male') ARMCD(ref = 'PBO') USUBJID; 14 | MODEL FEV1 = ARMCD / ddfm=kr solution chisq; 15 | REPEATED AVISIT / subject=USUBJID type=AR(1) r rcorr; 16 | LSMEANS ARMCD / pdiff=all cl alpha=0.05 slice=AVISIT; 17 | RUN; 18 | ", results = "TEXT") 19 | ``` 20 | 21 | # print html result 22 | 23 | ```{r, results = "asis"} 24 | cat(a$LST) 25 | ``` 26 | -------------------------------------------------------------------------------- /design/fev_mmrm.r: -------------------------------------------------------------------------------- 1 | df2sd(fev_data, "fev") 2 | 3 | a = run_sas(" 4 | PROC MIXED DATA = fev cl method=reml; 5 | CLASS RACE(ref = 'Asian') AVISIT(ref = 'VIS4') SEX(ref = 'Male') ARMCD(ref = 'PBO') USUBJID; 6 | MODEL FEV1 = ARMCD AVISIT ARMCD*AVISIT / ddfm=satterthwaite solution chisq; 7 | REPEATED AVISIT / subject=USUBJID type=un r rcorr; 8 | WEIGHT WEIGHT; 9 | LSMEANS AVISIT*ARMCD / pdiff=all cl alpha=0.05 slice=AVISIT; 10 | RUN; 11 | ") 12 | 13 | cat(a$LST) 14 | -------------------------------------------------------------------------------- /design/test.r: -------------------------------------------------------------------------------- 1 | library(sasr)# devtools::load_all() 2 | 3 | #get_sas_session() will be called automatically; or use `sas <- sas_session_ssh()` manually 4 | 5 | sascode = "proc candisc data=sashelp.iris out=outcan distance anova; 6 | class Species; 7 | var SepalLength SepalWidth PetalLength PetalWidth; 8 | run;" 9 | 10 | a <- run_sas( 11 | sascode, 12 | results = "TEXT" 13 | ) 14 | 15 | b <- run_sas( 16 | sascode, 17 | results = "HTML" 18 | ) 19 | 20 | # renaming column variable to replace . to _ 21 | iris2 <- iris 22 | colnames(iris2) <- stringr::str_replace(colnames(iris2), "\\.", "_") 23 | df2sd(iris2, table = "oros") 24 | 25 | # oros must exist! oros is defined through df2sd 26 | sascode = "proc candisc data=oros out=outcan2 distance anova; 27 | class Species; 28 | var Sepal_Length Sepal_Width Petal_Length Petal_Width; 29 | run;" 30 | 31 | d <- run_sas( 32 | sascode, 33 | results = "TEXT" 34 | ) 35 | 36 | cat(d$LOG) 37 | cat(d$LST) 38 | 39 | -------------------------------------------------------------------------------- /inst/WORDLIST: -------------------------------------------------------------------------------- 1 | Bove 2 | Hoffmann 3 | LST 4 | Reticulate 5 | SASPy 6 | SASdata 7 | Sabanes 8 | Viya 9 | funder 10 | hostname 11 | knitr 12 | onLoad 13 | reticulate 14 | sas 15 | sasdata 16 | saspy 17 | tunnelling 18 | -------------------------------------------------------------------------------- /inst/example.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "My R Markdown Document" 3 | output: html_document 4 | --- 5 | 6 | # Enable SASR 7 | 8 | ```{r} 9 | library(sasr) 10 | ``` 11 | 12 | # Execute SAS 13 | 14 | ```{sas} 15 | data example1; 16 | input x y $ z; 17 | cards; 18 | 6 A 60 19 | 6 A 70 20 | 2 A 100 21 | 2 B 10 22 | 3 B 67 23 | 2 C 81 24 | 3 C 63 25 | 5 C 55 26 | ; 27 | run; 28 | 29 | proc freq data = example1; 30 | tables y; 31 | run; 32 | ``` 33 | -------------------------------------------------------------------------------- /man/df2sd.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/run_sas.R 3 | \name{df2sd} 4 | \alias{df2sd} 5 | \title{Transfer data.frame to SAS} 6 | \usage{ 7 | df2sd(df, table = "_df", libref = "", ..., sas_session = get_sas_session()) 8 | } 9 | \arguments{ 10 | \item{df}{(\code{data.frame})\cr data frame to be transferred.} 11 | 12 | \item{table}{(\code{character})\cr table name in SAS.} 13 | 14 | \item{libref}{(\code{character})\cr library name in SAS.} 15 | 16 | \item{...}{additional arguments for \code{saspy.sasbase.SASsession.df2sd}} 17 | 18 | \item{sas_session}{(\code{saspy.sasbase.SASsession}) SAS session.} 19 | } 20 | \value{ 21 | "saspy.sasdata.SASdata" object. 22 | } 23 | \description{ 24 | \ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#experimental}{\figure{lifecycle-experimental.svg}{options: alt='[Experimental]'}}}{\strong{[Experimental]}} 25 | Transfer \code{data.frame} object from R environment to SAS. 26 | } 27 | -------------------------------------------------------------------------------- /man/dot-onLoad.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/zzz.R 3 | \name{.onLoad} 4 | \alias{.onLoad} 5 | \title{onLoad Function} 6 | \usage{ 7 | .onLoad(libname, pkgname) 8 | } 9 | \description{ 10 | onLoad Function 11 | } 12 | \keyword{internal} 13 | -------------------------------------------------------------------------------- /man/dot-sasr_env.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/zzz.R 3 | \docType{data} 4 | \name{.sasr_env} 5 | \alias{.sasr_env} 6 | \title{sasr Environment} 7 | \format{ 8 | An object of class \code{environment} of length 0. 9 | } 10 | \usage{ 11 | .sasr_env 12 | } 13 | \description{ 14 | sasr Environment 15 | } 16 | \keyword{internal} 17 | -------------------------------------------------------------------------------- /man/figures/sasr-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 新建项目 3 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /man/get_sas_cfg.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils.R 3 | \name{get_sas_cfg} 4 | \alias{get_sas_cfg} 5 | \title{Obtain the SAS Configuration File} 6 | \usage{ 7 | get_sas_cfg() 8 | } 9 | \value{ 10 | The file path of default SAS configuration file, or NULL if not found. 11 | } 12 | \description{ 13 | \ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#experimental}{\figure{lifecycle-experimental.svg}{options: alt='[Experimental]'}}}{\strong{[Experimental]}} 14 | Obtain the file path of the SAS configuration file. 15 | } 16 | \details{ 17 | Obtain the default sas configuration file. By default, it will search 18 | the \code{sascfg_personal.py} file under current directory. If it does not exist, it will 19 | search this file under home directory. If this file does not exist, NULL will be returned. 20 | } 21 | -------------------------------------------------------------------------------- /man/get_sas_session.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils.R 3 | \name{get_sas_session} 4 | \alias{get_sas_session} 5 | \title{Get the Last or Default SAS Session} 6 | \usage{ 7 | get_sas_session() 8 | } 9 | \value{ 10 | A new SAS session if there are no previous SAS session, or the last SAS session created. 11 | } 12 | \description{ 13 | \ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#experimental}{\figure{lifecycle-experimental.svg}{options: alt='[Experimental]'}}}{\strong{[Experimental]}} 14 | Obtain the last session or default session. 15 | } 16 | \details{ 17 | this function is designed to facilitate the R users programming practice 18 | of function oriented programming instead of object oriented programmings. 19 | } 20 | -------------------------------------------------------------------------------- /man/install_saspy.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils.R 3 | \name{install_saspy} 4 | \alias{install_saspy} 5 | \title{Install \code{saspy} Module} 6 | \usage{ 7 | install_saspy(method = "auto", conda = "auto") 8 | } 9 | \arguments{ 10 | \item{method}{(\code{character})\cr method to install \code{saspy}.} 11 | 12 | \item{conda}{(\code{character})\cr path to \code{conda} executable.} 13 | } 14 | \value{ 15 | No return value. 16 | } 17 | \description{ 18 | \ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#experimental}{\figure{lifecycle-experimental.svg}{options: alt='[Experimental]'}}}{\strong{[Experimental]}} 19 | Install \code{saspy} module in \code{reticulate}. 20 | } 21 | -------------------------------------------------------------------------------- /man/run_sas.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/run_sas.R 3 | \name{run_sas} 4 | \alias{run_sas} 5 | \title{Run SAS code with SAS Session} 6 | \usage{ 7 | run_sas(sas_code, results = c("TEXT", "HTML"), sas_session = get_sas_session()) 8 | } 9 | \arguments{ 10 | \item{sas_code}{(\code{character})\cr sas code to be executed.} 11 | 12 | \item{results}{(\code{character})\cr sas code execution results type.} 13 | 14 | \item{sas_session}{(\code{saspy.sasbase.SASsession}) SAS session.} 15 | } 16 | \value{ 17 | Named list with following elements: 18 | \itemize{ 19 | \item \code{LOG}: \code{string} of SAS execution log. 20 | \item \code{LST}: \code{string} of SAS execution result, in html or txt format. 21 | } 22 | } 23 | \description{ 24 | \ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#experimental}{\figure{lifecycle-experimental.svg}{options: alt='[Experimental]'}}}{\strong{[Experimental]}} 25 | Run SAS code with a SAS session. 26 | } 27 | \details{ 28 | \code{run_sas} will run sas code through SAS session. 29 | The results is a named list of \code{LST} and \code{LOG}. 30 | The result part will be stored in \code{LST}, and log will be stored in \code{LOG}. 31 | If \code{results} argument is "TEXT", then results are in text format; 32 | if \code{results} argument is "HTML", then results are in html format. 33 | } 34 | -------------------------------------------------------------------------------- /man/sas_engine.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/sas_engine.R 3 | \name{sas_engine} 4 | \alias{sas_engine} 5 | \title{SAS engine function} 6 | \usage{ 7 | sas_engine(options) 8 | } 9 | \arguments{ 10 | \item{options}{See knitr documentation on engines.} 11 | } 12 | \description{ 13 | SAS engine function 14 | } 15 | -------------------------------------------------------------------------------- /man/sas_session.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils.R 3 | \name{sas_session} 4 | \alias{sas_session} 5 | \title{Create SAS Session Based on Configuration File} 6 | \usage{ 7 | sas_session(sascfg = get_sas_cfg(), ...) 8 | } 9 | \arguments{ 10 | \item{sascfg}{(\code{string})\cr SAS session configuration.} 11 | 12 | \item{...}{additional arguments passed to \code{saspy.SASsession()}. 13 | Can override the configuration file.} 14 | } 15 | \value{ 16 | SAS session. 17 | } 18 | \description{ 19 | \ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#experimental}{\figure{lifecycle-experimental.svg}{options: alt='[Experimental]'}}}{\strong{[Experimental]}} 20 | Create a SAS session. 21 | } 22 | -------------------------------------------------------------------------------- /man/sas_session_ssh.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils.R 3 | \name{sas_session_ssh} 4 | \alias{sas_session_ssh} 5 | \title{Create SAS Session Based on Configuration File} 6 | \usage{ 7 | sas_session_ssh(sascfg = get_sas_cfg(), ...) 8 | } 9 | \arguments{ 10 | \item{sascfg}{(\code{string})\cr SAS session configuration.} 11 | 12 | \item{...}{additional arguments passed to \code{saspy.SASsession()}. 13 | Can override the configuration file.} 14 | } 15 | \value{ 16 | SAS session. 17 | } 18 | \description{ 19 | \ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} 20 | } 21 | -------------------------------------------------------------------------------- /man/sascfg.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/sascfg.R 3 | \name{sascfg} 4 | \alias{sascfg} 5 | \title{Create SAS Session Configuration File} 6 | \usage{ 7 | sascfg( 8 | name = "default", 9 | host, 10 | saspath, 11 | ssh = system("which ssh", intern = TRUE), 12 | encoding = "latin1", 13 | options = list("-fullstimer"), 14 | ..., 15 | sascfg = "sascfg_personal.py" 16 | ) 17 | } 18 | \arguments{ 19 | \item{name}{(\code{character})\cr name of the configuration.} 20 | 21 | \item{host}{(\code{character})\cr host name of remote server.} 22 | 23 | \item{saspath}{(\code{character})\cr SAS executable path on remote server.} 24 | 25 | \item{ssh}{(\code{character})\cr executable path of ssh.} 26 | 27 | \item{encoding}{(\code{character})\cr encoding of the SAS session.} 28 | 29 | \item{options}{(\code{list})\cr additional list of arguments to pass to \code{ssh} command.} 30 | 31 | \item{...}{additional arguments.} 32 | 33 | \item{sascfg}{(\code{character})\cr target file of configuration.} 34 | } 35 | \value{ 36 | No return value. 37 | } 38 | \description{ 39 | \ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#experimental}{\figure{lifecycle-experimental.svg}{options: alt='[Experimental]'}}}{\strong{[Experimental]}} 40 | Create SAS session configuration file based on argument. 41 | } 42 | \details{ 43 | \code{host} and \code{saspath} are required to connect to remote SAS server. Other arguments can follow default. 44 | If transferring datasets is needed and the client(running sasr) is not reachable from the server, 45 | then tunnelling is required. 46 | Use \verb{tunnel = }, \verb{rtunnel = } to specify tunnels and reverse tunnels. 47 | The values should be length 1 integer. 48 | } 49 | -------------------------------------------------------------------------------- /man/saspy.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/zzz.R 3 | \docType{data} 4 | \name{saspy} 5 | \alias{saspy} 6 | \title{saspy package} 7 | \format{ 8 | An object of class \code{python.builtin.module} (inherits from \code{python.builtin.object}) of length 0. 9 | } 10 | \usage{ 11 | saspy 12 | } 13 | \description{ 14 | saspy package 15 | } 16 | \keyword{internal} 17 | -------------------------------------------------------------------------------- /man/sasr-package.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/sasr-package.R 3 | \docType{package} 4 | \name{sasr-package} 5 | \alias{sasr} 6 | \alias{sasr-package} 7 | \title{\code{sasr} Package} 8 | \description{ 9 | \code{sasr} provides interface to SAS through \code{saspy} and \code{reticulate} in R. 10 | } 11 | \seealso{ 12 | Useful links: 13 | \itemize{ 14 | \item \url{https://github.com/insightsengineering/sasr/} 15 | \item \url{https://insightsengineering.github.io/sasr/latest-tag/} 16 | \item Report bugs at \url{https://github.com/insightsengineering/sasr/issues} 17 | } 18 | 19 | } 20 | \author{ 21 | \strong{Maintainer}: Liming Li \email{clark.liming@gmail.com} 22 | 23 | Authors: 24 | \itemize{ 25 | \item Daniel Sabanes Bove \email{daniel.sabanesbove@gmail.com} 26 | \item Isaac Gravestock \email{isaac.gravestock@roche.com} 27 | } 28 | 29 | Other contributors: 30 | \itemize{ 31 | \item F. Hoffmann-La Roche AG [copyright holder, funder] 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /man/sd2df.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/run_sas.R 3 | \name{sd2df} 4 | \alias{sd2df} 5 | \title{Transfer SAS Data to R} 6 | \usage{ 7 | sd2df(table, libref = "", ..., sas_session = get_sas_session()) 8 | } 9 | \arguments{ 10 | \item{table}{(\code{character})\cr table name in SAS.} 11 | 12 | \item{libref}{(\code{character})\cr library name in SAS.} 13 | 14 | \item{...}{additional arguments for \code{saspy.sasbase.SASsession.sd2df}} 15 | 16 | \item{sas_session}{(\code{saspy.sasbase.SASsession}) SAS session.} 17 | } 18 | \value{ 19 | \code{data.frame} object. 20 | } 21 | \description{ 22 | \ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#experimental}{\figure{lifecycle-experimental.svg}{options: alt='[Experimental]'}}}{\strong{[Experimental]}} 23 | Transfer the table in SAS session to R. 24 | } 25 | -------------------------------------------------------------------------------- /man/validate_data.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils.R 3 | \name{validate_data} 4 | \alias{validate_data} 5 | \title{Validate and Process \code{data.frame} for SAS} 6 | \usage{ 7 | validate_data(data) 8 | } 9 | \arguments{ 10 | \item{data}{(\code{data.frame})\cr data.frame to be checked.} 11 | } 12 | \value{ 13 | \code{data.frame}\cr 14 | } 15 | \description{ 16 | \ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#experimental}{\figure{lifecycle-experimental.svg}{options: alt='[Experimental]'}}}{\strong{[Experimental]}} 17 | Validate if data contains validate variable names in SAS, and 18 | remove possible row names. 19 | } 20 | \details{ 21 | In SAS, the variable names should be consist of letters, numbers and underscore. 22 | Other characters are not allowed. 23 | In addition, in SAS, row names(index) are not allowed. 24 | } 25 | \keyword{internal} 26 | -------------------------------------------------------------------------------- /man/validate_sascfg.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils.R 3 | \name{validate_sascfg} 4 | \alias{validate_sascfg} 5 | \title{Validate SAS Configuration File Exist} 6 | \usage{ 7 | validate_sascfg(sascfg) 8 | } 9 | \arguments{ 10 | \item{sascfg}{(\code{character})\cr file path of configuration.} 11 | } 12 | \description{ 13 | \ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#experimental}{\figure{lifecycle-experimental.svg}{options: alt='[Experimental]'}}}{\strong{[Experimental]}} 14 | Validate if SAS configuration file exist. 15 | } 16 | \details{ 17 | Currently, only the file existence check is conducted and the rest 18 | is checked at python side. 19 | } 20 | \keyword{internal} 21 | -------------------------------------------------------------------------------- /revdep/.gitignore: -------------------------------------------------------------------------------- 1 | checks 2 | library 3 | checks.noindex 4 | library.noindex 5 | cloud.noindex 6 | data.sqlite 7 | *.html 8 | -------------------------------------------------------------------------------- /sasr.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: No 4 | SaveWorkspace: No 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 | 18 | BuildType: Package 19 | PackageUseDevtools: Yes 20 | PackageInstallArgs: --no-multiarch --with-keep.source 21 | PackageRoxygenize: rd,collate,namespace 22 | -------------------------------------------------------------------------------- /staged_dependencies.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Information about this file: https://github.com/openpharma/staged.dependencies 3 | current_repo: 4 | repo: insightsengineering/sasr 5 | host: https://github.com 6 | upstream_repos: 7 | downstream_repos: 8 | -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | # This file is part of the standard setup for testthat. 2 | # It is recommended that you do not modify it. 3 | # 4 | # Where should you do additional test configuration? 5 | # Learn more about the roles of various files in: 6 | # * https://r-pkgs.org/tests.html 7 | # * https://testthat.r-lib.org/reference/test_package.html#special-files 8 | 9 | library(testthat) 10 | library(sasr) 11 | 12 | test_check("sasr") 13 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/rmarkdown.md: -------------------------------------------------------------------------------- 1 | # rmarkdown engine works 2 | 3 | Code 4 | rmarkdown::render(system.file("example.Rmd", package = "sasr"), quiet = TRUE) 5 | Output 6 | submit the following code: 7 | data example1; 8 | input x y $ z; 9 | cards; 10 | 6 A 60 11 | 6 A 70 12 | 2 A 100 13 | 2 B 10 14 | 3 B 67 15 | 2 C 81 16 | 3 C 63 17 | 5 C 55 18 | ; 19 | run; 20 | 21 | proc freq data = example1; 22 | tables y; 23 | run; 24 | format of result is HTML 25 | 26 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/run_sas.md: -------------------------------------------------------------------------------- 1 | # run_sas call the correct method 2 | 3 | Code 4 | run_sas("this is test", sas_session = dummy_session) 5 | Output 6 | submit the following code: 7 | this is test 8 | format of result is TEXT 9 | 10 | # df2sd call the correct method 11 | 12 | Code 13 | df2sd(iris2, "iris2", "work", sas_session = dummy_session) 14 | Output 15 | submit data into SAS work.iris2 16 | 17 | # sd2df call the correct method 18 | 19 | Code 20 | sd2df("iris2", "work", sas_session = dummy_session) 21 | Output 22 | obtain SAS dataset work.iris2 23 | 24 | -------------------------------------------------------------------------------- /tests/testthat/helper-dummy_session.R: -------------------------------------------------------------------------------- 1 | dummy_session <- list( 2 | submit = function(code, results, ...) { 3 | cat("submit the following code: \n") 4 | cat(code) 5 | cat("\nformat of result is ", results, "\n") 6 | }, 7 | df2sd = function(df, table, libref, ...) { 8 | df_call <- substitute(df, env = parent.frame()) 9 | cat(sprintf("submit data into SAS %s.%s\n", libref, table)) 10 | }, 11 | sd2df = function(table, libref, ...) { 12 | cat(sprintf("obtain SAS dataset %s.%s\n", libref, table)) 13 | }, 14 | sascfg = list(name = "a", mode = "ssh", SAScfg = list(a = list(tunnel = 123L, rtunnel = 321L))) 15 | ) 16 | 17 | iris2 <- iris 18 | colnames(iris2) <- gsub("\\.", "_", colnames(iris2)) 19 | -------------------------------------------------------------------------------- /tests/testthat/test-rmarkdown.R: -------------------------------------------------------------------------------- 1 | # knitr engin ---- 2 | test_that("rmarkdown engine works", { 3 | .sasr_env$.sas_session <- dummy_session 4 | expect_snapshot( 5 | rmarkdown::render(system.file("example.Rmd", package = "sasr"), quiet = TRUE, output_dir = tempdir()), 6 | ) 7 | }) 8 | -------------------------------------------------------------------------------- /tests/testthat/test-run_sas.R: -------------------------------------------------------------------------------- 1 | # run_sas ---- 2 | 3 | test_that("run_sas call the correct method", { 4 | expect_snapshot(run_sas("this is test", sas_session = dummy_session)) 5 | }) 6 | 7 | # df2sd ---- 8 | test_that("df2sd call the correct method", { 9 | expect_snapshot(df2sd(iris2, "iris2", "work", sas_session = dummy_session)) 10 | }) 11 | 12 | # sd2df ---- 13 | 14 | test_that("sd2df call the correct method", { 15 | expect_snapshot(sd2df("iris2", "work", sas_session = dummy_session)) 16 | }) 17 | -------------------------------------------------------------------------------- /tests/testthat/test-sascfg.R: -------------------------------------------------------------------------------- 1 | # sascfg ---- 2 | 3 | test_that("sascfg creates sas configuration file", { 4 | skip_if_not(py_available(TRUE)) 5 | tmpf <- tempfile() 6 | sascfg(ssh = "ssh", saspath = "sas", host = "test.com", sascfg = tmpf) 7 | on.exit(unlink(tmpf)) 8 | expect_file_exists(tmpf) 9 | }) 10 | -------------------------------------------------------------------------------- /tests/testthat/test-session.R: -------------------------------------------------------------------------------- 1 | # get_sas_session ---- 2 | 3 | test_that("get_sas_session returns the .sas_session", { 4 | .sasr_env$.sas_session <- "test" # not a real session 5 | expect_identical(.sasr_env$.sas_session, get_sas_session()) 6 | }) 7 | -------------------------------------------------------------------------------- /tests/testthat/test-utils.R: -------------------------------------------------------------------------------- 1 | # validate_data ---- 2 | 3 | test_that("validate_data will give errors if . exist", { 4 | df <- data.frame( 5 | a.1 = 1 6 | ) 7 | df2 <- data.frame( 8 | a = 1, 9 | b = 2 10 | ) 11 | expect_error(validate_data(df), "a\\.1 contains illegal characters that is not allowed\\.") 12 | expect_silent(validate_data(df2)) 13 | }) 14 | 15 | test_that("validate_data drop row names and give warnings", { 16 | df <- data.frame( 17 | a = 1, 18 | b = 2 19 | ) 20 | row.names(df) <- "test" 21 | expect_warning( 22 | { 23 | df2 <- validate_data(df) 24 | }, 25 | "row\\.names is not supported in SAS and will be dropped" 26 | ) 27 | expect_identical(row.names(df2), "1") 28 | }) 29 | 30 | # validate_sascfg ---- 31 | 32 | test_that("validate_sascfg works if file exists", { 33 | tmp <- tempfile() 34 | s <- file.create(tmp) 35 | on.exit(file.remove(tmp)) 36 | expect_silent(validate_sascfg(tmp)) 37 | }) 38 | 39 | test_that("validate_sascfg warns if given NULL", { 40 | expect_warning(validate_sascfg(NULL), "No SAS configuration file specified.") 41 | }) 42 | 43 | test_that("validate_sascfg errors if given non-existing file", { 44 | tmp <- tempfile() 45 | expect_error(validate_sascfg(tmp), "must exist to establish a connection") 46 | }) 47 | 48 | # get_sas_cfg ---- 49 | 50 | test_that("get_sas_cfg works as expected", { 51 | if (!file.exists("sascfg_personal.py")) { 52 | file.create("sascfg_personal.py") 53 | on.exit(file.remove("sascfg_personal.py")) 54 | } 55 | options(sascfg = NULL) 56 | expect_identical(get_sas_cfg(), "sascfg_personal.py") 57 | 58 | skip_on_cran() 59 | skip_on_ci() 60 | tmp <- tempfile(tmpdir = "~") 61 | tmp_name <- basename(tmp) 62 | if (!file.exists(tmp)) { 63 | file.create(tmp) 64 | on.exit(file.remove(tmp)) 65 | } 66 | options(sascfg = tmp_name) 67 | s <- file.create(tmp) 68 | on.exit(file.remove(tmp)) 69 | expect_identical(get_sas_cfg(), tmp) 70 | }) 71 | 72 | # install_saspy ---- 73 | 74 | test_that("install_saspy works", { 75 | skip_if_not_installed("mockery") 76 | mockery::stub(install_saspy, "askYesNo", function(...) TRUE) 77 | mockery::stub(install_saspy, "reticulate::py_install", function(...) TRUE) 78 | expect_identical( 79 | install_saspy(), 80 | TRUE 81 | ) 82 | 83 | mockery::stub(install_saspy, "askYesNo", function(...) FALSE) 84 | expect_error( 85 | install_saspy(), 86 | "Installation of saspy cancelled" 87 | ) 88 | }) 89 | 90 | # get_sas_session ---- 91 | 92 | test_that("get_sas_session works", { 93 | skip_if_not_installed("mockery") 94 | .sasr_env$.sas_session <- NULL 95 | mockery::stub(get_sas_session, "sas_session", function(...) TRUE) 96 | expect_true( 97 | get_sas_session() 98 | ) 99 | .sasr_env$.sas_session <- NULL 100 | mockery::stub(get_sas_session, "sas_session", function(...) NULL) 101 | expect_error( 102 | get_sas_session(), 103 | "SAS session not established" 104 | ) 105 | .sasr_env$.sas_session <- NULL 106 | }) 107 | 108 | # sas_session ---- 109 | 110 | test_that("sas_session works", { 111 | skip_if_not_installed("mockery") 112 | mockery::stub(sas_session, "saspy$SASsession", function(...) TRUE) 113 | mockery::stub(sas_session, "validate_sascfg", function(...) TRUE) 114 | expect_true(sas_session("test")) 115 | }) 116 | 117 | # get_sas_cfg ---- 118 | 119 | test_that("get_sas_cfg works", { 120 | options("sascfg" = "non_existing_file") 121 | on.exit({ 122 | options(sascfg = NULL) 123 | }) 124 | expect_null(get_sas_cfg()) 125 | }) 126 | -------------------------------------------------------------------------------- /vignettes/introduction.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Introduction to `sasr`" 3 | package: sasr 4 | output: 5 | rmarkdown::html_document: 6 | theme: "spacelab" 7 | highlight: "kate" 8 | toc: true 9 | toc_float: true 10 | vignette: | 11 | %\VignetteIndexEntry{Introduction to `sasr`} 12 | %\VignetteEncoding{UTF-8} 13 | %\VignetteEngine{knitr::rmarkdown} 14 | editor_options: 15 | chunk_output_type: console 16 | --- 17 | 18 | 19 | ## Introduction to `sasr` 20 | 21 | `sasr` is a package to provide `SAS` interface in R, with [`saspy`](https://sassoftware.github.io/saspy/) and [`reticulate`](https://rstudio.github.io/reticulate/) as backend. 22 | 23 | ## Documentations 24 | 25 | For functionality wrapped in `sasr`, you can find the documentations through R documentation system, or through online [documentation page](https://insightsengineering.github.io/sasr/latest-tag/). 26 | However, there can be some other arguments not documented(in `...`), and these arguments are described in `saspy` [documentation page](https://sassoftware.github.io/saspy/). 27 | 28 | ## Short Tutorial 29 | 30 | To use `sasr`, you need to follow these steps 31 | 32 | 1. Configure your SAS server in `sascfg_personal.py` under your working directory or the home directory. This is the default file that `sasr` will look at. However, you can still change that through `options(sascfg = )`, then `sasr` will try to find any name that is available in your specified option. 33 | 1. If you don't know how to create this file, use `sascfg()` to create the file. Required arguments include `host` and `saspath`. 34 | 1. `sascfg()` only creates ssh based SAS session. 35 | 1. Only password-less ssh connection is supported, e.g. ssh via public keys. 36 | 1. `host` is the hostname of the SAS server. 37 | 1. `saspath` is the SAS executable path on the SAS server. 38 | 1. Other arguments are added to the configuration file directly. 39 | 1. `tunnel` and `rtunnel` are required if you want to transfer datasets between R and SAS if the client (running sasr) is not reachable from the server. Use integers like `tunnel = 9999L` in R, or modify `sascfg_personal.py` to make sure they are integers. 40 | 1. You can create the configuration by yourself and then SAS connection will not be restricted to ssh. 41 | 1. You can have multiple configuration files with different file names 42 | 1. Create the SAS session based on the configuration file 43 | 1. To use the default connection specified in the configuration file, you can run any command like `run_sas`, `df2sd` or `sd2df`. 44 | 1. The session will be created if there is no session available stored in `.sasr_env$.sas_session` 45 | 1. If `.sasr_env$.sas_session` is created, this session will be used by default. 46 | 1. Do not create any variable called `.sas_session` in environment `sasr:::.sasr_env` 47 | 1. To create the session manually, you can call `sas_session()` 48 | 1. `SAS_session` have one argument `sascfg`, pointing to the SAS session configuration file. 49 | 1. To use multiple sessions, you need to store the session `your_session <- sas_session(sascfg)` 50 | 1. Transfer the datasets from R to SAS using `df2sd` 51 | 1. Tunneling must be enabled to transfer datasets. 52 | 1. The variable names of the datasets should not contain dots otherwise SAS may not recognize. 53 | 1. The index (row names) will not be transferred to SAS. 54 | 1. Use `run_sas` to submit SAS code to the SAS server. 55 | 1. The returned value is a named list, `LST` is the result and `LOG` is the log file 56 | 1. `run_sas` has argument `results=`, it can be either "TEXT" or "HTML". This argument decides the LST format. 57 | 1. Transfer SAS datasets back to R use `sd2df` 58 | --------------------------------------------------------------------------------