From cc5a9272af85889397ad64150cd52b2be41ca7c8 Mon Sep 17 00:00:00 2001 From: Lorenzo Copelli Date: Tue, 7 Apr 2026 13:41:05 +0200 Subject: [PATCH 1/5] Project import. (#1) * First commit (project import). * Added GitHub workflows. --- .Rbuildignore | 21 + .Rprofile | 1 + .github/workflows/codecov.yml | 30 + .github/workflows/pkgdown.yml | 41 + .gitignore | 5 + DESCRIPTION | 61 + LICENSE | 287 ++++ NAMESPACE | 28 + R/buildAuthenticationRequest.R | 61 + R/buildServiceRequest.R | 79 ++ R/distilleR-package.R | 6 + R/getAuthenticationToken.R | 69 + R/getProjects.R | 71 + R/getReport.R | 146 ++ R/getReports.R | 81 ++ R/handleHTTPErrors.R | 40 + R/parseCSVResponse.R | 59 + R/parseJSONResponse.R | 49 + R/parseXLSXResponse.R | 52 + R/performRequest.R | 44 + R/sleep.R | 22 + README.md | 53 +- _pkgdown.yml | 8 + distilleR.Rproj | 19 + man/distilleR-package.Rd | 33 + man/figures/logo.png | Bin 0 -> 51808 bytes man/getAuthenticationToken.Rd | 53 + man/getProjects.Rd | 50 + man/getReport.Rd | 83 ++ man/getReports.Rd | 59 + pkgdown/favicon/apple-touch-icon.png | Bin 0 -> 9171 bytes pkgdown/favicon/favicon-96x96.png | Bin 0 -> 3963 bytes pkgdown/favicon/favicon.ico | Bin 0 -> 15086 bytes pkgdown/favicon/favicon.svg | 1 + pkgdown/favicon/site.webmanifest | 21 + pkgdown/favicon/web-app-manifest-192x192.png | Bin 0 -> 9976 bytes pkgdown/favicon/web-app-manifest-512x512.png | Bin 0 -> 35302 bytes renv.lock | 487 +++++++ renv/.gitignore | 7 + renv/activate.R | 1180 +++++++++++++++++ renv/settings.json | 19 + tests/testthat.R | 12 + .../test-buildAuthenticationRequest.R | 29 + tests/testthat/test-buildServiceRequest.R | 38 + tests/testthat/test-getAuthenticationToken.R | 95 ++ tests/testthat/test-getProjects.R | 106 ++ tests/testthat/test-getReport.R | 362 +++++ tests/testthat/test-getReports.R | 131 ++ tests/testthat/test-handleHTTPErrors.R | 52 + tests/testthat/test-parseCSVResponse.R | 48 + tests/testthat/test-parseJSONResponse.R | 38 + tests/testthat/test-parseXLSXResponse.R | 53 + tests/testthat/test-performRequest.R | 92 ++ tests/testthat/test-sleep.R | 9 + vignettes/.gitignore | 2 + vignettes/distilleR.Rmd | 174 +++ 56 files changed, 4565 insertions(+), 2 deletions(-) create mode 100644 .Rbuildignore create mode 100644 .Rprofile create mode 100644 .github/workflows/codecov.yml create mode 100644 .github/workflows/pkgdown.yml create mode 100644 .gitignore create mode 100644 DESCRIPTION create mode 100644 LICENSE create mode 100644 NAMESPACE create mode 100644 R/buildAuthenticationRequest.R create mode 100644 R/buildServiceRequest.R create mode 100644 R/distilleR-package.R create mode 100644 R/getAuthenticationToken.R create mode 100644 R/getProjects.R create mode 100644 R/getReport.R create mode 100644 R/getReports.R create mode 100644 R/handleHTTPErrors.R create mode 100644 R/parseCSVResponse.R create mode 100644 R/parseJSONResponse.R create mode 100644 R/parseXLSXResponse.R create mode 100644 R/performRequest.R create mode 100644 R/sleep.R create mode 100644 _pkgdown.yml create mode 100644 distilleR.Rproj create mode 100644 man/distilleR-package.Rd create mode 100644 man/figures/logo.png create mode 100644 man/getAuthenticationToken.Rd create mode 100644 man/getProjects.Rd create mode 100644 man/getReport.Rd create mode 100644 man/getReports.Rd create mode 100644 pkgdown/favicon/apple-touch-icon.png create mode 100644 pkgdown/favicon/favicon-96x96.png create mode 100644 pkgdown/favicon/favicon.ico create mode 100644 pkgdown/favicon/favicon.svg create mode 100644 pkgdown/favicon/site.webmanifest create mode 100644 pkgdown/favicon/web-app-manifest-192x192.png create mode 100644 pkgdown/favicon/web-app-manifest-512x512.png create mode 100644 renv.lock create mode 100644 renv/.gitignore create mode 100644 renv/activate.R create mode 100644 renv/settings.json create mode 100644 tests/testthat.R create mode 100644 tests/testthat/test-buildAuthenticationRequest.R create mode 100644 tests/testthat/test-buildServiceRequest.R create mode 100644 tests/testthat/test-getAuthenticationToken.R create mode 100644 tests/testthat/test-getProjects.R create mode 100644 tests/testthat/test-getReport.R create mode 100644 tests/testthat/test-getReports.R create mode 100644 tests/testthat/test-handleHTTPErrors.R create mode 100644 tests/testthat/test-parseCSVResponse.R create mode 100644 tests/testthat/test-parseJSONResponse.R create mode 100644 tests/testthat/test-parseXLSXResponse.R create mode 100644 tests/testthat/test-performRequest.R create mode 100644 tests/testthat/test-sleep.R create mode 100644 vignettes/.gitignore create mode 100644 vignettes/distilleR.Rmd diff --git a/.Rbuildignore b/.Rbuildignore new file mode 100644 index 0000000..c50f4b3 --- /dev/null +++ b/.Rbuildignore @@ -0,0 +1,21 @@ +^renv$ +^renv\.lock$ +^.*\.Rproj$ +^\.Rproj\.user$ +^doc$ +^Meta$ +^_pkgdown\.yml$ +^docs$ +^pkgdown$ +^\.travis\.yml$ +^travis-tool\.sh\.cmd$ +^README\.Rmd$ +^README\.md$ +^codecov\.yml$ +^cran-comments\.md$ +^\.github$ +^\.github\/$ +^.github$ +.github +^CRAN-RELEASE$ +^LICENSE\.md$ diff --git a/.Rprofile b/.Rprofile new file mode 100644 index 0000000..81b960f --- /dev/null +++ b/.Rprofile @@ -0,0 +1 @@ +source("renv/activate.R") diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml new file mode 100644 index 0000000..de56e41 --- /dev/null +++ b/.github/workflows/codecov.yml @@ -0,0 +1,30 @@ +name: Run tests and upload coverage report to Codecov + +on: + push: + branches: [dev, main] + +jobs: + coverage: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up R + uses: r-lib/actions/setup-r@v2 + with: + use-public-rspm: true + + - name: Install R dependencies + run: | + Rscript -e "install.packages(c('covr', 'devtools', 'remotes'))" + Rscript -e "devtools::install_deps(dependencies = TRUE)" + + - name: Run tests and upload coverage to Codecov + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + NOT_CRAN: "false" + run: | + Rscript -e "covr::codecov(token = Sys.getenv('CODECOV_TOKEN'))" diff --git a/.github/workflows/pkgdown.yml b/.github/workflows/pkgdown.yml new file mode 100644 index 0000000..adcf05a --- /dev/null +++ b/.github/workflows/pkgdown.yml @@ -0,0 +1,41 @@ +name: Build and deploy pkgdown site to GitHub Pages + +on: + push: + branches: [main] + +jobs: + pkgdown: + runs-on: ubuntu-latest + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + permissions: + contents: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Pandoc + uses: r-lib/actions/setup-pandoc@v2 + + - name: Set up R + uses: r-lib/actions/setup-r@v2 + with: + use-public-rspm: true + + - name: Install R dependencies + run: | + Rscript -e "install.packages(c('pkgdown', 'devtools', 'remotes'))" + Rscript -e "devtools::install_deps(dependencies = TRUE)" + Rscript -e "devtools::install_local('.')" + + - name: Build pkgdown site + run: pkgdown::build_site_github_pages(new_process = FALSE, install = FALSE) + shell: Rscript {0} + + - name: Deploy site to gh-pages branch + uses: JamesIves/github-pages-deploy-action@v4 + with: + clean: false + branch: gh-pages + folder: docs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..234f028 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.Rproj.user +.Rhistory +.RData +.Ruserdata +docs diff --git a/DESCRIPTION b/DESCRIPTION new file mode 100644 index 0000000..873bcce --- /dev/null +++ b/DESCRIPTION @@ -0,0 +1,61 @@ +Package: distilleR +Type: Package +Title: A wrap around the DistillerSR APIs +Version: 1.0.0 +Maintainer: Luca Belmonte +Authors@R: + c(person(given = "Lorenzo", + family = "Copelli", + role = "aut", + comment = c(ORCID = "0009-0002-4305-065X")), + person(given = "Fulvio", + family = "Barizzone", + role = "aut"), + person(given = "Dayana Stephanie", + family = "Buzle", + role = "aut", + comment = c(ORCID = "0009-0003-2990-7431")), + person(given = "Rafael", + family = "Vieira", + role = "aut", + comment = c(ORCID = "0009-0009-0289-5438")), + person(given = "Luca", + family = "Belmonte", + role = c("aut", "cre"), + email = "luca.belmonte@efsa.europa.eu", + comment = c(ORCID = "0000-0002-7977-9170"))) +Description: The distilleR package provides a pool of functions to query + DistillerSR through its APIs. It features authentication and utilities to + retrieve data from DistillerSR projects and reports. +License: file LICENSE +URL: https://openefsa.github.io/distilleR +BugReports: https://github.com/openefsa/distilleR/issues +Depends: + R (>= 4.0.0) +Imports: + cli (>= 3.6.5), + checkmate (>= 2.3.1), + glue (>= 1.7.0), + httr2 (>= 1.2.1), + jsonlite (>= 1.8.7), + readr (>= 2.1.5), + readxl (>= 1.4.3), + tibble (>= 3.3.0) +Suggests: + devtools (>= 2.4.5), + knitr (>= 1.0), + rmarkdown (>= 2.0), + roxygen2 (>= 7.2.1), + testthat (>= 3.0.0), + usethis (>= 2.2.3), + covr (>= 3.6.4), + openxlsx (>= 4.2.8), + rlang (>= 1.1.4) +Encoding: UTF-8 +LazyData: true +RoxygenNote: 7.3.3 +Config/testthat/edition: 3 +VignetteBuilder: knitr +Config/Needs/website: pkgdown +Repository: CRAN +Roxygen: list(markdown = TRUE) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c29ce2f --- /dev/null +++ b/LICENSE @@ -0,0 +1,287 @@ + EUROPEAN UNION PUBLIC LICENCE v. 1.2 + EUPL © the European Union 2007, 2016 + +This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined +below) which is provided under the terms of this Licence. Any use of the Work, +other than as authorised under this Licence is prohibited (to the extent such +use is covered by a right of the copyright holder of the Work). + +The Work is provided under the terms of this Licence when the Licensor (as +defined below) has placed the following notice immediately following the +copyright notice for the Work: + + Licensed under the EUPL + +or has expressed by any other means his willingness to license under the EUPL. + +1. Definitions + +In this Licence, the following terms have the following meaning: + +- ‘The Licence’: this Licence. + +- ‘The Original Work’: the work or software distributed or communicated by the + Licensor under this Licence, available as Source Code and also as Executable + Code as the case may be. + +- ‘Derivative Works’: the works or software that could be created by the + Licensee, based upon the Original Work or modifications thereof. This Licence + does not define the extent of modification or dependence on the Original Work + required in order to classify a work as a Derivative Work; this extent is + determined by copyright law applicable in the country mentioned in Article 15. + +- ‘The Work’: the Original Work or its Derivative Works. + +- ‘The Source Code’: the human-readable form of the Work which is the most + convenient for people to study and modify. + +- ‘The Executable Code’: any code which has generally been compiled and which is + meant to be interpreted by a computer as a program. + +- ‘The Licensor’: the natural or legal person that distributes or communicates + the Work under the Licence. + +- ‘Contributor(s)’: any natural or legal person who modifies the Work under the + Licence, or otherwise contributes to the creation of a Derivative Work. + +- ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of + the Work under the terms of the Licence. + +- ‘Distribution’ or ‘Communication’: any act of selling, giving, lending, + renting, distributing, communicating, transmitting, or otherwise making + available, online or offline, copies of the Work or providing access to its + essential functionalities at the disposal of any other natural or legal + person. + +2. Scope of the rights granted by the Licence + +The Licensor hereby grants You a worldwide, royalty-free, non-exclusive, +sublicensable licence to do the following, for the duration of copyright vested +in the Original Work: + +- use the Work in any circumstance and for all usage, +- reproduce the Work, +- modify the Work, and make Derivative Works based upon the Work, +- communicate to the public, including the right to make available or display + the Work or copies thereof to the public and perform publicly, as the case may + be, the Work, +- distribute the Work or copies thereof, +- lend and rent the Work or copies thereof, +- sublicense rights in the Work or copies thereof. + +Those rights can be exercised on any media, supports and formats, whether now +known or later invented, as far as the applicable law permits so. + +In the countries where moral rights apply, the Licensor waives his right to +exercise his moral right to the extent allowed by law in order to make effective +the licence of the economic rights here above listed. + +The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to +any patents held by the Licensor, to the extent necessary to make use of the +rights granted on the Work under this Licence. + +3. Communication of the Source Code + +The Licensor may provide the Work either in its Source Code form, or as +Executable Code. If the Work is provided as Executable Code, the Licensor +provides in addition a machine-readable copy of the Source Code of the Work +along with each copy of the Work that the Licensor distributes or indicates, in +a notice following the copyright notice attached to the Work, a repository where +the Source Code is easily and freely accessible for as long as the Licensor +continues to distribute or communicate the Work. + +4. Limitations on copyright + +Nothing in this Licence is intended to deprive the Licensee of the benefits from +any exception or limitation to the exclusive rights of the rights owners in the +Work, of the exhaustion of those rights or of other applicable limitations +thereto. + +5. Obligations of the Licensee + +The grant of the rights mentioned above is subject to some restrictions and +obligations imposed on the Licensee. Those obligations are the following: + +Attribution right: The Licensee shall keep intact all copyright, patent or +trademarks notices and all notices that refer to the Licence and to the +disclaimer of warranties. The Licensee must include a copy of such notices and a +copy of the Licence with every copy of the Work he/she distributes or +communicates. The Licensee must cause any Derivative Work to carry prominent +notices stating that the Work has been modified and the date of modification. + +Copyleft clause: If the Licensee distributes or communicates copies of the +Original Works or Derivative Works, this Distribution or Communication will be +done under the terms of this Licence or of a later version of this Licence +unless the Original Work is expressly distributed only under this version of the +Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee +(becoming Licensor) cannot offer or impose any additional terms or conditions on +the Work or Derivative Work that alter or restrict the terms of the Licence. + +Compatibility clause: If the Licensee Distributes or Communicates Derivative +Works or copies thereof based upon both the Work and another work licensed under +a Compatible Licence, this Distribution or Communication can be done under the +terms of this Compatible Licence. For the sake of this clause, ‘Compatible +Licence’ refers to the licences listed in the appendix attached to this Licence. +Should the Licensee's obligations under the Compatible Licence conflict with +his/her obligations under this Licence, the obligations of the Compatible +Licence shall prevail. + +Provision of Source Code: When distributing or communicating copies of the Work, +the Licensee will provide a machine-readable copy of the Source Code or indicate +a repository where this Source will be easily and freely available for as long +as the Licensee continues to distribute or communicate the Work. + +Legal Protection: This Licence does not grant permission to use the trade names, +trademarks, service marks, or names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the copyright notice. + +6. Chain of Authorship + +The original Licensor warrants that the copyright in the Original Work granted +hereunder is owned by him/her or licensed to him/her and that he/she has the +power and authority to grant the Licence. + +Each Contributor warrants that the copyright in the modifications he/she brings +to the Work are owned by him/her or licensed to him/her and that he/she has the +power and authority to grant the Licence. + +Each time You accept the Licence, the original Licensor and subsequent +Contributors grant You a licence to their contributions to the Work, under the +terms of this Licence. + +7. Disclaimer of Warranty + +The Work is a work in progress, which is continuously improved by numerous +Contributors. It is not a finished work and may therefore contain defects or +‘bugs’ inherent to this type of development. + +For the above reason, the Work is provided under the Licence on an ‘as is’ basis +and without warranties of any kind concerning the Work, including without +limitation merchantability, fitness for a particular purpose, absence of defects +or errors, accuracy, non-infringement of intellectual property rights other than +copyright as stated in Article 6 of this Licence. + +This disclaimer of warranty is an essential part of the Licence and a condition +for the grant of any rights to the Work. + +8. Disclaimer of Liability + +Except in the cases of wilful misconduct or damages directly caused to natural +persons, the Licensor will in no event be liable for any direct or indirect, +material or moral, damages of any kind, arising out of the Licence or of the use +of the Work, including without limitation, damages for loss of goodwill, work +stoppage, computer failure or malfunction, loss of data or any commercial +damage, even if the Licensor has been advised of the possibility of such damage. +However, the Licensor will be liable under statutory product liability laws as +far such laws apply to the Work. + +9. Additional agreements + +While distributing the Work, You may choose to conclude an additional agreement, +defining obligations or services consistent with this Licence. However, if +accepting obligations, You may act only on your own behalf and on your sole +responsibility, not on behalf of the original Licensor or any other Contributor, +and only if You agree to indemnify, defend, and hold each Contributor harmless +for any liability incurred by, or claims asserted against such Contributor by +the fact You have accepted any warranty or additional liability. + +10. Acceptance of the Licence + +The provisions of this Licence can be accepted by clicking on an icon ‘I agree’ +placed under the bottom of a window displaying the text of this Licence or by +affirming consent in any other similar way, in accordance with the rules of +applicable law. Clicking on that icon indicates your clear and irrevocable +acceptance of this Licence and all of its terms and conditions. + +Similarly, you irrevocably accept this Licence and all of its terms and +conditions by exercising any rights granted to You by Article 2 of this Licence, +such as the use of the Work, the creation by You of a Derivative Work or the +Distribution or Communication by You of the Work or copies thereof. + +11. Information to the public + +In case of any Distribution or Communication of the Work by means of electronic +communication by You (for example, by offering to download the Work from a +remote location) the distribution channel or media (for example, a website) must +at least provide to the public the information requested by the applicable law +regarding the Licensor, the Licence and the way it may be accessible, concluded, +stored and reproduced by the Licensee. + +12. Termination of the Licence + +The Licence and the rights granted hereunder will terminate automatically upon +any breach by the Licensee of the terms of the Licence. + +Such a termination will not terminate the licences of any person who has +received the Work from the Licensee under the Licence, provided such persons +remain in full compliance with the Licence. + +13. Miscellaneous + +Without prejudice of Article 9 above, the Licence represents the complete +agreement between the Parties as to the Work. + +If any provision of the Licence is invalid or unenforceable under applicable +law, this will not affect the validity or enforceability of the Licence as a +whole. Such provision will be construed or reformed so as necessary to make it +valid and enforceable. + +The European Commission may publish other linguistic versions or new versions of +this Licence or updated versions of the Appendix, so far this is required and +reasonable, without reducing the scope of the rights granted by the Licence. New +versions of the Licence will be published with a unique version number. + +All linguistic versions of this Licence, approved by the European Commission, +have identical value. Parties can take advantage of the linguistic version of +their choice. + +14. Jurisdiction + +Without prejudice to specific agreement between parties, + +- any litigation resulting from the interpretation of this License, arising + between the European Union institutions, bodies, offices or agencies, as a + Licensor, and any Licensee, will be subject to the jurisdiction of the Court + of Justice of the European Union, as laid down in article 272 of the Treaty on + the Functioning of the European Union, + +- any litigation arising between other parties and resulting from the + interpretation of this License, will be subject to the exclusive jurisdiction + of the competent court where the Licensor resides or conducts its primary + business. + +15. Applicable Law + +Without prejudice to specific agreement between parties, + +- this Licence shall be governed by the law of the European Union Member State + where the Licensor has his seat, resides or has his registered office, + +- this licence shall be governed by Belgian law if the Licensor has no seat, + residence or registered office inside a European Union Member State. + +Appendix + +‘Compatible Licences’ according to Article 5 EUPL are: + +- GNU General Public License (GPL) v. 2, v. 3 +- GNU Affero General Public License (AGPL) v. 3 +- Open Software License (OSL) v. 2.1, v. 3.0 +- Eclipse Public License (EPL) v. 1.0 +- CeCILL v. 2.0, v. 2.1 +- Mozilla Public Licence (MPL) v. 2 +- GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3 +- Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for + works other than software +- European Union Public Licence (EUPL) v. 1.1, v. 1.2 +- Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong + Reciprocity (LiLiQ-R+). + +The European Commission may update this Appendix to later versions of the above +licences without producing a new version of the EUPL, as long as they provide +the rights granted in Article 2 of this Licence and protect the covered Source +Code from exclusive appropriation. + +All other changes or additions to this Appendix require the production of a new +EUPL version. \ No newline at end of file diff --git a/NAMESPACE b/NAMESPACE new file mode 100644 index 0000000..6c46707 --- /dev/null +++ b/NAMESPACE @@ -0,0 +1,28 @@ +# Generated by roxygen2: do not edit by hand + +export(getAuthenticationToken) +export(getProjects) +export(getReport) +export(getReports) +importFrom(checkmate,assert_choice) +importFrom(checkmate,assert_class) +importFrom(checkmate,assert_int) +importFrom(checkmate,assert_list) +importFrom(checkmate,assert_logical) +importFrom(checkmate,assert_string) +importFrom(cli,cli_abort) +importFrom(glue,glue) +importFrom(httr2,req_body_json) +importFrom(httr2,req_headers) +importFrom(httr2,req_method) +importFrom(httr2,req_perform) +importFrom(httr2,req_timeout) +importFrom(httr2,request) +importFrom(httr2,resp_body_raw) +importFrom(httr2,resp_body_string) +importFrom(httr2,resp_status) +importFrom(jsonlite,fromJSON) +importFrom(readr,cols) +importFrom(readr,read_csv) +importFrom(readxl,read_excel) +importFrom(tibble,as_tibble) diff --git a/R/buildAuthenticationRequest.R b/R/buildAuthenticationRequest.R new file mode 100644 index 0000000..47baf72 --- /dev/null +++ b/R/buildAuthenticationRequest.R @@ -0,0 +1,61 @@ +#' Build an authentication request for the DistillerSR API. +#' +#' This helper function constructs and configures an authentication request +#' object for the DistillerSR API using the `httr2` package. +#' +#' @param distillerInstanceUrl `character` (string). The URL of the DistillerSR +#' instance. It must end without a slash. +#' +#' @param distillerKey `character` (string). The personal access key generated +#' in DistillerSR. +#' +#' @param timeout `integer`. The maximum number of seconds to wait for the +#' authentication response. +#' +#' By default: 1800 seconds (30 minutes). +#' +#' @return An `httr2_request` object representing the configured authentication +#' request. +#' +#' @details +#' The function sets the HTTP method to `POST`, sets the timeout, and includes +#' the following headers: +#' \itemize{ +#' \item `Authorization` - the API key used for authentication. +#' } +#' +#' @importFrom checkmate assert_string assert_int +#' @importFrom httr2 request req_method req_headers req_timeout +#' +#' @examplesIf FALSE +#' distillerInstanceUrl_ <- "https://url.to.distiller.instance" +#' distillerKey_ <- "YOUR_API_KEY" +#' +#' request_ <- .buildAuthenticationRequest( +#' distillerInstanceUrl = distillerInstanceUrl_, +#' distillerKey = distillerKey_) +#' +#' @keywords internal +#' @noRd +#' +.buildAuthenticationRequest <- function( + distillerInstanceUrl, + distillerKey, + timeout = 1800) { + + assert_string(distillerInstanceUrl, pattern = "[^/]$") + assert_string(distillerKey) + assert_int(timeout) + + authenticationUrl_ <- glue("{distillerInstanceUrl}/auth") + + request_ <- request(authenticationUrl_) |> + req_method("POST") |> + req_headers( + "Authorization" = paste("Key", distillerKey), + "Content-Type" = "application/octet-stream" + ) |> + req_timeout(timeout) + + return(request_) +} diff --git a/R/buildServiceRequest.R b/R/buildServiceRequest.R new file mode 100644 index 0000000..607d3be --- /dev/null +++ b/R/buildServiceRequest.R @@ -0,0 +1,79 @@ +#' Build a request to a service for the DistillerSR API. +#' +#' This helper function constructs and configures a request object for a +#' DistillerSR API service using the `httr2` package. +#' +#' @param serviceUrl `character` (string). The URL of the service endpoint. +#' +#' @param distillerToken `character` (string). The personal access token +#' generated by the authentication request. +#' +#' @param body `list` (optional). A list containing the body parameters to be +#' encoded into JSON format. +#' +#' By default: NULL (no body). +#' +#' @param timeout `integer`. The maximum number of seconds to wait for the +#' response. +#' +#' By default: 1800 seconds (30 minutes). +#' +#' @return An `httr2_request` object representing the configured service +#' request. +#' +#' @details +#' The function sets the HTTP method, sets the timeout, and includes the +#' following headers: +#' \itemize{ +#' \item `Authorization` - the Bearer token used for authentication. +#' } +#' +#' @importFrom checkmate assert_string assert_list assert_int +#' @importFrom httr2 request req_method req_headers req_timeout req_body_json +#' +#' @examplesIf FALSE +#' serviceUrl_ <- "https://url.to.distiller.instance/projects" +#' +#' distillerToken_ <- getAuthenticationToken() +#' +#' serviceRequest_ <- .buildServiceRequest( +#' serviceUrl = serviceUrl_, +#' distillerToken = distillerToken_) +#' +#' @keywords internal +#' @noRd +#' +.buildServiceRequest <- function( + serviceUrl, + distillerToken, + body = NULL, + timeout = 1800) { + + assert_string(serviceUrl, pattern = "[^/]$") + assert_string(distillerToken) + if (!is.null(body)) { assert_list(body) } + assert_int(timeout) + + request_ <- request(serviceUrl) |> + req_headers( + "Authorization" = paste("Bearer", distillerToken) + ) |> + req_timeout(timeout) + + if (is.null(body)) { + request_ <- request_ |> + req_method("GET") |> + req_headers( + "Content-Type" = "application/octet-stream" + ) + } else { + request_ <- request_ |> + req_method("POST") |> + req_headers( + "Content-Type" = "application/json" + ) |> + req_body_json(body) + } + + return(request_) +} diff --git a/R/distilleR-package.R b/R/distilleR-package.R new file mode 100644 index 0000000..a65cf64 --- /dev/null +++ b/R/distilleR-package.R @@ -0,0 +1,6 @@ +#' @keywords internal +"_PACKAGE" + +## usethis namespace: start +## usethis namespace: end +NULL diff --git a/R/getAuthenticationToken.R b/R/getAuthenticationToken.R new file mode 100644 index 0000000..fd202a2 --- /dev/null +++ b/R/getAuthenticationToken.R @@ -0,0 +1,69 @@ +#' Authenticate to a DistillerSR session. +#' +#' Authenticates a user to a DistillerSR instance using a personal access key. +#' The function returns a valid authentication token that can be used to access +#' protected DistillerSR API endpoints. +#' +#' By default, the personal access key and the instance URL are read from the +#' environment variables `DISTILLER_API_KEY` and `DISTILLER_INSTANCE_URL`. +#' +#' @param distillerKey `character` (string). The personal access key generated +#' in DistillerSR. +#' +#' By default: Sys.getenv("DISTILLER_API_KEY"). +#' +#' @param distillerInstanceUrl `character` (string). The base URL of the +#' DistillerSR instance. +#' +#' By default: Sys.getenv("DISTILLER_INSTANCE_URL"). +#' +#' @param timeout `integer`. The maximum number of seconds to wait for the +#' authentication response. +#' +#' By default: 1800 seconds (30 minutes). +#' +#' @return A string containing a valid DistillerSR authentication token. +#' +#' @importFrom checkmate assert_string assert_int +#' +#' @examplesIf FALSE +#' # If 'DISTILLER_INSTANCE_URL' and 'DISTILLER_API_KEY' are defined in your +#' # environment (e.g. .Renviron). +#' distillerToken_ <- getAuthenticationToken() +#' +#' # If 'distillerInstanceUrl' and 'distillerKey' are to be specified manually. +#' distillerToken_ <- getAuthenticationToken( +#' distillerInstanceUrl = "https://url.to.distiller.instance", +#' distillerKey = "YOUR_API_KEY" +#' ) +#' +#' @export +#' +getAuthenticationToken <- function( + distillerInstanceUrl = Sys.getenv("DISTILLER_INSTANCE_URL"), + distillerKey = Sys.getenv("DISTILLER_API_KEY"), + timeout = 1800) { + + assert_string(distillerInstanceUrl, pattern = "[^/]$") + assert_string(distillerKey) + assert_int(timeout) + + request_ <- .buildAuthenticationRequest( + distillerInstanceUrl = distillerInstanceUrl, + distillerKey = distillerKey, + timeout = timeout) + + response_ <- .performRequest( + request = request_, + errorMessage = "Authentication request failed") + + .handleHTTPErrors( + response = response_, + errorMessage = "Authentication failed") + + responseData_ <- .parseJSONResponse( + response = response_, + errorMessage = "Failed to parse authentication response") + + return(responseData_$token) +} diff --git a/R/getProjects.R b/R/getProjects.R new file mode 100644 index 0000000..08512b2 --- /dev/null +++ b/R/getProjects.R @@ -0,0 +1,71 @@ +#' Get the list of the Distiller projects associated to the authenticated user. +#' +#' This function queries the DistillerSR API to retrieve the list of projects +#' accessible to the authenticated user. It requires an authentication token +#' and a valid API instance URL. The result is a dataframe listing available +#' projects. +#' +#' @param distillerToken `character` (string). The token the user gets once +#' authenticated. +#' +#' @param distillerInstanceUrl `character` (string). The distiller instance URL. +#' +#' By default: Sys.getenv("DISTILLER_INSTANCE_URL"). +#' +#' @param timeout `integer`. The maximum number of seconds to wait for the +#' response. +#' +#' By default: 1800 seconds (30 minutes). +#' +#' @return A tibble with four columns: +#' \itemize{ +#' \item \code{id}: The project ID. +#' \item \code{name}: The name of the project. +#' \item \code{de_project_id}. +#' \item \code{is_hidden}. +#' } +#' +#' @importFrom checkmate assert_string assert_int +#' @importFrom tibble as_tibble +#' +#' @seealso \code{\link{getAuthenticationToken}} +#' +#' @examplesIf FALSE +#' distillerToken_ <- getAuthenticationToken() +#' +#' projects_ <- getProjects(distillerToken = distillerToken_) +#' +#' @export +#' +getProjects <- function( + distillerInstanceUrl = Sys.getenv("DISTILLER_INSTANCE_URL"), + distillerToken, + timeout = 1800) { + + assert_string(distillerInstanceUrl, pattern = "[^/]$") + assert_string(distillerToken) + assert_int(timeout) + + projectsUrl_ <- glue("{distillerInstanceUrl}/projects") + + request_ <- .buildServiceRequest( + serviceUrl = projectsUrl_, + distillerToken = distillerToken, + timeout = timeout) + + response_ <- .performRequest( + request = request_, + errorMessage = "Request for projects failed") + + .handleHTTPErrors( + response = response_, + errorMessage = "Unable to retrieve projects") + + responseData_ <- .parseJSONResponse( + response = response_, + errorMessage = "Failed to parse projects service response") + + responseData_ <- as_tibble(responseData_) + + return(responseData_) +} diff --git a/R/getReport.R b/R/getReport.R new file mode 100644 index 0000000..dfaa5f9 --- /dev/null +++ b/R/getReport.R @@ -0,0 +1,146 @@ +#' Get a Distiller report associated to a project of the authenticated user. +#' +#' This function queries the DistillerSR API to retrieve a saved report +#' associated with a given project ID. It requires user authentication +#' and a valid API endpoint URL. The result is a dataframe containing metadata +#' about the saved report. +#' +#' @param projectId `integer`. The ID of the project as provided by DistillerSR. +#' +#' @param reportId `integer`. The ID of the report as provided by DistillerSR. +#' +#' @param format `character` (string). The desired format for the document. It +#' can be either excel or csv. +#' +#' @param distillerInstanceUrl `character` (string). The distiller instance URL. +#' +#' By default: Sys.getenv("DISTILLER_INSTANCE_URL"). +#' +#' @param distillerToken `character` (string). The token the user gets once +#' authenticated. +#' +#' @param timeout `integer`. The maximum number of seconds to wait for the +#' response. +#' +#' By default: 1800 seconds (30 minutes). +#' +#' @param attempts `integer`. The maximum number of attempts. +#' +#' By default: 1 attempt. +#' +#' @param retryEach `integer`. The delay between attempts. +#' +#' By default: 600 seconds (10 minutes). +#' +#' @param verbose `logical`. A flag to specify whether to make the function +#' verbose or not. +#' +#' By default: TRUE. +#' +#' @return A data frame containing the Distiller report as designed within +#' DistillerSR. +#' +#' @importFrom checkmate assert_int assert_string assert_choice assert_logical +#' @importFrom cli cli_abort +#' +#' @seealso \code{\link{getAuthenticationToken}} +#' @seealso \code{\link{getProjects}} +#' @seealso \code{\link{getReports}} +#' +#' @examplesIf FALSE +#' distillerToken_ <- getAuthenticationToken() +#' projects_ <- getProjects(distillerToken = distillerToken_) +#' reports_ <- getReports( +#' projectId = projects_$id[1], +#' distillerToken = distillerToken_) +#' +#' report_ <- getReport( +#' projectId = projects_$id[1], +#' reportID = reports_$id[7], +#' format = "csv", +#' distillerToken = distillerToken_) +#' +#' @export +#' +getReport <- function( + projectId, + reportId, + format = c("excel", "csv"), + distillerInstanceUrl = Sys.getenv("DISTILLER_INSTANCE_URL"), + distillerToken, + timeout = 1800, + attempts = 1, + retryEach = 600, + verbose = TRUE) { + + assert_int(projectId) + assert_int(reportId) + assert_string(format) + assert_choice(format, eval(formals()$format)) + assert_string(distillerInstanceUrl, pattern = "[^/]$") + assert_string(distillerToken) + assert_int(attempts, lower = 1) + assert_int(retryEach, lower = 0) + assert_logical(verbose) + + reportUrl_ <- glue("{distillerInstanceUrl}/datarama/query") + + request_ <- .buildServiceRequest( + serviceUrl = reportUrl_, + distillerToken = distillerToken, + timeout = timeout, + body = list( + "project_id" = projectId, + "saved_report_id" = reportId, + "use_saved_format"= TRUE)) + + attempt_ <- 0 + + while (attempt_ < attempts) { + attempt_ <- attempt_ + 1 + + if (verbose && attempts > 1) { + print(glue("Starting attempt {attempt_}...")) + } + + tryCatch({ + response_ <- .performRequest( + request = request_, + errorMessage = glue("Request for report {reportId} failed")) + + .handleHTTPErrors( + response = response_, + errorMessage = glue("Unable to retrieve report {reportId}")) + + if (format == "csv") { + csvData_ <- .parseCSVResponse( + response = response_, + errorMessage = glue("Failed to parse CSV for report {reportId}")) + return(csvData_) + + } else { + xlsxData_ <- .parseXLSXResponse( + response = response_, + errorMessage = glue("Failed to parse XLSX for report {reportId}")) + return(xlsxData_) + } + }, + error = function(e_) { + if (verbose) { + print(glue("Attempt failed with reason:\n{e_}")) + } + + if (attempt_ < attempts) { + if (verbose) { + print(glue("Sleeping for {retryEach} seconds...")) + } + .sleep(retryEach) + } + }) + } + + cli_abort(c( + glue("Unable to retrieve report {reportId}"), + "x" = "All attempts to retrieve the report failed" + )) +} diff --git a/R/getReports.R b/R/getReports.R new file mode 100644 index 0000000..8b3f6a9 --- /dev/null +++ b/R/getReports.R @@ -0,0 +1,81 @@ +#' Get the list of the Distiller reports associated to a project of the +#' authenticated user. +#' +#' This function queries the DistillerSR API to retrieve the list of saved +#' reports associated with a given project ID. It requires user authentication +#' and a valid API endpoint URL. The result is a dataframe containing metadata +#' about each saved report. +#' +#' @param projectId `integer`. The ID of the project as provided by DistillerSR. +#' +#' @param distillerInstanceUrl `character` (string). The distiller instance URL. +#' +#' By default: Sys.getenv("DISTILLER_INSTANCE_URL"). +#' +#' @param distillerToken `character` (string). The token the user gets once +#' authenticated. +#' +#' @param timeout `integer`. The maximum number of seconds to wait for the +#' response. +#' +#' By default: 1800 seconds (30 minutes). +#' +#' @return A tibble with four columns: +#' \itemize{ +#' \item \code{id}: The ID of the saved report. +#' \item \code{name}: The name of the report. +#' \item \code{date}: The creation date of the report. +#' \item \code{view}: The format of the report (e.g., html, csv, excel). +#' } +#' +#' @importFrom checkmate assert_int assert_string +#' @importFrom glue glue +#' +#' @seealso \code{\link{getAuthenticationToken}} +#' @seealso \code{\link{getProjects}} +#' +#' @examplesIf FALSE +#' distillerToken_ <- getAuthenticationToken() +#' projects_ <- getProjects(distillerToken = distillerToken_) +#' +#' reports_ <- getReports( +#' projectId = projects_$id[1], +#' distillerToken = distillerToken_) +#' +#' @export +#' +getReports <- function( + projectId, + distillerInstanceUrl = Sys.getenv("DISTILLER_INSTANCE_URL"), + distillerToken, + timeout = 1800) { + + assert_int(projectId) + assert_string(distillerInstanceUrl, pattern = "[^/]$") + assert_string(distillerToken) + assert_int(timeout) + + reportsUrl_ <- distillerInstanceUrl + reportsUrl_ <- glue("{reportsUrl_}/projects/{projectId}/reports/datarama") + + request_ <- .buildServiceRequest( + serviceUrl = reportsUrl_, + distillerToken = distillerToken, + timeout = timeout) + + response_ <- .performRequest( + request = request_, + errorMessage = glue("Request for project {projectId} reports failed")) + + .handleHTTPErrors( + response = response_, + errorMessage = glue("Unable to retrieve reports for project {projectId}")) + + responseData_ <- .parseJSONResponse( + response = response_, + errorMessage = "Failed to parse reports service response") + + responseData_ <- as_tibble(responseData_) + + return(responseData_) +} diff --git a/R/handleHTTPErrors.R b/R/handleHTTPErrors.R new file mode 100644 index 0000000..9cd7f7c --- /dev/null +++ b/R/handleHTTPErrors.R @@ -0,0 +1,40 @@ +#' Handle non-successful HTTP responses from the DistillerSR API. +#' +#' This helper function checks whether an HTTP response from the DistillerSR API +#' indicates success (status code 200). If the response contains any other +#' status code, it raises a formatted error. +#' +#' @param response `httr2_response`. An object returned by `req_perform()`. +#' +#' @param errorMessage `character` (string). The message to display in case of +#' error. +#' +#' @return Invisibly returns `NULL` if the response is successful (status code +#' 200). Otherwise, execution stops with a descriptive error message. +#' +#' @importFrom checkmate assert_class assert_string +#' @importFrom httr2 resp_status +#' @importFrom cli cli_abort +#' +#' @examplesIf FALSE +#' response_ <- req_perform(request("https://example.org/")) +#' +#' .handleHTTPErrors(response = response_) +#' +#' @keywords internal +#' @noRd +#' +.handleHTTPErrors <- function(response, errorMessage = "API request failed") { + + assert_class(response, "httr2_response") + assert_string(errorMessage) + + status_ <- resp_status(response) + + if (status_ != 200) { + cli_abort(c( + errorMessage, + "x" = "(HTTP {status_})" + )) + } +} diff --git a/R/parseCSVResponse.R b/R/parseCSVResponse.R new file mode 100644 index 0000000..7b38cf4 --- /dev/null +++ b/R/parseCSVResponse.R @@ -0,0 +1,59 @@ +#' Parse an API CSV response. +#' +#' This helper function parses the CSV body of an API response. +#' +#' @param response `httr2_response`. An HTTP response object returned by +#' `httr2::req_perform()`. +#' +#' @param errorMessage `character` (string). The message to display in case of +#' errors. +#' +#' @param verbose `logical`. A flag to specify whether to make the parsing +#' verbose or not. +#' +#' By default: TRUE. +#' +#' @return A dataframe containing the parsed CSV response. +#' +#' @details +#' If the API response body cannot be parsed as valid CSV, the function raises +#' an error, including the original parsing error message. +#' +#' @importFrom checkmate assert_class assert_string assert_logical +#' @importFrom readr read_csv cols +#' @importFrom httr2 resp_body_string +#' @importFrom cli cli_abort +#' +#' @examplesIf FALSE +#' response_ <- req_perform(request("https://example.org/")) +#' +#' csvResponseData_ <- .parseCSVResponse(response = response_) +#' +#' @keywords internal +#' @noRd +#' +.parseCSVResponse <- function( + response, + errorMessage = "Failed to parse API CSV response", + verbose = TRUE) { + + assert_class(response, "httr2_response") + assert_string(errorMessage) + assert_logical(verbose) + + csvResponseData_ <- tryCatch( + read_csv( + resp_body_string(response), + col_types = cols(.default = "c"), + show_col_types = verbose), + + error = function(e) { + cli_abort(c( + errorMessage, + "x" = conditionMessage(e) + )) + } + ) + + return(csvResponseData_) +} diff --git a/R/parseJSONResponse.R b/R/parseJSONResponse.R new file mode 100644 index 0000000..66b43c0 --- /dev/null +++ b/R/parseJSONResponse.R @@ -0,0 +1,49 @@ +#' Parse a JSON API response. +#' +#' This helper function parses the JSON body of an API response. +#' +#' @param response `httr2_response`. An HTTP response object returned by +#' `httr2::req_perform()`. +#' +#' @param errorMessage `character` (string). The message to display in case of +#' errors. +#' +#' @return A data structure containing the parsed JSON response. +#' +#' @details +#' If the API response body cannot be parsed as valid JSON, the function raises +#' an error, including the original parsing error message. +#' +#' @importFrom checkmate assert_class assert_string +#' @importFrom jsonlite fromJSON +#' @importFrom httr2 resp_body_string +#' @importFrom cli cli_abort +#' +#' @examplesIf FALSE +#' response_ <- req_perform(request("https://example.org/")) +#' +#' responseData_ <- .parseJSONResponse(response = response_) +#' +#' @keywords internal +#' @noRd +#' +.parseJSONResponse <- function( + response, + errorMessage = "Failed to parse API response") { + + assert_class(response, "httr2_response") + assert_string(errorMessage) + + responseData_ <- tryCatch( + fromJSON(resp_body_string(response)), + + error = function(e) { + cli_abort(c( + errorMessage, + "x" = conditionMessage(e) + )) + } + ) + + return(responseData_) +} diff --git a/R/parseXLSXResponse.R b/R/parseXLSXResponse.R new file mode 100644 index 0000000..58aeba7 --- /dev/null +++ b/R/parseXLSXResponse.R @@ -0,0 +1,52 @@ +#' Parse an API XLSX response. +#' +#' This helper function parses the XLSX body of an API response. +#' +#' @param response `httr2_response`. An HTTP response object returned by +#' `httr2::req_perform()`. +#' +#' @param errorMessage `character` (string). The message to display in case of +#' errors. +#' +#' @return A dataframe containing the parsed XLSX response. +#' +#' @details +#' If the API response body cannot be parsed as valid XLSX, the function raises +#' an error, including the original parsing error message. +#' +#' @importFrom checkmate assert_class assert_string +#' @importFrom readxl read_excel +#' @importFrom httr2 resp_body_raw +#' @importFrom cli cli_abort +#' +#' @examplesIf FALSE +#' response_ <- req_perform(request("https://example.org/")) +#' +#' xlsxResponseData_ <- .parseXLSXResponse(response = response_) +#' +#' @keywords internal +#' @noRd +#' +.parseXLSXResponse <- function( + response, + errorMessage = "Failed to parse API XLSX response") { + + assert_class(response, "httr2_response") + assert_string(errorMessage) + + xlsxResponseData_ <- tryCatch( + { + xlsxTempFile_ <- tempfile(fileext = ".xlsx") + writeBin(resp_body_raw(response), xlsxTempFile_) + read_excel(xlsxTempFile_, col_types = "text") + }, + error = function(e) { + cli_abort(c( + errorMessage, + "x" = conditionMessage(e) + )) + } + ) + + return(xlsxResponseData_) +} diff --git a/R/performRequest.R b/R/performRequest.R new file mode 100644 index 0000000..1720121 --- /dev/null +++ b/R/performRequest.R @@ -0,0 +1,44 @@ +#' Perform an HTTP request to the with error handling. +#' +#' This helper function executes a prepared HTTP request and safely handles any +#' connection-level errors. If an error occurs during the request (e.g., network +#' failure, timeout, DNS issue), it aborts execution with a clear message. +#' +#' @param request `httr2_request`. A request object created with the `httr2` +#' package and configured with methods, headers and parameters. +#' +#' @param errorMessage `character` (string). The message to display in case of +#' errors. +#' +#' @return The HTTP response object returned by `httr2::req_perform()` if the +#' request is successful. +#' +#' @importFrom checkmate assert_class assert_string +#' @importFrom httr2 req_perform +#' @importFrom cli cli_abort +#' +#' @examplesIf FALSE +#' request_ <- request("https://example.org/") +#' +#' response_ <- .performRequest(request = request_) +#' +#' @keywords internal +#' @noRd +#' +.performRequest <- function(request, errorMessage = "Request failed") { + + assert_class(request, "httr2_request") + assert_string(errorMessage) + + response_ <- tryCatch( + req_perform(request), + error = function(e) { + cli_abort(c( + errorMessage, + "x" = conditionMessage(e) + )) + } + ) + + return(response_) +} diff --git a/R/sleep.R b/R/sleep.R new file mode 100644 index 0000000..a44948d --- /dev/null +++ b/R/sleep.R @@ -0,0 +1,22 @@ +#' Sleeps for the given number of seconds. +#' +#' This function wraps the base Sys.sleep() function to make it mockable and +#' reachable inside unit tests. +#' +#' @param seconds `integer`. The maximum number of seconds to wait. +#' +#' @importFrom checkmate assert_int +#' +#' @examplesIf FALSE +#' # Sleep for 1 second. +#' .sleep(1) +#' +#' @keywords internal +#' @noRd +#' +.sleep <- function(seconds) { + + assert_int(seconds) + + Sys.sleep(seconds) +} diff --git a/README.md b/README.md index 137a443..54ad34d 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,51 @@ -# distilleR -A wrap around the DistillerSR APIs. +# distilleR + +[![Lifecycle: stable](https://img.shields.io/badge/lifecycle-stable-brightgreen.svg)](https://lifecycle.r-lib.org/articles/stages.html#stable) [![codecov](https://codecov.io/gh/openefsa/distilleR/branch/main/graph/badge.svg?token=BF95QQPCA1)](https://codecov.io/gh/openefsa/distilleR) + +## Overview + +The **distilleR** package provides a pool of functions to query **DistillerSR** through its APIs. +It features authentication and utilities to retrieve data from DistillerSR projects and reports. + +The package is intended for researchers, analysts, and practitioners who require convenient programmatic access to DistillerSR data. + +## Installation + +### From CRAN + +```r +install.packages("distilleR") +``` + +### Development version (from GitHub) + +To install the latest development version: + +```r +# install.packages("devtools") +devtools::install_github("openefsa/distilleR") +``` + +## Requirements + +An active internet connection is required, as the package communicates with DistillerSR online services to fetch and process data. + +## Usage + +Once installed, load the package as usual: + +```r +library(distilleR) +``` + +Basic usage examples and full documentation are available in the package [vignette](vignettes/distilleR.Rmd): + +```r +vignette("distilleR") +``` + +## Links + +- **Source code:** [GitHub – openefsa/distilleR](https://github.com/openefsa/distilleR) +- **Bug reports:** [Issues on GitHub](https://github.com/openefsa/distilleR/issues) +- **DistillerSR API Documentation:** [https://apidocs.evidencepartners.com/](https://apidocs.evidencepartners.com/) \ No newline at end of file diff --git a/_pkgdown.yml b/_pkgdown.yml new file mode 100644 index 0000000..6efc70b --- /dev/null +++ b/_pkgdown.yml @@ -0,0 +1,8 @@ +template: + bootstrap: 5 + +development: + mode: auto + +url: https://openefsa.github.io/distilleR +destination: docs diff --git a/distilleR.Rproj b/distilleR.Rproj new file mode 100644 index 0000000..584b854 --- /dev/null +++ b/distilleR.Rproj @@ -0,0 +1,19 @@ +Version: 1.0 + +RestoreWorkspace: Default +SaveWorkspace: Default +AlwaysSaveHistory: Default + +EnableCodeIndexing: Yes +UseSpacesForTab: Yes +NumSpacesForTab: 2 +Encoding: UTF-8 + +RnwWeave: Sweave +LaTeX: pdfLaTeX + +BuildType: Package +PackageUseDevtools: Yes +PackageInstallArgs: --no-multiarch --with-keep.source +PackageCheckArgs: --as-cran +PackageRoxygenize: rd,collate,namespace diff --git a/man/distilleR-package.Rd b/man/distilleR-package.Rd new file mode 100644 index 0000000..2fad476 --- /dev/null +++ b/man/distilleR-package.Rd @@ -0,0 +1,33 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/distilleR-package.R +\docType{package} +\name{distilleR-package} +\alias{distilleR} +\alias{distilleR-package} +\title{distilleR: A wrap around the DistillerSR APIs} +\description{ +\if{html}{\figure{logo.png}{options: style='float: right' alt='logo' width='120'}} + +The distilleR package provides a pool of functions to query DistillerSR through its APIs. It features authentication and utilities to retrieve data from DistillerSR projects and reports. +} +\seealso{ +Useful links: +\itemize{ + \item \url{https://openefsa.github.io/distilleR} + \item Report bugs at \url{https://github.com/openefsa/distilleR/issues} +} + +} +\author{ +\strong{Maintainer}: Luca Belmonte \email{luca.belmonte@efsa.europa.eu} (\href{https://orcid.org/0000-0002-7977-9170}{ORCID}) + +Authors: +\itemize{ + \item Lorenzo Copelli (\href{https://orcid.org/0009-0002-4305-065X}{ORCID}) + \item Fulvio Barizzone + \item Dayana Stephanie Buzle (\href{https://orcid.org/0009-0003-2990-7431}{ORCID}) + \item Rafael Vieira (\href{https://orcid.org/0009-0009-0289-5438}{ORCID}) +} + +} +\keyword{internal} diff --git a/man/figures/logo.png b/man/figures/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..6e9927be617fed8107d284a496f093b288e649ff GIT binary patch literal 51808 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 literal 0 HcmV?d00001 diff --git a/pkgdown/favicon/favicon-96x96.png b/pkgdown/favicon/favicon-96x96.png new file mode 100644 index 0000000000000000000000000000000000000000..74a9877684946ef28f55bb88cbbcb6c59bcdba72 GIT binary patch literal 3963 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 VrfRmnks<&9002ovPDHLkV1kEKzX1RM literal 0 HcmV?d00001 diff --git a/pkgdown/favicon/favicon.ico b/pkgdown/favicon/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..3a06a7d04b937f15ce25dad3fdc1b69febb1b5b9 GIT binary patch literal 15086 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 literal 0 HcmV?d00001 diff --git a/pkgdown/favicon/favicon.svg b/pkgdown/favicon/favicon.svg new file mode 100644 index 0000000..eebcbc7 --- /dev/null +++ b/pkgdown/favicon/favicon.svg @@ -0,0 +1 @@ +RealFaviconGeneratorhttps://realfavicongenerator.net \ No newline at end of file diff --git a/pkgdown/favicon/site.webmanifest b/pkgdown/favicon/site.webmanifest new file mode 100644 index 0000000..4ebda26 --- /dev/null +++ b/pkgdown/favicon/site.webmanifest @@ -0,0 +1,21 @@ +{ + "name": "", + "short_name": "", + "icons": [ + { + "src": "/web-app-manifest-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "/web-app-manifest-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} \ No newline at end of file diff --git a/pkgdown/favicon/web-app-manifest-192x192.png b/pkgdown/favicon/web-app-manifest-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..9c70a4bfeff02da65c99e66d16eb178cc931a539 GIT binary patch literal 9976 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 literal 0 HcmV?d00001 diff --git a/pkgdown/favicon/web-app-manifest-512x512.png b/pkgdown/favicon/web-app-manifest-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..468a06973f8ace94ee437c18efa5dad20c8cb099 GIT binary patch literal 35302 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 Reading from a connection which does not supply a 'gzip' magic + # > header is equivalent to reading from the original connection + conn <- gzcon(file(bundle, open = "rb", raw = TRUE)) + on.exit(close(conn)) + + # The default pax header is 512 bytes long and the first pax extended header + # with the comment should be 51 bytes long + # `52 comment=` (11 chars) + 40 byte SHA1 hash + len <- 0x200 + 0x33 + res <- rawToChar(readBin(conn, "raw", n = len)[0x201:len]) + + if (grepl("^52 comment=", res)) { + sub("52 comment=", "", res) + } else { + NULL + } + } + + renv_bootstrap_install <- function(version, tarball, library) { + + # attempt to install it into project library + dir.create(library, showWarnings = FALSE, recursive = TRUE) + output <- renv_bootstrap_install_impl(library, tarball) + + # check for successful install + status <- attr(output, "status") + if (is.null(status) || identical(status, 0L)) + return(status) + + # an error occurred; report it + header <- "installation of renv failed" + lines <- paste(rep.int("=", nchar(header)), collapse = "") + text <- paste(c(header, lines, output), collapse = "\n") + stop(text) + + } + + renv_bootstrap_install_impl <- function(library, tarball) { + + # invoke using system2 so we can capture and report output + bin <- R.home("bin") + exe <- if (Sys.info()[["sysname"]] == "Windows") "R.exe" else "R" + R <- file.path(bin, exe) + + args <- c( + "--vanilla", "CMD", "INSTALL", "--no-multiarch", + "-l", shQuote(path.expand(library)), + shQuote(path.expand(tarball)) + ) + + system2(R, args, stdout = TRUE, stderr = TRUE) + + } + + renv_bootstrap_platform_prefix <- function() { + + # construct version prefix + version <- paste(R.version$major, R.version$minor, sep = ".") + prefix <- paste("R", numeric_version(version)[1, 1:2], sep = "-") + + # include SVN revision for development versions of R + # (to avoid sharing platform-specific artefacts with released versions of R) + devel <- + identical(R.version[["status"]], "Under development (unstable)") || + identical(R.version[["nickname"]], "Unsuffered Consequences") + + if (devel) + prefix <- paste(prefix, R.version[["svn rev"]], sep = "-r") + + # build list of path components + components <- c(prefix, R.version$platform) + + # include prefix if provided by user + prefix <- renv_bootstrap_platform_prefix_impl() + if (!is.na(prefix) && nzchar(prefix)) + components <- c(prefix, components) + + # build prefix + paste(components, collapse = "/") + + } + + renv_bootstrap_platform_prefix_impl <- function() { + + # if an explicit prefix has been supplied, use it + prefix <- Sys.getenv("RENV_PATHS_PREFIX", unset = NA) + if (!is.na(prefix)) + return(prefix) + + # if the user has requested an automatic prefix, generate it + auto <- Sys.getenv("RENV_PATHS_PREFIX_AUTO", unset = NA) + if (auto %in% c("TRUE", "True", "true", "1")) + return(renv_bootstrap_platform_prefix_auto()) + + # empty string on failure + "" + + } + + renv_bootstrap_platform_prefix_auto <- function() { + + prefix <- tryCatch(renv_bootstrap_platform_os(), error = identity) + if (inherits(prefix, "error") || prefix %in% "unknown") { + + msg <- paste( + "failed to infer current operating system", + "please file a bug report at https://github.com/rstudio/renv/issues", + sep = "; " + ) + + warning(msg) + + } + + prefix + + } + + renv_bootstrap_platform_os <- function() { + + sysinfo <- Sys.info() + sysname <- sysinfo[["sysname"]] + + # handle Windows + macOS up front + if (sysname == "Windows") + return("windows") + else if (sysname == "Darwin") + return("macos") + + # check for os-release files + for (file in c("/etc/os-release", "/usr/lib/os-release")) + if (file.exists(file)) + return(renv_bootstrap_platform_os_via_os_release(file, sysinfo)) + + # check for redhat-release files + if (file.exists("/etc/redhat-release")) + return(renv_bootstrap_platform_os_via_redhat_release()) + + "unknown" + + } + + renv_bootstrap_platform_os_via_os_release <- function(file, sysinfo) { + + # read /etc/os-release + release <- utils::read.table( + file = file, + sep = "=", + quote = c("\"", "'"), + col.names = c("Key", "Value"), + comment.char = "#", + stringsAsFactors = FALSE + ) + + vars <- as.list(release$Value) + names(vars) <- release$Key + + # get os name + os <- tolower(sysinfo[["sysname"]]) + + # read id + id <- "unknown" + for (field in c("ID", "ID_LIKE")) { + if (field %in% names(vars) && nzchar(vars[[field]])) { + id <- vars[[field]] + break + } + } + + # read version + version <- "unknown" + for (field in c("UBUNTU_CODENAME", "VERSION_CODENAME", "VERSION_ID", "BUILD_ID")) { + if (field %in% names(vars) && nzchar(vars[[field]])) { + version <- vars[[field]] + break + } + } + + # join together + paste(c(os, id, version), collapse = "-") + + } + + renv_bootstrap_platform_os_via_redhat_release <- function() { + + # read /etc/redhat-release + contents <- readLines("/etc/redhat-release", warn = FALSE) + + # infer id + id <- if (grepl("centos", contents, ignore.case = TRUE)) + "centos" + else if (grepl("redhat", contents, ignore.case = TRUE)) + "redhat" + else + "unknown" + + # try to find a version component (very hacky) + version <- "unknown" + + parts <- strsplit(contents, "[[:space:]]")[[1L]] + for (part in parts) { + + nv <- tryCatch(numeric_version(part), error = identity) + if (inherits(nv, "error")) + next + + version <- nv[1, 1] + break + + } + + paste(c("linux", id, version), collapse = "-") + + } + + renv_bootstrap_library_root_name <- function(project) { + + # use project name as-is if requested + asis <- Sys.getenv("RENV_PATHS_LIBRARY_ROOT_ASIS", unset = "FALSE") + if (asis) + return(basename(project)) + + # otherwise, disambiguate based on project's path + id <- substring(renv_bootstrap_hash_text(project), 1L, 8L) + paste(basename(project), id, sep = "-") + + } + + renv_bootstrap_library_root <- function(project) { + + prefix <- renv_bootstrap_profile_prefix() + + path <- Sys.getenv("RENV_PATHS_LIBRARY", unset = NA) + if (!is.na(path)) + return(paste(c(path, prefix), collapse = "/")) + + path <- renv_bootstrap_library_root_impl(project) + if (!is.null(path)) { + name <- renv_bootstrap_library_root_name(project) + return(paste(c(path, prefix, name), collapse = "/")) + } + + renv_bootstrap_paths_renv("library", project = project) + + } + + renv_bootstrap_library_root_impl <- function(project) { + + root <- Sys.getenv("RENV_PATHS_LIBRARY_ROOT", unset = NA) + if (!is.na(root)) + return(root) + + type <- renv_bootstrap_project_type(project) + if (identical(type, "package")) { + userdir <- renv_bootstrap_user_dir() + return(file.path(userdir, "library")) + } + + } + + renv_bootstrap_validate_version <- function(version, description = NULL) { + + # resolve description file + # + # avoid passing lib.loc to `packageDescription()` below, since R will + # use the loaded version of the package by default anyhow. note that + # this function should only be called after 'renv' is loaded + # https://github.com/rstudio/renv/issues/1625 + description <- description %||% packageDescription("renv") + + # check whether requested version 'version' matches loaded version of renv + sha <- attr(version, "sha", exact = TRUE) + valid <- if (!is.null(sha)) + renv_bootstrap_validate_version_dev(sha, description) + else + renv_bootstrap_validate_version_release(version, description) + + if (valid) + return(TRUE) + + # the loaded version of renv doesn't match the requested version; + # give the user instructions on how to proceed + remote <- if (!is.null(description[["RemoteSha"]])) { + paste("rstudio/renv", description[["RemoteSha"]], sep = "@") + } else { + paste("renv", description[["Version"]], sep = "@") + } + + # display both loaded version + sha if available + friendly <- renv_bootstrap_version_friendly( + version = description[["Version"]], + sha = description[["RemoteSha"]] + ) + + fmt <- paste( + "renv %1$s was loaded from project library, but this project is configured to use renv %2$s.", + "- Use `renv::record(\"%3$s\")` to record renv %1$s in the lockfile.", + "- Use `renv::restore(packages = \"renv\")` to install renv %2$s into the project library.", + sep = "\n" + ) + catf(fmt, friendly, renv_bootstrap_version_friendly(version), remote) + + FALSE + + } + + renv_bootstrap_validate_version_dev <- function(version, description) { + expected <- description[["RemoteSha"]] + is.character(expected) && startswith(expected, version) + } + + renv_bootstrap_validate_version_release <- function(version, description) { + expected <- description[["Version"]] + is.character(expected) && identical(expected, version) + } + + renv_bootstrap_hash_text <- function(text) { + + hashfile <- tempfile("renv-hash-") + on.exit(unlink(hashfile), add = TRUE) + + writeLines(text, con = hashfile) + tools::md5sum(hashfile) + + } + + renv_bootstrap_load <- function(project, libpath, version) { + + # try to load renv from the project library + if (!requireNamespace("renv", lib.loc = libpath, quietly = TRUE)) + return(FALSE) + + # warn if the version of renv loaded does not match + renv_bootstrap_validate_version(version) + + # execute renv load hooks, if any + hooks <- getHook("renv::autoload") + for (hook in hooks) + if (is.function(hook)) + tryCatch(hook(), error = warnify) + + # load the project + renv::load(project) + + TRUE + + } + + renv_bootstrap_profile_load <- function(project) { + + # if RENV_PROFILE is already set, just use that + profile <- Sys.getenv("RENV_PROFILE", unset = NA) + if (!is.na(profile) && nzchar(profile)) + return(profile) + + # check for a profile file (nothing to do if it doesn't exist) + path <- renv_bootstrap_paths_renv("profile", profile = FALSE, project = project) + if (!file.exists(path)) + return(NULL) + + # read the profile, and set it if it exists + contents <- readLines(path, warn = FALSE) + if (length(contents) == 0L) + return(NULL) + + # set RENV_PROFILE + profile <- contents[[1L]] + if (!profile %in% c("", "default")) + Sys.setenv(RENV_PROFILE = profile) + + profile + + } + + renv_bootstrap_profile_prefix <- function() { + profile <- renv_bootstrap_profile_get() + if (!is.null(profile)) + return(file.path("profiles", profile, "renv")) + } + + renv_bootstrap_profile_get <- function() { + profile <- Sys.getenv("RENV_PROFILE", unset = "") + renv_bootstrap_profile_normalize(profile) + } + + renv_bootstrap_profile_set <- function(profile) { + profile <- renv_bootstrap_profile_normalize(profile) + if (is.null(profile)) + Sys.unsetenv("RENV_PROFILE") + else + Sys.setenv(RENV_PROFILE = profile) + } + + renv_bootstrap_profile_normalize <- function(profile) { + + if (is.null(profile) || profile %in% c("", "default")) + return(NULL) + + profile + + } + + renv_bootstrap_path_absolute <- function(path) { + + substr(path, 1L, 1L) %in% c("~", "/", "\\") || ( + substr(path, 1L, 1L) %in% c(letters, LETTERS) && + substr(path, 2L, 3L) %in% c(":/", ":\\") + ) + + } + + renv_bootstrap_paths_renv <- function(..., profile = TRUE, project = NULL) { + renv <- Sys.getenv("RENV_PATHS_RENV", unset = "renv") + root <- if (renv_bootstrap_path_absolute(renv)) NULL else project + prefix <- if (profile) renv_bootstrap_profile_prefix() + components <- c(root, renv, prefix, ...) + paste(components, collapse = "/") + } + + renv_bootstrap_project_type <- function(path) { + + descpath <- file.path(path, "DESCRIPTION") + if (!file.exists(descpath)) + return("unknown") + + desc <- tryCatch( + read.dcf(descpath, all = TRUE), + error = identity + ) + + if (inherits(desc, "error")) + return("unknown") + + type <- desc$Type + if (!is.null(type)) + return(tolower(type)) + + package <- desc$Package + if (!is.null(package)) + return("package") + + "unknown" + + } + + renv_bootstrap_user_dir <- function() { + dir <- renv_bootstrap_user_dir_impl() + path.expand(chartr("\\", "/", dir)) + } + + renv_bootstrap_user_dir_impl <- function() { + + # use local override if set + override <- getOption("renv.userdir.override") + if (!is.null(override)) + return(override) + + # use R_user_dir if available + tools <- asNamespace("tools") + if (is.function(tools$R_user_dir)) + return(tools$R_user_dir("renv", "cache")) + + # try using our own backfill for older versions of R + envvars <- c("R_USER_CACHE_DIR", "XDG_CACHE_HOME") + for (envvar in envvars) { + root <- Sys.getenv(envvar, unset = NA) + if (!is.na(root)) + return(file.path(root, "R/renv")) + } + + # use platform-specific default fallbacks + if (Sys.info()[["sysname"]] == "Windows") + file.path(Sys.getenv("LOCALAPPDATA"), "R/cache/R/renv") + else if (Sys.info()[["sysname"]] == "Darwin") + "~/Library/Caches/org.R-project.R/R/renv" + else + "~/.cache/R/renv" + + } + + renv_bootstrap_version_friendly <- function(version, shafmt = NULL, sha = NULL) { + sha <- sha %||% attr(version, "sha", exact = TRUE) + parts <- c(version, sprintf(shafmt %||% " [sha: %s]", substring(sha, 1L, 7L))) + paste(parts, collapse = "") + } + + renv_bootstrap_exec <- function(project, libpath, version) { + if (!renv_bootstrap_load(project, libpath, version)) + renv_bootstrap_run(version, libpath) + } + + renv_bootstrap_run <- function(version, libpath) { + + # perform bootstrap + bootstrap(version, libpath) + + # exit early if we're just testing bootstrap + if (!is.na(Sys.getenv("RENV_BOOTSTRAP_INSTALL_ONLY", unset = NA))) + return(TRUE) + + # try again to load + if (requireNamespace("renv", lib.loc = libpath, quietly = TRUE)) { + return(renv::load(project = getwd())) + } + + # failed to download or load renv; warn the user + msg <- c( + "Failed to find an renv installation: the project will not be loaded.", + "Use `renv::activate()` to re-initialize the project." + ) + + warning(paste(msg, collapse = "\n"), call. = FALSE) + + } + + renv_json_read <- function(file = NULL, text = NULL) { + + jlerr <- NULL + + # if jsonlite is loaded, use that instead + if ("jsonlite" %in% loadedNamespaces()) { + + json <- catch(renv_json_read_jsonlite(file, text)) + if (!inherits(json, "error")) + return(json) + + jlerr <- json + + } + + # otherwise, fall back to the default JSON reader + json <- catch(renv_json_read_default(file, text)) + if (!inherits(json, "error")) + return(json) + + # report an error + if (!is.null(jlerr)) + stop(jlerr) + else + stop(json) + + } + + renv_json_read_jsonlite <- function(file = NULL, text = NULL) { + text <- paste(text %||% read(file), collapse = "\n") + jsonlite::fromJSON(txt = text, simplifyVector = FALSE) + } + + renv_json_read_default <- function(file = NULL, text = NULL) { + + # find strings in the JSON + text <- paste(text %||% read(file), collapse = "\n") + pattern <- '["](?:(?:\\\\.)|(?:[^"\\\\]))*?["]' + locs <- gregexpr(pattern, text, perl = TRUE)[[1]] + + # if any are found, replace them with placeholders + replaced <- text + strings <- character() + replacements <- character() + + if (!identical(c(locs), -1L)) { + + # get the string values + starts <- locs + ends <- locs + attr(locs, "match.length") - 1L + strings <- substring(text, starts, ends) + + # only keep those requiring escaping + strings <- grep("[[\\]{}:]", strings, perl = TRUE, value = TRUE) + + # compute replacements + replacements <- sprintf('"\032%i\032"', seq_along(strings)) + + # replace the strings + mapply(function(string, replacement) { + replaced <<- sub(string, replacement, replaced, fixed = TRUE) + }, strings, replacements) + + } + + # transform the JSON into something the R parser understands + transformed <- replaced + transformed <- gsub("{}", "`names<-`(list(), character())", transformed, fixed = TRUE) + transformed <- gsub("[[{]", "list(", transformed, perl = TRUE) + transformed <- gsub("[]}]", ")", transformed, perl = TRUE) + transformed <- gsub(":", "=", transformed, fixed = TRUE) + text <- paste(transformed, collapse = "\n") + + # parse it + json <- parse(text = text, keep.source = FALSE, srcfile = NULL)[[1L]] + + # construct map between source strings, replaced strings + map <- as.character(parse(text = strings)) + names(map) <- as.character(parse(text = replacements)) + + # convert to list + map <- as.list(map) + + # remap strings in object + remapped <- renv_json_remap(json, map) + + # evaluate + eval(remapped, envir = baseenv()) + + } + + renv_json_remap <- function(json, map) { + + # fix names + if (!is.null(names(json))) { + lhs <- match(names(json), names(map), nomatch = 0L) + rhs <- match(names(map), names(json), nomatch = 0L) + names(json)[rhs] <- map[lhs] + } + + # fix values + if (is.character(json)) + return(map[[json]] %||% json) + + # handle true, false, null + if (is.name(json)) { + text <- as.character(json) + if (text == "true") + return(TRUE) + else if (text == "false") + return(FALSE) + else if (text == "null") + return(NULL) + } + + # recurse + if (is.recursive(json)) { + for (i in seq_along(json)) { + json[i] <- list(renv_json_remap(json[[i]], map)) + } + } + + json + + } + + # load the renv profile, if any + renv_bootstrap_profile_load(project) + + # construct path to library root + root <- renv_bootstrap_library_root(project) + + # construct library prefix for platform + prefix <- renv_bootstrap_platform_prefix() + + # construct full libpath + libpath <- file.path(root, prefix) + + # run bootstrap code + renv_bootstrap_exec(project, libpath, version) + + invisible() + +}) diff --git a/renv/settings.json b/renv/settings.json new file mode 100644 index 0000000..74c1d4b --- /dev/null +++ b/renv/settings.json @@ -0,0 +1,19 @@ +{ + "bioconductor.version": null, + "external.libraries": [], + "ignored.packages": [], + "package.dependency.fields": [ + "Imports", + "Depends", + "LinkingTo" + ], + "ppm.enabled": null, + "ppm.ignored.urls": [], + "r.version": null, + "snapshot.type": "explicit", + "use.cache": true, + "vcs.ignore.cellar": true, + "vcs.ignore.library": true, + "vcs.ignore.local": true, + "vcs.manage.ignores": true +} diff --git a/tests/testthat.R b/tests/testthat.R new file mode 100644 index 0000000..7af360f --- /dev/null +++ b/tests/testthat.R @@ -0,0 +1,12 @@ +# This file is part of the standard setup for testthat. +# It is recommended that you do not modify it. +# +# Where should you do additional test configuration? +# Learn more about the roles of various files in: +# * https://r-pkgs.org/testing-design.html#sec-tests-files-overview +# * https://testthat.r-lib.org/articles/special-files.html + +library(testthat) +library(distilleR) + +test_check("distilleR") diff --git a/tests/testthat/test-buildAuthenticationRequest.R b/tests/testthat/test-buildAuthenticationRequest.R new file mode 100644 index 0000000..b6195e9 --- /dev/null +++ b/tests/testthat/test-buildAuthenticationRequest.R @@ -0,0 +1,29 @@ +test_that("The URL must be a string", { + expect_error( + .buildAuthenticationRequest( + distillerInstanceUrl = 123, + distillerKey = "DISTILLER_API_KEY")) +}) + +test_that("The API key must be a string", { + expect_error( + .buildAuthenticationRequest( + distillerInstanceUrl = "https://example.org", + distillerKey = 123)) +}) + +test_that("The timeout must be an integer", { + expect_error( + .buildAuthenticationRequest( + distillerInstanceUrl = "https://example.org", + distillerKey = "DISTILLER_API_KEY", + timeout = "")) +}) + +test_that("The result must be a httr2_request", { + expect_s3_class( + .buildAuthenticationRequest( + distillerInstanceUrl = "https://example.org", + distillerKey = "DISTILLER_API_KEY"), + "httr2_request") +}) diff --git a/tests/testthat/test-buildServiceRequest.R b/tests/testthat/test-buildServiceRequest.R new file mode 100644 index 0000000..e579df6 --- /dev/null +++ b/tests/testthat/test-buildServiceRequest.R @@ -0,0 +1,38 @@ +test_that("The URL must be a string", { + expect_error( + .buildServiceRequest( + serviceUrl = 123, + distillerToken = "DISTILLER_TOKEN")) +}) + +test_that("The token must be a string", { + expect_error( + .buildServiceRequest( + serviceUrl = "https://example.org/service", + distillerToken = 123)) +}) + +test_that("The body must be a list", { + expect_error( + .buildServiceRequest( + serviceUrl = "https://example.org/service", + distillerToken = "DISTILLER_TOKEN", + body = 123)) +}) + +test_that("The timeout must be an integer", { + expect_error( + .buildServiceRequest( + serviceUrl = "https://example.org/service", + distillerToken = "DISTILLER_TOKEN", + timeout = "")) +}) + +test_that("The result must be a httr2_request", { + expect_s3_class( + .buildServiceRequest( + serviceUrl = "https://example.org/service", + distillerToken = "DISTILLER_TOKEN", + body = list("a" = 1)), + "httr2_request") +}) diff --git a/tests/testthat/test-getAuthenticationToken.R b/tests/testthat/test-getAuthenticationToken.R new file mode 100644 index 0000000..1dfa293 --- /dev/null +++ b/tests/testthat/test-getAuthenticationToken.R @@ -0,0 +1,95 @@ +test_that("The url must be a string", { + expect_error( + getAuthenticationToken( + distillerInstanceUrl = 123, + distillerKey = "DISTILLER_API_KEY")) +}) + +test_that("The API key must be a string", { + expect_error( + getAuthenticationToken( + distillerInstanceUrl = "", + distillerKey = 123)) +}) + +test_that("The timeout must be an integer", { + expect_error( + getAuthenticationToken( + distillerInstanceUrl = "", + distillerKey = "DISTILLER_API_KEY", + timeout = "")) +}) + +test_that("Expect no errors if the request succeeds", { + response_ <- httr2::response( + status_code = 200, + url = "https://example.org/auth", + method = "POST", + headers = list("Content-Type" = "application/json"), + body = charToRaw("{\"token\": \"DISTILLER_TOKEN\"}")) + + with_mocked_bindings( + .performRequest = function(request, errorMessage) { + return(response_) + }, { + expect_no_error( + getAuthenticationToken( + distillerInstanceUrl = "https://example.org", + distillerKey = "DISTILLER_API_KEY")) + } + ) +}) + +test_that("Expect an error if a bad endpoint is specified", { + with_mocked_bindings( + .performRequest = function(request, errorMessage) { + if (request$url == "https://example.org/auth") { stop() } + }, { + expect_error( + getAuthenticationToken( + distillerInstanceUrl = "https://example.org", + distillerKey = "DISTILLER_API_KEY")) + } + ) +}) + +# This test requires the DISTILLER_INSTANCE_URL and DISTILLER_API_KEY +# environment variables to be set. +# This test performs real requests to the DistillerSR API. +test_that("Expect an error if a bad endpoint is specified", { + skip_on_cran() + + distillerInstanceUrl_ <- Sys.getenv("DISTILLER_INSTANCE_URL") + + expect_error( + getAuthenticationToken( + distillerInstanceUrl = glue("{distillerInstanceUrl_}/bad_endpoint"), + distillerKey = Sys.getenv("DISTILLER_API_KEY"))) +}) + +test_that("Expect an error if a wrong API key is specified", { + with_mocked_bindings( + .performRequest = function(request, errorMessage) { + authorization_ <- rlang::wref_value(request$headers$Authorization) + if (authorization_ == "Key BAD_API_KEY") { stop() } + }, { + expect_error( + getAuthenticationToken( + distillerInstanceUrl = "https://example.org", + distillerKey = "BAD_API_KEY")) + } + ) +}) + +# This test requires the DISTILLER_INSTANCE_URL environment variable to be set. +# This test performs real requests to the DistillerSR API. +test_that("Expect an error if a wrong API key is specified", { + skip_on_cran() + + distillerInstanceUrl_ <- Sys.getenv("DISTILLER_INSTANCE_URL") + + expect_error( + getAuthenticationToken( + distillerInstanceUrl = glue("{distillerInstanceUrl_}/bad_endpoint"), + distillerKey = Sys.getenv("BAD_API_KEY"))) +}) diff --git a/tests/testthat/test-getProjects.R b/tests/testthat/test-getProjects.R new file mode 100644 index 0000000..d48b18b --- /dev/null +++ b/tests/testthat/test-getProjects.R @@ -0,0 +1,106 @@ +test_that("The url must be a string", { + expect_error( + getProjects( + distillerInstanceUrl = 123, + distillerToken = "DISTILLER_TOKEN")) +}) + +test_that("The token must be a string", { + expect_error( + getProjects( + distillerInstanceUrl = "", + distillerToken = 123)) +}) + +test_that("The timeout must be an integer", { + expect_error( + getProjects( + distillerInstanceUrl = "", + distillerToken = "DISTILLER_TOKEN", + timeout = "")) +}) + +test_that("Expect an error if a bad instance URL is specified", { + with_mocked_bindings( + .performRequest = function(request, errorMessage) { + if (request$url == "https://invalid_instance/projects") { stop() } + }, { + expect_error( + getProjects( + distillerInstanceUrl = "https://invalid_instance", + distillerToken = "DISTILLER_TOKEN")) + } + ) +}) + +# This test requires the DISTILLER_INSTANCE_URL and DISTILLER_API_KEY +# environment variables to be set. +# This test performs real requests to the DistillerSR API. +test_that("Expect an error if a bad instance URL is specified", { + skip_on_cran() + + distillerInstanceUrl_ <- "https://invalid_instance" + distillerToken_ <- getAuthenticationToken() + + expect_error( + getProjects( + distillerInstanceUrl = distillerInstanceUrl_, + distillerToken = distillerToken_)) +}) + +test_that("Expect an error if a bad token is specified", { + with_mocked_bindings( + .performRequest = function(request, errorMessage) { + authorization_ <- rlang::wref_value(request$headers$Authorization) + if (authorization_ == "Bearer BAD_TOKEN") { stop() } + }, { + expect_error( + getProjects( + distillerToken = "BAD_TOKEN")) + } + ) +}) + +# This test requires the DISTILLER_INSTANCE_URL environment variable to be set. +# This test performs real requests to the DistillerSR API. +test_that("Expect an error if a bad token is specified", { + skip_on_cran() + + expect_error( + getProjects( + distillerToken = "BAD_TOKEN")) +}) + +test_that("A four-column tibble must be returned", { + response_ <- httr2::response( + status_code = 200, + url = "https://example.org/projects", + method = "GET", + headers = list("Content-Type" = "application/json"), + body = charToRaw("{\"a\": 1, \"b\": 2, \"c\": 3, \"d\": 4}")) + + with_mocked_bindings( + .performRequest = function(request, errorMessage) { + return(response_) + }, { + projects_ <- getProjects(distillerToken = "DISTILLER_TOKEN") + + expect_s3_class(projects_, "data.frame") + expect_equal(ncol(projects_), 4) + } + ) +}) + +# This test requires the DISTILLER_INSTANCE_URL and DISTILLER_API_KEY +# environment variables to be set. +# This test performs real requests to the DistillerSR API. +test_that("A four-column tibble must be returned", { + skip_on_cran() + + distillerToken_ <- getAuthenticationToken() + + projects_ <- getProjects(distillerToken = distillerToken_) + + expect_s3_class(projects_, "data.frame") + expect_equal(ncol(projects_), 4) +}) diff --git a/tests/testthat/test-getReport.R b/tests/testthat/test-getReport.R new file mode 100644 index 0000000..601cdbf --- /dev/null +++ b/tests/testthat/test-getReport.R @@ -0,0 +1,362 @@ +test_that("The project ID must be an integer", { + expect_error( + getReport( + projectId = 123, + reportId = 456, + format = "", + distillerInstanceUrl = "", + distillerToken = "DISTILLER_TOKEN", + timeout = 1800, + attempts = 3, + retryEach = 3600)) +}) + +test_that("The project ID must be an integer", { + expect_error( + getReport( + projectId = "", + reportId = 456, + format = "", + distillerInstanceUrl = "", + distillerToken = "DISTILLER_TOKEN", + timeout = 1800, + attempts = 3, + retryEach = 3600)) +}) + +test_that("The report ID must be an integer", { + expect_error( + getReport( + projectId = 123, + reportId = "", + format = "", + distillerInstanceUrl = "", + distillerToken = "DISTILLER_TOKEN", + timeout = 1800, + attempts = 3, + retryEach = 3600)) +}) + +test_that("The format must be a string", { + expect_error( + getReport( + projectId = 123, + reportId = 456, + format = 1, + distillerInstanceUrl = "", + distillerToken = "DISTILLER_TOKEN", + timeout = 1800, + attempts = 3, + retryEach = 3600)) +}) + +test_that("The format must be allowed", { + expect_error( + getReport( + projectId = 123, + reportId = 456, + format = "json", + distillerInstanceUrl = "", + distillerToken = "DISTILLER_TOKEN", + timeout = 1800, + attempts = 3, + retryEach = 3600)) +}) + +test_that("The url must be a string", { + expect_error( + getReport( + projectId = 123, + reportId = 456, + format = "", + distillerInstanceUrl = 1, + distillerToken = "DISTILLER_TOKEN", + timeout = 1800, + attempts = 3, + retryEach = 3600)) +}) + +test_that("The token must be a string", { + expect_error( + getReport( + projectId = 123, + reportId = 456, + format = "", + distillerInstanceUrl = "", + distillerToken = 1, + timeout = 1800, + attempts = 3, + retryEach = 3600)) +}) + +test_that("The timeout must be an integer", { + expect_error( + getReport( + projectId = 123, + reportId = 456, + format = "", + distillerInstanceUrl = "", + distillerToken = "DISTILLER_TOKEN", + timeout = "", + attempts = 3, + retryEach = 3600)) +}) + +test_that("The number of attempts must be an integer", { + expect_error( + getReport( + projectId = 123, + reportId = 456, + format = "", + distillerInstanceUrl = "", + distillerToken = "DISTILLER_TOKEN", + timeout = 1800, + attempts = "", + retryEach = 3600)) +}) + +test_that("The retry delay must be an integer", { + expect_error( + getReport( + projectId = 123, + reportId = 456, + format = "", + distillerInstanceUrl = "", + distillerToken = "DISTILLER_TOKEN", + timeout = 1800, + attempts = 3, + retryEach = "")) +}) + +test_that("The verbose flag must be a logical", { + expect_error( + getReport( + projectId = 123, + reportId = 456, + format = "", + distillerInstanceUrl = "", + distillerToken = "DISTILLER_TOKEN", + timeout = 1800, + attempts = 3, + retryEach = 3600, + verbose = 123)) +}) + +test_that("The attempts must be at least one", { + expect_error( + getReport( + projectId = 123, + reportId = 456, + format = "", + distillerInstanceUrl = "", + distillerToken = "DISTILLER_TOKEN", + timeout = 1800, + attempts = 0, + retryEach = 3600)) +}) + +test_that("The retry delay must be at least the timeout value", { + expect_error( + getReport( + projectId = 123, + reportId = 456, + format = "", + distillerInstanceUrl = "", + distillerToken = "DISTILLER_TOKEN", + timeout = 1800, + attempts = 3, + retryEach = 1700)) +}) + +test_that("Expect an error if a bad instance URL is specified", { + with_mocked_bindings( + .performRequest = function(request, errorMessage) { + invalidUrl_ <- "https://invalid_instance/datarama/query" + if (request$url == invalidUrl_) { stop() } + }, { + expect_error( + getReport( + projectId = 123, + reportId = 456, + format = "excel", + distillerInstanceUrl = "https://invalid_instance", + distillerToken = "DISTILLER_TOKEN", + retryEach = 1)) + } + ) +}) + +# This test requires the DISTILLER_INSTANCE_URL, DISTILLER_API_KEY, +# DISTILLER_PROJECT_ID_TEST, and DISTILLER_REPORT_ID_TEST environment variables +# to be set. +# This test performs real requests to the DistillerSR API. +test_that("Expect an error if a bad instance URL is specified", { + skip_on_cran() + + distillerInstanceUrl_ <- "https://invalid_instance" + distillerToken_ <- getAuthenticationToken() + distillerProjectId_ <- Sys.getenv("DISTILLER_PROJECT_ID_TEST") + distillerReportId_ <- Sys.getenv("DISTILLER_REPORT_ID_TEST") + + expect_error( + getReport( + projectId = as.integer(distillerProjectId_), + reportId = as.integer(distillerReportId_), + format = "excel", + distillerInstanceUrl = distillerInstanceUrl_, + distillerToken = "DISTILLER_TOKEN")) +}) + +test_that("Expect an error if a bad token is specified", { + with_mocked_bindings( + .performRequest = function(request, errorMessage) { + authorization_ <- rlang::wref_value(request$headers$Authorization) + if (authorization_ == "Bearer BAD_TOKEN") { stop() } + }, { + expect_error( + getReport( + projectId = 123, + reportId = 456, + format = "excel", + distillerInstanceUrl = "https://example.org", + distillerToken = "BAD_TOKEN")) + } + ) +}) + +# This test requires the DISTILLER_INSTANCE_URL, DISTILLER_PROJECT_ID_TEST, +# DISTILLER_REPORT_ID_TEST and environment variables to be set. +# This test performs real requests to the DistillerSR API. +test_that("Expect an error if a bad token is specified", { + skip_on_cran() + + distillerProjectId_ <- Sys.getenv("DISTILLER_PROJECT_ID_TEST") + distillerReportId_ <- Sys.getenv("DISTILLER_REPORT_ID_TEST") + + expect_error( + getReport( + projectId = as.integer(distillerProjectId_), + reportId = as.integer(distillerReportId_), + format = "excel", + distillerToken = "BAD_TOKEN")) +}) + +test_that("A tibble must be returned (XLSX)", { + dataframe_ <- data.frame(a = 1, b = 2, c = 3) + + xlsxTempFile_ <- tempfile(fileext = ".xlsx") + openxlsx::write.xlsx(dataframe_, xlsxTempFile_) + + bodyRaw_ <- readBin( + xlsxTempFile_, + what = "raw", + n = file.info(xlsxTempFile_)$size) + + response_ <- httr2::response( + status_code = 200, + url = "https://example.org/endpoint", + method = "POST", + headers = list( + "Content-Type" = + "application/vnd.openxmlformats-officedocuments.spreadsheetml.sheet"), + body = bodyRaw_) + + with_mocked_bindings( + .performRequest = function(request, errorMessage) { + return(response_) + }, { + reports_ <- getReport( + projectId = 123, + reportId = 456, + format = "excel", + distillerToken = "DISTILLER_TOKEN") + + expect_s3_class(reports_, "data.frame") + } + ) +}) + +# This test requires the DISTILLER_INSTANCE_URL, DISTILLER_API_KEY, +# DISTILLER_PROJECT_ID_TEST, and DISTILLER_REPORT_ID_TEST environment variables +# to be set. +# This test performs real requests to the DistillerSR API. +test_that("A tibble must be returned (XLSX)", { + skip_on_cran() + + distillerToken_ <- getAuthenticationToken() + distillerProjectId_ <- Sys.getenv("DISTILLER_PROJECT_ID_TEST") + distillerReportId_ <- Sys.getenv("DISTILLER_REPORT_ID_TEST") + + reports_ <- getReport( + projectId = as.integer(distillerProjectId_), + reportId = as.integer(distillerReportId_), + format = "excel", + distillerToken = distillerToken_) + + expect_s3_class(reports_, "data.frame") +}) + +test_that("A tibble must be returned (CSV)", { + response_ <- httr2::response( + status_code = 200, + url = "https://example.org/endpoint", + method = "POST", + headers = list("Content-Type" = "text/csv"), + body = charToRaw("a,b,c\n1,2,3")) + + with_mocked_bindings( + .performRequest = function(request, errorMessage) { + return(response_) + }, + read_csv = function(file, col_types, show_col_types) { + readr::read_csv(file, col_types = col_types, show_col_types = FALSE) + }, { + reports_ <- getReport( + projectId = 123, + reportId = 456, + format = "csv", + distillerToken = "DISTILLER_TOKEN") + + expect_s3_class(reports_, "data.frame") + } + ) +}) + +test_that("The delay mechanism must work properly", { + sleepCalled_ <- FALSE + + with_mocked_bindings( + .performRequest = function(...) { stop() }, + .sleep = function(seconds) { + sleepCalled_ <<- TRUE + }, { + expect_error( + getReport( + projectId = 123, + reportId = 456, + format = "csv", + distillerToken = "DISTILLER_TOKEN", + attempts = 2, + retryEach = 1, + verbose = FALSE)) + + expect_true(sleepCalled_) + } + ) +}) + +test_that("The verbosity must work properly", { + with_mocked_bindings( + .performRequest = function(...) { stop() }, + { + expect_error( + getReport( + projectId = 123, + reportId = 456, + format = "csv", + distillerToken = "DISTILLER_TOKEN", + attempts = 2, + retryEach = 1)) + } + ) +}) diff --git a/tests/testthat/test-getReports.R b/tests/testthat/test-getReports.R new file mode 100644 index 0000000..8c8d123 --- /dev/null +++ b/tests/testthat/test-getReports.R @@ -0,0 +1,131 @@ +test_that("The project ID must be an integer", { + expect_error( + getReports( + projectId = "", + distillerInstanceUrl = "", + distillerToken = "DISTILLER_TOKEN")) +}) + +test_that("The url must be a string", { + expect_error( + getReports( + projectId = 123, + distillerInstanceUrl = 123, + distillerToken = "DISTILLER_TOKEN")) +}) + +test_that("The token must be a string", { + expect_error( + getReports( + projectId = 123, + distillerInstanceUrl = "", + distillerToken = 123)) +}) + +test_that("The timeout must be an integer", { + expect_error( + getReports( + projectId = 123, + distillerInstanceUrl = "", + distillerToken = "DISTILLER_TOKEN", + timeout = "")) +}) + +test_that("Expect an error if a bad instance URL is specified", { + with_mocked_bindings( + .performRequest = function(request, errorMessage) { + invalidUrl_ <- "https://invalid_instance/projects/123/reports/datarama" + if (request$url == invalidUrl_) { stop() } + }, { + expect_error( + getReports( + projectId = 123, + distillerInstanceUrl = "https://invalid_instance", + distillerToken = "DISTILLER_TOKEN")) + } + ) +}) + +# This test requires the DISTILLER_INSTANCE_URL, DISTILLER_API_KEY, and +# DISTILLER_PROJECT_ID_TEST environment variables to be set. +# This test performs real requests to the DistillerSR API. +test_that("Expect an error if a bad instance URL is specified", { + skip_on_cran() + + distillerInstanceUrl_ <- "https://invalid_instance" + distillerToken_ <- getAuthenticationToken() + distillerProjectId_ <- Sys.getenv("DISTILLER_PROJECT_ID_TEST") + + expect_error( + getReports( + projectId = as.integer(distillerProjectId_), + distillerInstanceUrl = distillerInstanceUrl_, + distillerToken = distillerToken_)) +}) + +test_that("Expect an error if a bad token is specified", { + with_mocked_bindings( + .performRequest = function(request, errorMessage) { + authorization_ <- rlang::wref_value(request$headers$Authorization) + if (authorization_ == "Bearer BAD_TOKEN") { stop() } + }, { + expect_error( + getReports( + projectId = 123, + distillerToken = "BAD_TOKEN")) + } + ) +}) + +# This test requires the DISTILLER_INSTANCE_URL and DISTILLER_PROJECT_ID_TEST +# environment variables to be set. +# This test performs real requests to the DistillerSR API. +test_that("Expect an error if a bad token is specified", { + skip_on_cran() + + distillerProjectId_ <- Sys.getenv("DISTILLER_PROJECT_ID_TEST") + + expect_error( + getReports( + projectId = as.integer(distillerProjectId_), + distillerToken = "BAD_TOKEN")) +}) + +test_that("A four-column tibble must be returned", { + response_ <- httr2::response( + status_code = 200, + url = "https://example.org/projects", + method = "GET", + headers = list("Content-Type" = "application/json"), + body = charToRaw("{\"a\": 1, \"b\": 2, \"c\": 3, \"d\": 4}")) + + with_mocked_bindings( + .performRequest = function(request, errorMessage) { + return(response_) + }, { + reports_ <- getReports( + projectId = 123, + distillerToken = "DISTILLER_TOKEN") + + expect_s3_class(reports_, "data.frame") + expect_equal(ncol(reports_), 4) + } + ) +}) + +# This test requires the DISTILLER_INSTANCE_URL, DISTILLER_API_KEY, and +# DISTILLER_PROJECT_ID_TEST environment variables to be set. +# This test performs real requests to the DistillerSR API. +test_that("A four-column tibble must be returned", { + skip_on_cran() + + distillerToken_ <- getAuthenticationToken() + distillerProjectId_ <- Sys.getenv("DISTILLER_PROJECT_ID_TEST") + + reports_ <- getReports( + projectId = as.integer(distillerProjectId_), + distillerToken = distillerToken_) + + expect_s3_class(reports_, "data.frame") + expect_equal(ncol(reports_), 4) +}) diff --git a/tests/testthat/test-handleHTTPErrors.R b/tests/testthat/test-handleHTTPErrors.R new file mode 100644 index 0000000..20a775f --- /dev/null +++ b/tests/testthat/test-handleHTTPErrors.R @@ -0,0 +1,52 @@ +test_that("The response parameter must be an httr2_response object", { + expect_error(.handleHTTPErrors(response = 123)) +}) + +test_that("The error message parameter must be a string", { + expect_error(.handleHTTPErrors( + response = httr2::response(), + errorMessage = 123)) +}) + +test_that("The function must fail for invalid error codes", { + response_ <- httr2::response( + status_code = 403, + url = "https://example.org", + method = "POST", + headers = list("Content-Type" = "application/json"), + body = charToRaw("{'a': 1}")) + + expect_error( + .handleHTTPErrors( + response = response_)) +}) + +test_that("The function must succeed for valid error codes", { + response_ <- httr2::response( + status_code = 200, + url = "https://example.org", + method = "GET", + headers = list("Content-Type" = "application/json"), + body = charToRaw("{\"a\": 1}")) + + expect_no_error( + .handleHTTPErrors( + response = response_)) +}) + +# This test requires the DISTILLER_INSTANCE_URL and DISTILLER_API_KEY +# environment variables to be set. +# This test performs real requests to the DistillerSR API. +test_that("The function must succeed for valid error codes", { + skip_on_cran() + + request_ <- .buildAuthenticationRequest( + distillerInstanceUrl = Sys.getenv("DISTILLER_INSTANCE_URL"), + distillerKey = Sys.getenv("DISTILLER_API_KEY")) + + response_ <- .performRequest(request = request_) + + expect_no_error( + .handleHTTPErrors( + response = response_)) +}) diff --git a/tests/testthat/test-parseCSVResponse.R b/tests/testthat/test-parseCSVResponse.R new file mode 100644 index 0000000..ec11ef9 --- /dev/null +++ b/tests/testthat/test-parseCSVResponse.R @@ -0,0 +1,48 @@ +test_that("The response parameter must be an httr2_response object", { + expect_error( + .parseCSVResponse( + response = 123)) +}) + +test_that("The error message parameter must be a string", { + expect_error( + .parseCSVResponse( + response = httr2::response(), + errorMessage = 123)) +}) + +test_that("The verbose flag must be a logical", { + expect_error( + .parseCSVResponse( + response = httr2::response(), + errorMessage = "", + verbose = 123)) +}) + +test_that("The output must be a dataframe", { + response_ <- httr2::response( + status_code = 200, + url = "https://example.org/endpoint", + method = "POST", + headers = list("Content-Type" = "text/csv"), + body = charToRaw("a,b,c\n1,2,3")) + + expect_s3_class( + .parseCSVResponse( + response = response_, verbose = FALSE), + "data.frame") +}) + +test_that("Expect an error if the response delivers invalid data", { + response_ <- httr2::response( + status_code = 200, + url = "https://example.org/endpoint", + method = "POST", + headers = list( + "Content-Type" = "text/csv"), + body = charToRaw("invalid CSV data")) + + expect_error( + .parseCSVResponse( + response = response_)) +}) diff --git a/tests/testthat/test-parseJSONResponse.R b/tests/testthat/test-parseJSONResponse.R new file mode 100644 index 0000000..4c9cc55 --- /dev/null +++ b/tests/testthat/test-parseJSONResponse.R @@ -0,0 +1,38 @@ +test_that("The response parameter must be an httr2_response object", { + expect_error( + .parseJSONResponse( + response = 123)) +}) + +test_that("The error message parameter must be a string", { + expect_error( + .parseJSONResponse( + response = httr2::response(), + errorMessage = 123)) +}) + +test_that("Expect an error if the response delivers invalid data", { + response_ <- httr2::response( + status_code = 200, + url = "https://example.org/endpoint", + method = "POST", + headers = list("Content-Type" = "application/json"), + body = charToRaw("{'a': 1,")) + + expect_error( + .parseJSONResponse( + response = response_)) +}) + +test_that("Expect no error if all the parameters are correct", { + response_ <- httr2::response( + status_code = 200, + url = "https://example.org/endpoint", + method = "POST", + headers = list("Content-Type" = "application/json"), + body = charToRaw("{\"a\": 1}")) + + expect_no_error( + .parseJSONResponse( + response = response_)) +}) diff --git a/tests/testthat/test-parseXLSXResponse.R b/tests/testthat/test-parseXLSXResponse.R new file mode 100644 index 0000000..49a1f36 --- /dev/null +++ b/tests/testthat/test-parseXLSXResponse.R @@ -0,0 +1,53 @@ +test_that("The response parameter must be an httr2_response object", { + expect_error( + .parseXLSXResponse( + response = 123)) +}) + +test_that("The error message parameter must be a string", { + expect_error( + .parseXLSXResponse( + response = httr2::response(), + errorMessage = 123)) +}) + +test_that("The output must be a dataframe", { + df_ <- data.frame(a = 1, b = 2, c = 3) + + xlsxTempFile_ <- tempfile(fileext = ".xlsx") + openxlsx::write.xlsx(df_, xlsxTempFile_) + + bodyRaw_ <- readBin( + xlsxTempFile_, + what = "raw", + n = file.info(xlsxTempFile_)$size) + + response_ <- httr2::response( + status_code = 200, + url = "https://example.org/endpoint", + method = "POST", + headers = list( + "Content-Type" = + "application/vnd.openxmlformats-officedocuments.spreadsheetml.sheet"), + body = bodyRaw_) + + expect_s3_class( + .parseXLSXResponse( + response = response_), + "data.frame") +}) + +test_that("Expect an error if the response delivers invalid data", { + response_ <- httr2::response( + status_code = 200, + url = "https://example.org/endpoint", + method = "POST", + headers = list( + "Content-Type" = + "application/vnd.openxmlformats-officedocuments.spreadsheetml.sheet"), + body = charToRaw("invalid XLSX data")) + + expect_error( + .parseXLSXResponse( + response = response_)) +}) diff --git a/tests/testthat/test-performRequest.R b/tests/testthat/test-performRequest.R new file mode 100644 index 0000000..f6e8ced --- /dev/null +++ b/tests/testthat/test-performRequest.R @@ -0,0 +1,92 @@ +test_that("The request parameter must be an httr2_request object", { + expect_error( + .performRequest( + request = 123)) +}) + +test_that("The error message parameter must be a string", { + expect_error( + .performRequest( + request = httr2::request(), + errorMessage = 123)) +}) + +test_that("Expect an error if the request is malformed", { + request_ <- .buildAuthenticationRequest( + distillerInstanceUrl = "https://invalid_domain", + distillerKey = "api_key") + + expect_error( + .performRequest( + request = request_)) +}) + +test_that("Expect no error if the request object is correct", { + request_ <- .buildAuthenticationRequest( + distillerInstanceUrl = "DISTILLER_INSTANCE_URL", + distillerKey = "DISTILLER_API_KEY") + + response_ <- httr2::response( + status_code = 200) + + with_mocked_bindings( + req_perform = function(request) { + return(response_) + }, { + expect_no_error( + .performRequest( + request = request_)) + } + ) +}) + +# This test requires the DISTILLER_INSTANCE_URL and DISTILLER_API_KEY +# environment variables to be set. +# This test performs real requests to the DistillerSR API. +test_that("Expect no error if the request object is correct", { + skip_on_cran() + + request_ <- .buildAuthenticationRequest( + distillerInstanceUrl = Sys.getenv("DISTILLER_INSTANCE_URL"), + distillerKey = Sys.getenv("DISTILLER_API_KEY")) + + expect_no_error( + .performRequest( + request = request_)) +}) + +test_that("The result must be an httr2_response object", { + request_ <- .buildAuthenticationRequest( + distillerInstanceUrl = "DISTILLER_INSTANCE_URL", + distillerKey = "DISTILLER_API_KEY") + + response_ <- httr2::response( + status_code = 200) + + with_mocked_bindings( + req_perform = function(request) { + return(response_) + }, { + expect_s3_class( + .performRequest( + request = request_), + "httr2_response") + } + ) +}) + +# This test requires the DISTILLER_INSTANCE_URL and DISTILLER_API_KEY +# environment variables to be set. +# This test performs real requests to the DistillerSR API. +test_that("The result must be an httr2_response object", { + skip_on_cran() + + request_ <- .buildAuthenticationRequest( + distillerInstanceUrl = Sys.getenv("DISTILLER_INSTANCE_URL"), + distillerKey = Sys.getenv("DISTILLER_API_KEY")) + + expect_s3_class( + .performRequest( + request = request_), + "httr2_response") +}) diff --git a/tests/testthat/test-sleep.R b/tests/testthat/test-sleep.R new file mode 100644 index 0000000..5f1a04d --- /dev/null +++ b/tests/testthat/test-sleep.R @@ -0,0 +1,9 @@ +test_that("The number of seconds must be an integer", { + expect_error( + .sleep("1")) +}) + +test_that("Expect no errors if the number of seconds is correct", { + expect_no_error( + .sleep(1)) +}) diff --git a/vignettes/.gitignore b/vignettes/.gitignore new file mode 100644 index 0000000..097b241 --- /dev/null +++ b/vignettes/.gitignore @@ -0,0 +1,2 @@ +*.html +*.R diff --git a/vignettes/distilleR.Rmd b/vignettes/distilleR.Rmd new file mode 100644 index 0000000..5b24dde --- /dev/null +++ b/vignettes/distilleR.Rmd @@ -0,0 +1,174 @@ +--- +title: "distilleR" +output: rmarkdown::html_vignette +vignette: > + %\VignetteIndexEntry{distilleR} + %\VignetteEngine{knitr::rmarkdown} + %\VignetteEncoding{UTF-8} +--- + +```{r, include=FALSE} +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>" +) +``` + +## Overview + +The **distilleR** package provides a pool of functions to query **DistillerSR** through its APIs. +It features authentication and utilities to retrieve data from DistillerSR projects and reports. + +The package is intended for researchers, analysts, and practitioners who require convenient programmatic access to DistillerSR data. + +## Installation + +### From CRAN + +```{r eval=FALSE} +install.packages("distilleR") +``` + +### Development version (from GitHub) + +To install the latest development version: + +```{r eval=FALSE} +# install.packages("devtools") +devtools::install_github("openefsa/distilleR") +``` + +## Requirements + +An active internet connection is required, as the package communicates with DistillerSR online services to fetch and process data. + +## Working with API keys and environment variables + +The *distilleR* package requires an authentication token to function properly. The token can be obtained by authenticating through the `getAuthenticationToken()` function. The function needs your personal API key provided by DistillerSR. You can provide your API key in one of two ways: + +1. By setting it in the `.Renviron` file.\ +2. By including it manually in the authentication request. + +### Setting the API key via `.Renviron` + +The `.Renviron` file is used to define environment variables that R loads automatically at the start of each session. This approach is particularly convenient for sensitive information like API keys, as it allows you to use them in any R script or function without hardcoding them. + +Place the `.Renviron` file in your home directory (for example, `C:/Users/username/Documents/.Renviron` on Windows or `~/.Renviron` on Unix-like systems). You can create or edit this file with any plain text editor. + +Add your DistillerSR API key in the following format: + +`DISTILLER_API_KEY=` + +After saving the file, R will automatically read the API key on startup. + +### Setting the API key manually for the authentication request + +Alternatively, you can provide the API key directly in the `distillerKey` argument of the `getAuthenticationToken()` function. This is useful if you refer not to store the API key globally. For example: + +```{r apiKeyExample, eval=FALSE} +token <- getAuthenticationToken(distillerKey = "") +``` + +### Setting the DistillerSR instance URL + +The *distilleR* package needs to know the instance URL on which DistillerSR is running to function properly. You can provide the instance URL in one of two ways: + +1. By setting it in the `.Renviron` file.\ +2. By including it manually in each API request. + +If you prefer to store the URL in the `.Renviron` file, add your DistillerSR instance URL in the following format: + +`DISTILLER_INSTANCE_URL=` + +After saving the file, R will automatically read the API key on startup. + +Alternatively, you can provide the instance URL directly in the `distillerInstanceUrl` argument of functions that require it. This is useful if you refer not to store the instance URL globally. For example: + +```{r instanceUrlExample1, eval=FALSE} +token <- getAuthenticationToken( + distillerKey = "", + distillerInstanceUrl = "" +) +``` + +or + +```{r instanceUrlExample2, eval=FALSE} +projects <- getProjects( + token = token, + distillerInstanceUrl = "" +) +``` + +## Basic usage + +The main purpose of *distilleR* is to query the DistillerSR APIs for specific project or report codes and retrieve relevant information across various endpoints. + +Below are examples demonstrating how to use the functions in this package. First, load the *distilleR* package: + +```{r loadLibrary, eval=FALSE} +library(distilleR) +``` + +To explore the arguments and usage of a specific function, you can run: + +```{r eval=FALSE} +help("") +``` + +This will show the full documentation for the function, including its arguments, return values, and usage examples. + +For example, if you are working with the `getReport()` function, you can check its documentation with: + +```{r eval=FALSE} +help("getReport") +``` + +## Getting an authentication token + +Before using functions of this package, you must obtain an authentication token derived from the API key provided by DistillerSR. To to so, use the `getAuthenticationToken()` function: + +```{r tokenSetup, eval=FALSE} +token <- getAuthenticationToken() +``` + +The token can be now used to perform API calls using the `getProjects()`, `getReports()`, and `getReport()` functions. + +## Getting the list of projects associated with the user + +If you want to retrieve the list of all the available projects associated with your DistillerSR account, you can browse them with the `getProjects()` function, as follows: + +```{r projectsList, eval=FALSE} +projects <- getProjects(distillerToken = token) + +print(projects) +``` + +## Getting the list of reports associated with a project + +Each individual project has its own associated set of projects. You can retrieve the list of associated reports with the `getReports()` function, as follows: + +```{r reportsList, eval=FALSE} +reports <- getReports(projectId = 1234, distillerToken = token) + +print(reports) +``` + +## Getting a specific report + +You can retrieve a specific report with the `getReport()` function by specifying a project ID and a report ID, as follows: + +```{r getReport, eval=FALSE} +projectId_ <- 1234 +reportId_ <- 567 + +report <- getReport( + projectId = projectId_, + reportId = reportId_, + format = "excel", + distillerToken = token) + +print(head(report)) +``` + +Note that for very large reports, CSV files are generally a better choice. Exporting to Excel may cause issues when tables exceed one million rows, whereas CSV handles large datasets more reliably. From 2b9e1c2dd954a974aaa13fb0027e542733d30f12 Mon Sep 17 00:00:00 2001 From: Lorenzo Copelli Date: Tue, 7 Apr 2026 14:34:29 +0200 Subject: [PATCH 2/5] Fixed tests. (#2) --- tests/testthat/test-getAuthenticationToken.R | 4 +- tests/testthat/test-getProjects.R | 9 +- tests/testthat/test-getReport.R | 135 +++++++++++-------- tests/testthat/test-getReports.R | 8 +- 4 files changed, 95 insertions(+), 61 deletions(-) diff --git a/tests/testthat/test-getAuthenticationToken.R b/tests/testthat/test-getAuthenticationToken.R index 1dfa293..ca8763e 100644 --- a/tests/testthat/test-getAuthenticationToken.R +++ b/tests/testthat/test-getAuthenticationToken.R @@ -8,14 +8,14 @@ test_that("The url must be a string", { test_that("The API key must be a string", { expect_error( getAuthenticationToken( - distillerInstanceUrl = "", + distillerInstanceUrl = "https://example.org", distillerKey = 123)) }) test_that("The timeout must be an integer", { expect_error( getAuthenticationToken( - distillerInstanceUrl = "", + distillerInstanceUrl = "https://example.org", distillerKey = "DISTILLER_API_KEY", timeout = "")) }) diff --git a/tests/testthat/test-getProjects.R b/tests/testthat/test-getProjects.R index d48b18b..adb5f50 100644 --- a/tests/testthat/test-getProjects.R +++ b/tests/testthat/test-getProjects.R @@ -8,14 +8,14 @@ test_that("The url must be a string", { test_that("The token must be a string", { expect_error( getProjects( - distillerInstanceUrl = "", + distillerInstanceUrl = "https://example.org", distillerToken = 123)) }) test_that("The timeout must be an integer", { expect_error( getProjects( - distillerInstanceUrl = "", + distillerInstanceUrl = "https://example.org", distillerToken = "DISTILLER_TOKEN", timeout = "")) }) @@ -56,6 +56,7 @@ test_that("Expect an error if a bad token is specified", { }, { expect_error( getProjects( + distillerInstanceUrl = "https://example.org", distillerToken = "BAD_TOKEN")) } ) @@ -83,7 +84,9 @@ test_that("A four-column tibble must be returned", { .performRequest = function(request, errorMessage) { return(response_) }, { - projects_ <- getProjects(distillerToken = "DISTILLER_TOKEN") + projects_ <- getProjects( + distillerInstanceUrl = "https://example.org", + distillerToken = "DISTILLER_TOKEN") expect_s3_class(projects_, "data.frame") expect_equal(ncol(projects_), 4) diff --git a/tests/testthat/test-getReport.R b/tests/testthat/test-getReport.R index 601cdbf..95e5f6b 100644 --- a/tests/testthat/test-getReport.R +++ b/tests/testthat/test-getReport.R @@ -3,12 +3,13 @@ test_that("The project ID must be an integer", { getReport( projectId = 123, reportId = 456, - format = "", - distillerInstanceUrl = "", + format = "csv", + distillerInstanceUrl = "https://example.org", distillerToken = "DISTILLER_TOKEN", timeout = 1800, - attempts = 3, - retryEach = 3600)) + attempts = 1, + retryEach = 3600, + verbose = FALSE)) }) test_that("The project ID must be an integer", { @@ -16,12 +17,13 @@ test_that("The project ID must be an integer", { getReport( projectId = "", reportId = 456, - format = "", - distillerInstanceUrl = "", + format = "csv", + distillerInstanceUrl = "https://example.org", distillerToken = "DISTILLER_TOKEN", timeout = 1800, - attempts = 3, - retryEach = 3600)) + attempts = 1, + retryEach = 3600, + verbose = FALSE)) }) test_that("The report ID must be an integer", { @@ -29,12 +31,13 @@ test_that("The report ID must be an integer", { getReport( projectId = 123, reportId = "", - format = "", - distillerInstanceUrl = "", + format = "csv", + distillerInstanceUrl = "https://example.org", distillerToken = "DISTILLER_TOKEN", timeout = 1800, - attempts = 3, - retryEach = 3600)) + attempts = 1, + retryEach = 3600, + verbose = FALSE)) }) test_that("The format must be a string", { @@ -43,11 +46,12 @@ test_that("The format must be a string", { projectId = 123, reportId = 456, format = 1, - distillerInstanceUrl = "", + distillerInstanceUrl = "https://example.org", distillerToken = "DISTILLER_TOKEN", timeout = 1800, - attempts = 3, - retryEach = 3600)) + attempts = 1, + retryEach = 3600, + verbose = FALSE)) }) test_that("The format must be allowed", { @@ -56,11 +60,12 @@ test_that("The format must be allowed", { projectId = 123, reportId = 456, format = "json", - distillerInstanceUrl = "", + distillerInstanceUrl = "https://example.org", distillerToken = "DISTILLER_TOKEN", timeout = 1800, - attempts = 3, - retryEach = 3600)) + attempts = 1, + retryEach = 3600, + verbose = FALSE)) }) test_that("The url must be a string", { @@ -68,12 +73,13 @@ test_that("The url must be a string", { getReport( projectId = 123, reportId = 456, - format = "", + format = "csv", distillerInstanceUrl = 1, distillerToken = "DISTILLER_TOKEN", timeout = 1800, - attempts = 3, - retryEach = 3600)) + attempts = 1, + retryEach = 3600, + verbose = FALSE)) }) test_that("The token must be a string", { @@ -81,12 +87,13 @@ test_that("The token must be a string", { getReport( projectId = 123, reportId = 456, - format = "", - distillerInstanceUrl = "", + format = "csv", + distillerInstanceUrl = "https://example.org", distillerToken = 1, timeout = 1800, - attempts = 3, - retryEach = 3600)) + attempts = 1, + retryEach = 3600, + verbose = FALSE)) }) test_that("The timeout must be an integer", { @@ -94,12 +101,13 @@ test_that("The timeout must be an integer", { getReport( projectId = 123, reportId = 456, - format = "", - distillerInstanceUrl = "", + format = "csv", + distillerInstanceUrl = "https://example.org", distillerToken = "DISTILLER_TOKEN", timeout = "", - attempts = 3, - retryEach = 3600)) + attempts = 1, + retryEach = 3600, + verbose = FALSE)) }) test_that("The number of attempts must be an integer", { @@ -107,12 +115,13 @@ test_that("The number of attempts must be an integer", { getReport( projectId = 123, reportId = 456, - format = "", - distillerInstanceUrl = "", + format = "csv", + distillerInstanceUrl = "https://example.org", distillerToken = "DISTILLER_TOKEN", timeout = 1800, attempts = "", - retryEach = 3600)) + retryEach = 3600, + verbose = FALSE)) }) test_that("The retry delay must be an integer", { @@ -120,12 +129,13 @@ test_that("The retry delay must be an integer", { getReport( projectId = 123, reportId = 456, - format = "", - distillerInstanceUrl = "", + format = "csv", + distillerInstanceUrl = "https://example.org", distillerToken = "DISTILLER_TOKEN", timeout = 1800, - attempts = 3, - retryEach = "")) + attempts = 1, + retryEach = "", + verbose = FALSE)) }) test_that("The verbose flag must be a logical", { @@ -133,11 +143,11 @@ test_that("The verbose flag must be a logical", { getReport( projectId = 123, reportId = 456, - format = "", - distillerInstanceUrl = "", + format = "csv", + distillerInstanceUrl = "https://example.org", distillerToken = "DISTILLER_TOKEN", timeout = 1800, - attempts = 3, + attempts = 1, retryEach = 3600, verbose = 123)) }) @@ -147,12 +157,13 @@ test_that("The attempts must be at least one", { getReport( projectId = 123, reportId = 456, - format = "", - distillerInstanceUrl = "", + format = "csv", + distillerInstanceUrl = "https://example.org", distillerToken = "DISTILLER_TOKEN", timeout = 1800, attempts = 0, - retryEach = 3600)) + retryEach = 3600, + verbose = FALSE)) }) test_that("The retry delay must be at least the timeout value", { @@ -160,12 +171,13 @@ test_that("The retry delay must be at least the timeout value", { getReport( projectId = 123, reportId = 456, - format = "", - distillerInstanceUrl = "", + format = "csv", + distillerInstanceUrl = "https://example.org", distillerToken = "DISTILLER_TOKEN", timeout = 1800, - attempts = 3, - retryEach = 1700)) + attempts = 1, + retryEach = 1700, + verbose = FALSE)) }) test_that("Expect an error if a bad instance URL is specified", { @@ -181,7 +193,8 @@ test_that("Expect an error if a bad instance URL is specified", { format = "excel", distillerInstanceUrl = "https://invalid_instance", distillerToken = "DISTILLER_TOKEN", - retryEach = 1)) + retryEach = 1, + verbose = FALSE)) } ) }) @@ -204,7 +217,9 @@ test_that("Expect an error if a bad instance URL is specified", { reportId = as.integer(distillerReportId_), format = "excel", distillerInstanceUrl = distillerInstanceUrl_, - distillerToken = "DISTILLER_TOKEN")) + distillerToken = "DISTILLER_TOKEN", + attempts = 1, + verbose = FALSE)) }) test_that("Expect an error if a bad token is specified", { @@ -219,7 +234,9 @@ test_that("Expect an error if a bad token is specified", { reportId = 456, format = "excel", distillerInstanceUrl = "https://example.org", - distillerToken = "BAD_TOKEN")) + distillerToken = "BAD_TOKEN", + attempts = 1, + verbose = FALSE)) } ) }) @@ -238,7 +255,9 @@ test_that("Expect an error if a bad token is specified", { projectId = as.integer(distillerProjectId_), reportId = as.integer(distillerReportId_), format = "excel", - distillerToken = "BAD_TOKEN")) + distillerToken = "BAD_TOKEN", + attempts = 1, + verbose = FALSE)) }) test_that("A tibble must be returned (XLSX)", { @@ -269,7 +288,10 @@ test_that("A tibble must be returned (XLSX)", { projectId = 123, reportId = 456, format = "excel", - distillerToken = "DISTILLER_TOKEN") + distillerInstanceUrl = "https://example.org", + distillerToken = "DISTILLER_TOKEN", + attempts = 1, + verbose = FALSE) expect_s3_class(reports_, "data.frame") } @@ -291,7 +313,9 @@ test_that("A tibble must be returned (XLSX)", { projectId = as.integer(distillerProjectId_), reportId = as.integer(distillerReportId_), format = "excel", - distillerToken = distillerToken_) + distillerToken = distillerToken_, + attempts = 1, + verbose = FALSE) expect_s3_class(reports_, "data.frame") }) @@ -315,7 +339,10 @@ test_that("A tibble must be returned (CSV)", { projectId = 123, reportId = 456, format = "csv", - distillerToken = "DISTILLER_TOKEN") + distillerInstanceUrl = "https://example.org", + distillerToken = "DISTILLER_TOKEN", + attempts = 1, + verbose = FALSE) expect_s3_class(reports_, "data.frame") } @@ -335,6 +362,7 @@ test_that("The delay mechanism must work properly", { projectId = 123, reportId = 456, format = "csv", + distillerInstanceUrl = "https://example.org", distillerToken = "DISTILLER_TOKEN", attempts = 2, retryEach = 1, @@ -354,6 +382,7 @@ test_that("The verbosity must work properly", { projectId = 123, reportId = 456, format = "csv", + distillerInstanceUrl = "https://example.org", distillerToken = "DISTILLER_TOKEN", attempts = 2, retryEach = 1)) diff --git a/tests/testthat/test-getReports.R b/tests/testthat/test-getReports.R index 8c8d123..cd70456 100644 --- a/tests/testthat/test-getReports.R +++ b/tests/testthat/test-getReports.R @@ -2,7 +2,7 @@ test_that("The project ID must be an integer", { expect_error( getReports( projectId = "", - distillerInstanceUrl = "", + distillerInstanceUrl = "https://example.org", distillerToken = "DISTILLER_TOKEN")) }) @@ -18,7 +18,7 @@ test_that("The token must be a string", { expect_error( getReports( projectId = 123, - distillerInstanceUrl = "", + distillerInstanceUrl = "https://example.org", distillerToken = 123)) }) @@ -26,7 +26,7 @@ test_that("The timeout must be an integer", { expect_error( getReports( projectId = 123, - distillerInstanceUrl = "", + distillerInstanceUrl = "https://example.org", distillerToken = "DISTILLER_TOKEN", timeout = "")) }) @@ -72,6 +72,7 @@ test_that("Expect an error if a bad token is specified", { expect_error( getReports( projectId = 123, + distillerInstanceUrl = "https://example.org", distillerToken = "BAD_TOKEN")) } ) @@ -105,6 +106,7 @@ test_that("A four-column tibble must be returned", { }, { reports_ <- getReports( projectId = 123, + distillerInstanceUrl = "https://example.org", distillerToken = "DISTILLER_TOKEN") expect_s3_class(reports_, "data.frame") From cbc1ad33b92606dbce55232730760e12e06c2722 Mon Sep 17 00:00:00 2001 From: Lorenzo Copelli Date: Wed, 8 Apr 2026 18:45:53 +0200 Subject: [PATCH 3/5] Fixed authors. (#4) --- DESCRIPTION | 3 ++- man/distilleR-package.Rd | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 873bcce..c1b48b5 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -10,7 +10,8 @@ Authors@R: comment = c(ORCID = "0009-0002-4305-065X")), person(given = "Fulvio", family = "Barizzone", - role = "aut"), + role = "aut", + comment = c(ORCID = "0009-0006-3035-520X")), person(given = "Dayana Stephanie", family = "Buzle", role = "aut", diff --git a/man/distilleR-package.Rd b/man/distilleR-package.Rd index 2fad476..ea2aebb 100644 --- a/man/distilleR-package.Rd +++ b/man/distilleR-package.Rd @@ -24,7 +24,7 @@ Useful links: Authors: \itemize{ \item Lorenzo Copelli (\href{https://orcid.org/0009-0002-4305-065X}{ORCID}) - \item Fulvio Barizzone + \item Fulvio Barizzone (\href{https://orcid.org/0009-0006-3035-520X}{ORCID}) \item Dayana Stephanie Buzle (\href{https://orcid.org/0009-0003-2990-7431}{ORCID}) \item Rafael Vieira (\href{https://orcid.org/0009-0009-0289-5438}{ORCID}) } From 79bab6d4474cbc330bf10b2d3ecfd9310a84f35b Mon Sep 17 00:00:00 2001 From: Lorenzo Copelli Date: Mon, 13 Apr 2026 14:22:30 +0200 Subject: [PATCH 4/5] Fixed issues for CRAN submission. (#6) --- DESCRIPTION | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index c1b48b5..886ffee 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: distilleR Type: Package -Title: A wrap around the DistillerSR APIs +Title: A Wrap Around the DistillerSR APIs Version: 1.0.0 Maintainer: Luca Belmonte Authors@R: @@ -26,13 +26,13 @@ Authors@R: email = "luca.belmonte@efsa.europa.eu", comment = c(ORCID = "0000-0002-7977-9170"))) Description: The distilleR package provides a pool of functions to query - DistillerSR through its APIs. It features authentication and utilities to + DistillerSR through its APIs. It features authentication and utilities to retrieve data from DistillerSR projects and reports. License: file LICENSE URL: https://openefsa.github.io/distilleR BugReports: https://github.com/openefsa/distilleR/issues Depends: - R (>= 4.0.0) + R (>= 4.1.0) Imports: cli (>= 3.6.5), checkmate (>= 2.3.1), From 86334127fff59fe4073e07e73cf98afdf2d962b3 Mon Sep 17 00:00:00 2001 From: Lorenzo Copelli Date: Mon, 13 Apr 2026 14:48:47 +0200 Subject: [PATCH 5/5] Added missing files. (#8) --- man/distilleR-package.Rd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/man/distilleR-package.Rd b/man/distilleR-package.Rd index ea2aebb..16abea5 100644 --- a/man/distilleR-package.Rd +++ b/man/distilleR-package.Rd @@ -4,7 +4,7 @@ \name{distilleR-package} \alias{distilleR} \alias{distilleR-package} -\title{distilleR: A wrap around the DistillerSR APIs} +\title{distilleR: A Wrap Around the DistillerSR APIs} \description{ \if{html}{\figure{logo.png}{options: style='float: right' alt='logo' width='120'}}