├── .github ├── .gitignore ├── ISSUE_TEMPLATE │ └── issue_template.md ├── workflows │ ├── with-auth.yml │ ├── format-suggest.yaml │ ├── pkgdown.yaml │ ├── R-CMD-check.yaml │ ├── test-coverage.yaml │ └── pr-commands.yaml ├── SUPPORT.md └── CONTRIBUTING.md ├── revdep ├── README.md ├── failures.md ├── problems.md ├── .gitignore └── cran.md ├── air.toml ├── R ├── aaa.R ├── sysdata.rda ├── utils-pipe.R ├── schema_RowData.R ├── rectangle.R ├── zzz.R ├── schema_SheetProperties.R ├── gs4_browse.R ├── schema_ProtectedRange.R ├── schema_NamedRange.R ├── sheet_properties.R ├── gs4_fodder.R ├── gs4_endpoints.R ├── gs4_share.R ├── schema_GridCoordinate.R ├── gs4_get.R ├── schema_Sheet.R ├── gs4_find.R ├── roxygen.R ├── sheet_rename.R ├── schemas.R ├── schema_GridRange.R ├── sheet_delete.R ├── cell-specification.R ├── sheet_freeze.R ├── sheet_append.R └── range_add_named.R ├── vignettes ├── .gitignore └── articles │ ├── precompile.R │ ├── example-sheets.Rmd │ ├── auth.Rmd │ └── fun-with-googledrive-and-readxl.Rmd ├── LICENSE ├── man ├── roxygen │ └── templates │ │ ├── ss-return.R │ │ ├── skip-read.R │ │ ├── reformat.R │ │ ├── n_max.R │ │ └── range.R ├── figures │ ├── logo.png │ ├── lifecycle-retired.svg │ ├── lifecycle-defunct.svg │ ├── lifecycle-archived.svg │ ├── lifecycle-maturing.svg │ ├── lifecycle-deprecated.svg │ ├── lifecycle-superseded.svg │ ├── lifecycle-questioning.svg │ ├── lifecycle-experimental.svg │ ├── lifecycle-soft-deprecated.svg │ └── lifecycle-stable.svg ├── pipe.Rd ├── googlesheets4-vctrs.Rd ├── gs4_random.Rd ├── gs4_has_token.Rd ├── gs4_user.Rd ├── gs4_oauth_app.Rd ├── gs4_browse.Rd ├── gs4_fodder.Rd ├── gs4_endpoints.Rd ├── gs4_deauth.Rd ├── gs4_examples.Rd ├── gs4_token.Rd ├── googlesheets4-package.Rd ├── gs4_get.Rd ├── gs4_find.Rd ├── sheet_properties.Rd ├── gs4_scopes.Rd ├── cell-specification.Rd ├── gs4_formula.Rd ├── sheet_rename.Rd ├── sheet_delete.Rd ├── gs4_create.Rd ├── spread_sheet.Rd └── sheet_resize.Rd ├── tests ├── testthat.R ├── testthat │ ├── ref │ │ ├── dribble.rds │ │ └── googlesheets4-cell-tests.rds │ ├── _snaps │ │ ├── utils-ui.md │ │ ├── sheet_add.md │ │ ├── schemas.md │ │ ├── range_read.md │ │ └── gs4_auth.md │ ├── test-gs4_find.R │ ├── test-aaa.R │ ├── test-gs4_fodder.R │ ├── test-request_generate.R │ ├── test-gs4_endpoints.R │ ├── test-sheet_rename.R │ ├── test-sheet_append.R │ ├── test-sheet_relocate.R │ ├── test-make_column.R │ ├── test-sheet_delete.R │ ├── test-utils-ui.R │ ├── test-sheet_copy.R │ ├── test-utils-sheet.R │ ├── test-rectangle.R │ ├── test-range_speedread.R │ ├── helper.R │ ├── test-range_flood.R │ ├── test-sheet_add.R │ ├── test-schema_GridCoordinate.R │ ├── test-gs4_formula.R │ ├── test-gs4_example.R │ ├── test-utils.R │ ├── test-get_cells.R │ ├── test-ctype.R │ ├── test-schema_GridRange.R │ ├── test-sheet_write.R │ ├── test-sheet_resize.R │ ├── test-range_autofit.R │ ├── test-range_read_cells.R │ └── test-gs4_auth.R └── spelling.R ├── .vscode ├── extensions.json └── settings.json ├── pkgdown └── favicon │ ├── favicon.ico │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── apple-touch-icon.png │ ├── apple-touch-icon-60x60.png │ ├── apple-touch-icon-76x76.png │ ├── apple-touch-icon-120x120.png │ ├── apple-touch-icon-152x152.png │ └── apple-touch-icon-180x180.png ├── tools ├── logos │ ├── googlesheets4.png │ └── googlesheets4-og-1280x640.png └── test-fixtures │ ├── googlesheets4-cell-tests │ ├── range-experimentation.R │ ├── formulas.R │ └── empties-and-formats.R │ └── googlesheets4-col-types │ ├── na-is-honored.R │ └── lots-of-column-types.R ├── .gitignore ├── data-raw ├── old │ ├── 20170421_sheets-v4_endpoints.rds │ ├── 20170421_sheets-v4_endpoints-list.rds │ ├── 20170421_sheets-v4_endpoints-tibble.rds │ ├── 20180222_sheets-v4_endpoints-list.rds │ ├── 20180222_sheets-v4_endpoints-tibble.rds │ ├── 20180322_sheets-v4_endpoints-list.rds │ ├── 20180322_sheets-v4_endpoints-tibble.rds │ ├── 20181113_sheets-v4_endpoints-list.rds │ └── 20181113_sheets-v4_endpoints-tibble.rds ├── schemas │ ├── RowData │ ├── AddNamedRangeRequest │ ├── AddSheetRequest │ ├── DeleteDimensionRequest │ ├── AddProtectedRangeRequest │ ├── DeleteProtectedRangeRequest │ ├── BooleanCondition │ ├── ConditionValue │ ├── DeleteRangeRequest │ ├── InsertDimensionRequest │ ├── UpdateNamedRangeRequest │ ├── NamedRange │ ├── GridCoordinate │ ├── UpdateSheetPropertiesRequest │ ├── UpdateProtectedRangeRequest │ ├── Color │ ├── RepeatCellRequest │ ├── DimensionRange │ ├── AutoResizeDimensionsRequest │ ├── SetDataValidationRequest │ ├── Editors │ ├── DataValidationRule │ ├── DuplicateSheetRequest │ ├── AppendCellsRequest │ ├── UpdateCellsRequest │ ├── GridRange │ ├── GridProperties │ ├── ProtectedRange │ ├── TextFormat │ ├── Spreadsheet │ ├── SpreadsheetProperties │ ├── CellFormat │ ├── SheetProperties │ ├── CellData │ └── Sheet ├── errors │ ├── 404-nonexistent-sheet.R │ └── 404-nonexistent-sheet.md ├── see-all-examples.R ├── deaths-example-sheet.R ├── googlesheets4-col-types-NAs.R ├── gapminder-example-sheets.R ├── gs4-examples-inventory.R └── schema-rectangling.R ├── cran-comments.md ├── codecov.yml ├── .Rbuildignore ├── googlesheets4.Rproj ├── inst ├── extdata │ ├── fake-oauth-client-id-and-secret.json │ └── example_and_test_sheets.csv └── WORDLIST ├── LICENSE.md └── DESCRIPTION /.github/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | -------------------------------------------------------------------------------- /revdep/README.md: -------------------------------------------------------------------------------- 1 | # Revdeps 2 | 3 | -------------------------------------------------------------------------------- /air.toml: -------------------------------------------------------------------------------- 1 | [format] 2 | skip = ["tribble"] 3 | -------------------------------------------------------------------------------- /revdep/failures.md: -------------------------------------------------------------------------------- 1 | *Wow, no problems at all. :)* -------------------------------------------------------------------------------- /revdep/problems.md: -------------------------------------------------------------------------------- 1 | *Wow, no problems at all. :)* -------------------------------------------------------------------------------- /R/aaa.R: -------------------------------------------------------------------------------- 1 | .googlesheets4 <- new.env(parent = emptyenv()) 2 | -------------------------------------------------------------------------------- /vignettes/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | *.R 3 | !precompile.R 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2023 2 | COPYRIGHT HOLDER: googlesheets4 authors 3 | -------------------------------------------------------------------------------- /R/sysdata.rda: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidyverse/googlesheets4/HEAD/R/sysdata.rda -------------------------------------------------------------------------------- /man/roxygen/templates/ss-return.R: -------------------------------------------------------------------------------- 1 | #' @return The input `ss`, as an instance of [`sheets_id`] 2 | -------------------------------------------------------------------------------- /man/figures/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidyverse/googlesheets4/HEAD/man/figures/logo.png -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | library(testthat) 2 | library(googlesheets4) 3 | 4 | test_check("googlesheets4") 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "Posit.air-vscode" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /pkgdown/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidyverse/googlesheets4/HEAD/pkgdown/favicon/favicon.ico -------------------------------------------------------------------------------- /tools/logos/googlesheets4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidyverse/googlesheets4/HEAD/tools/logos/googlesheets4.png -------------------------------------------------------------------------------- /tests/testthat/ref/dribble.rds: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidyverse/googlesheets4/HEAD/tests/testthat/ref/dribble.rds -------------------------------------------------------------------------------- /pkgdown/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidyverse/googlesheets4/HEAD/pkgdown/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /pkgdown/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidyverse/googlesheets4/HEAD/pkgdown/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /revdep/.gitignore: -------------------------------------------------------------------------------- 1 | checks 2 | library 3 | checks.noindex 4 | library.noindex 5 | cloud.noindex 6 | data.sqlite 7 | *.html 8 | -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidyverse/googlesheets4/HEAD/pkgdown/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidyverse/googlesheets4/HEAD/pkgdown/favicon/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidyverse/googlesheets4/HEAD/pkgdown/favicon/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /tools/logos/googlesheets4-og-1280x640.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidyverse/googlesheets4/HEAD/tools/logos/googlesheets4-og-1280x640.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidyverse/googlesheets4/HEAD/pkgdown/favicon/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidyverse/googlesheets4/HEAD/pkgdown/favicon/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidyverse/googlesheets4/HEAD/pkgdown/favicon/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | internal 2 | .Rproj.user 3 | .Rhistory 4 | .RData 5 | .Ruserdata 6 | inst/doc 7 | scratch.R 8 | *cache* 9 | docs/ 10 | *.xlsx 11 | -------------------------------------------------------------------------------- /data-raw/old/20170421_sheets-v4_endpoints.rds: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidyverse/googlesheets4/HEAD/data-raw/old/20170421_sheets-v4_endpoints.rds -------------------------------------------------------------------------------- /tests/testthat/ref/googlesheets4-cell-tests.rds: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidyverse/googlesheets4/HEAD/tests/testthat/ref/googlesheets4-cell-tests.rds -------------------------------------------------------------------------------- /data-raw/old/20170421_sheets-v4_endpoints-list.rds: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidyverse/googlesheets4/HEAD/data-raw/old/20170421_sheets-v4_endpoints-list.rds -------------------------------------------------------------------------------- /data-raw/old/20170421_sheets-v4_endpoints-tibble.rds: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidyverse/googlesheets4/HEAD/data-raw/old/20170421_sheets-v4_endpoints-tibble.rds -------------------------------------------------------------------------------- /data-raw/old/20180222_sheets-v4_endpoints-list.rds: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidyverse/googlesheets4/HEAD/data-raw/old/20180222_sheets-v4_endpoints-list.rds -------------------------------------------------------------------------------- /data-raw/old/20180222_sheets-v4_endpoints-tibble.rds: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidyverse/googlesheets4/HEAD/data-raw/old/20180222_sheets-v4_endpoints-tibble.rds -------------------------------------------------------------------------------- /data-raw/old/20180322_sheets-v4_endpoints-list.rds: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidyverse/googlesheets4/HEAD/data-raw/old/20180322_sheets-v4_endpoints-list.rds -------------------------------------------------------------------------------- /data-raw/old/20180322_sheets-v4_endpoints-tibble.rds: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidyverse/googlesheets4/HEAD/data-raw/old/20180322_sheets-v4_endpoints-tibble.rds -------------------------------------------------------------------------------- /data-raw/old/20181113_sheets-v4_endpoints-list.rds: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidyverse/googlesheets4/HEAD/data-raw/old/20181113_sheets-v4_endpoints-list.rds -------------------------------------------------------------------------------- /data-raw/old/20181113_sheets-v4_endpoints-tibble.rds: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidyverse/googlesheets4/HEAD/data-raw/old/20181113_sheets-v4_endpoints-tibble.rds -------------------------------------------------------------------------------- /tests/testthat/_snaps/utils-ui.md: -------------------------------------------------------------------------------- 1 | # abort_unsupported_conversion() works 2 | 3 | Don't know how to make an instance of from something of class . 4 | 5 | -------------------------------------------------------------------------------- /tests/spelling.R: -------------------------------------------------------------------------------- 1 | if (requireNamespace("spelling", quietly = TRUE)) { 2 | spelling::spell_check_test( 3 | vignettes = TRUE, 4 | error = FALSE, 5 | skip_on_cran = TRUE 6 | ) 7 | } 8 | -------------------------------------------------------------------------------- /tests/testthat/test-gs4_find.R: -------------------------------------------------------------------------------- 1 | test_that("gs4_find() works", { 2 | skip_if_offline() 3 | skip_if_no_token() 4 | 5 | df <- gs4_find(n_max = 5) 6 | expect_s3_class(df, "dribble") 7 | }) 8 | -------------------------------------------------------------------------------- /cran-comments.md: -------------------------------------------------------------------------------- 1 | ## revdepcheck results 2 | 3 | We checked 17 reverse dependencies, comparing R CMD check results across CRAN and dev versions of this package. 4 | 5 | * We saw 0 new problems 6 | * We failed to check 0 packages 7 | -------------------------------------------------------------------------------- /revdep/cran.md: -------------------------------------------------------------------------------- 1 | ## revdepcheck results 2 | 3 | We checked 17 reverse dependencies, comparing R CMD check results across CRAN and dev versions of this package. 4 | 5 | * We saw 0 new problems 6 | * We failed to check 0 packages 7 | 8 | -------------------------------------------------------------------------------- /tests/testthat/test-aaa.R: -------------------------------------------------------------------------------- 1 | test_that("token registered with googlesheets4 and googledrive", { 2 | skip_if_offline() 3 | skip_if_no_token() 4 | 5 | expect_true(gs4_has_token()) 6 | expect_true(googledrive::drive_has_token()) 7 | }) 8 | -------------------------------------------------------------------------------- /man/roxygen/templates/skip-read.R: -------------------------------------------------------------------------------- 1 | #' @param skip Minimum number of rows to skip before reading anything, be it 2 | #' column names or data. Leading empty rows are automatically skipped, so this 3 | #' is a lower bound. Ignored if `range` is given. 4 | -------------------------------------------------------------------------------- /data-raw/schemas/RowData: -------------------------------------------------------------------------------- 1 | # RowData 2 | # A tibble: 1 × 6 3 | property type instance_of array_of format enum 4 | 5 | 1 values array CellData 6 | -------------------------------------------------------------------------------- /R/utils-pipe.R: -------------------------------------------------------------------------------- 1 | #' Pipe operator 2 | #' 3 | #' See \code{magrittr::\link[magrittr]{\%>\%}} for details. 4 | #' 5 | #' @name %>% 6 | #' @rdname pipe 7 | #' @keywords internal 8 | #' @export 9 | #' @importFrom magrittr %>% 10 | #' @usage lhs \%>\% rhs 11 | NULL 12 | -------------------------------------------------------------------------------- /man/roxygen/templates/reformat.R: -------------------------------------------------------------------------------- 1 | #' @param reformat Logical, indicates whether to reformat the affected cells. 2 | #' Currently googlesheets4 provides no real support for formatting, so 3 | #' `reformat = TRUE` effectively means that edited cells become unformatted. 4 | -------------------------------------------------------------------------------- /data-raw/schemas/AddNamedRangeRequest: -------------------------------------------------------------------------------- 1 | # AddNamedRangeRequest 2 | # A tibble: 1 × 6 3 | property type instance_of array_of format enum 4 | 5 | 1 namedRange object NamedRange 6 | -------------------------------------------------------------------------------- /data-raw/schemas/AddSheetRequest: -------------------------------------------------------------------------------- 1 | # AddSheetRequest 2 | # A tibble: 1 × 6 3 | property type instance_of array_of format enum 4 | 5 | 1 properties object SheetProperties 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[r]": { 3 | "editor.formatOnSave": true, 4 | "editor.defaultFormatter": "Posit.air-vscode" 5 | }, 6 | "[quarto]": { 7 | "editor.formatOnSave": true, 8 | "editor.defaultFormatter": "quarto.quarto" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /data-raw/schemas/DeleteDimensionRequest: -------------------------------------------------------------------------------- 1 | # DeleteDimensionRequest 2 | # A tibble: 1 × 6 3 | property type instance_of array_of format enum 4 | 5 | 1 range object DimensionRange 6 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | 3 | coverage: 4 | status: 5 | project: 6 | default: 7 | target: auto 8 | threshold: 1% 9 | informational: true 10 | patch: 11 | default: 12 | target: auto 13 | threshold: 1% 14 | informational: true 15 | -------------------------------------------------------------------------------- /tests/testthat/test-gs4_fodder.R: -------------------------------------------------------------------------------- 1 | test_that("gs4_fodder() works", { 2 | dat <- gs4_fodder(3, 5) 3 | expect_named(dat, LETTERS[1:5]) 4 | ltrs <- rep(LETTERS[1:5], each = 3) 5 | nbrs <- rep(1:3, 5) + 1 6 | expect_equal( 7 | as.vector(as.matrix(dat)), 8 | paste0(ltrs, nbrs) 9 | ) 10 | }) 11 | -------------------------------------------------------------------------------- /vignettes/articles/precompile.R: -------------------------------------------------------------------------------- 1 | # articles that I only want to compile intentionally 2 | 3 | library(knitr) 4 | 5 | knit( 6 | # the leading `_` keeps pkgdown from re-rendering the original 7 | "vignettes/articles/_dates-and-times.Rmd.orig.Rmd", 8 | "vignettes/articles/dates-and-times.Rmd" 9 | ) 10 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/sheet_add.md: -------------------------------------------------------------------------------- 1 | # sheet_add() rejects non-character `sheet` 2 | 3 | Code 4 | sheet_add(test_sheet("googlesheets4-cell-tests"), sheet = 3) 5 | Condition 6 | Error in `sheet_add()`: 7 | ! `sheet` must be : 8 | x `sheet` has class . 9 | 10 | -------------------------------------------------------------------------------- /data-raw/schemas/AddProtectedRangeRequest: -------------------------------------------------------------------------------- 1 | # AddProtectedRangeRequest 2 | # A tibble: 1 × 6 3 | property type instance_of array_of format enum 4 | 5 | 1 protectedRange object ProtectedRange 6 | -------------------------------------------------------------------------------- /man/pipe.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils-pipe.R 3 | \name{\%>\%} 4 | \alias{\%>\%} 5 | \title{Pipe operator} 6 | \usage{ 7 | lhs \%>\% rhs 8 | } 9 | \description{ 10 | See \code{magrittr::\link[magrittr]{\%>\%}} for details. 11 | } 12 | \keyword{internal} 13 | -------------------------------------------------------------------------------- /data-raw/schemas/DeleteProtectedRangeRequest: -------------------------------------------------------------------------------- 1 | # DeleteProtectedRangeRequest 2 | # A tibble: 1 × 6 3 | property type instance_of array_of format enum 4 | 5 | 1 protectedRangeId integer int32 6 | -------------------------------------------------------------------------------- /R/schema_RowData.R: -------------------------------------------------------------------------------- 1 | # creates an array of instances of RowData 2 | as_RowData <- function(df, col_names = TRUE) { 3 | df_cells <- purrr::modify(df, as_CellData) 4 | df_rows <- pmap(df_cells, list) 5 | if (col_names) { 6 | df_rows <- c(list(as_CellData(names(df))), df_rows) 7 | } 8 | map(df_rows, ~ list(values = unname(.x))) 9 | } 10 | -------------------------------------------------------------------------------- /tools/test-fixtures/googlesheets4-cell-tests/range-experimentation.R: -------------------------------------------------------------------------------- 1 | devtools::load_all() # I assume we're in googlesheets4 source 2 | 3 | googlesheets4:::gs4_auth_testing() 4 | 5 | ss <- test_sheet_create("googlesheets4-cell-tests") 6 | gs4_browse(ss) 7 | 8 | df <- gs4_fodder(5) 9 | sheet_write(df, ss, sheet = "range-experimentation") 10 | -------------------------------------------------------------------------------- /data-raw/schemas/BooleanCondition: -------------------------------------------------------------------------------- 1 | # BooleanCondition 2 | # A tibble: 2 × 6 3 | property type instance_of array_of format enum 4 | 5 | 1 type enum 6 | 2 values array ConditionValue 7 | -------------------------------------------------------------------------------- /data-raw/schemas/ConditionValue: -------------------------------------------------------------------------------- 1 | # ConditionValue 2 | # A tibble: 2 × 6 3 | property type instance_of array_of format enum 4 | 5 | 1 relativeDate enum 6 | 2 userEnteredValue string 7 | -------------------------------------------------------------------------------- /data-raw/schemas/DeleteRangeRequest: -------------------------------------------------------------------------------- 1 | # DeleteRangeRequest 2 | # A tibble: 2 × 6 3 | property type instance_of array_of format enum 4 | 5 | 1 range object GridRange 6 | 2 shiftDimension enum 7 | -------------------------------------------------------------------------------- /man/roxygen/templates/n_max.R: -------------------------------------------------------------------------------- 1 | #' @param n_max Maximum number of data rows to parse into the returned tibble. 2 | #' Trailing empty rows are automatically skipped, so this is an upper bound on 3 | #' the number of rows in the result. Ignored if `range` is given. `n_max` is 4 | #' imposed locally, after reading all non-empty cells, so, if speed is an 5 | #' issue, it is better to use `range`. 6 | -------------------------------------------------------------------------------- /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^internal$ 2 | ^\.Rproj\.user$ 3 | ^docs$ 4 | ^codecov\.yml$ 5 | ^data-raw$ 6 | ^scratch\.R$ 7 | ^old$ 8 | ^googlesheets4\.Rproj$ 9 | ^LICENSE\.md$ 10 | ^vignettes$ 11 | ^README\.Rmd$ 12 | ^\.github$ 13 | ^pkgdown$ 14 | ^index\.Rmd$ 15 | ^index\.md$ 16 | ^cran-comments\.md$ 17 | ^CRAN-RELEASE$ 18 | ^tools$ 19 | ^revdep$ 20 | ^CRAN-SUBMISSION$ 21 | ^[.]?air[.]toml$ 22 | ^\.vscode$ 23 | -------------------------------------------------------------------------------- /data-raw/schemas/InsertDimensionRequest: -------------------------------------------------------------------------------- 1 | # InsertDimensionRequest 2 | # A tibble: 2 × 6 3 | property type instance_of array_of format enum 4 | 5 | 1 inheritFromBefore boolean 6 | 2 range object DimensionRange 7 | -------------------------------------------------------------------------------- /data-raw/schemas/UpdateNamedRangeRequest: -------------------------------------------------------------------------------- 1 | # UpdateNamedRangeRequest 2 | # A tibble: 2 × 6 3 | property type instance_of array_of format enum 4 | 5 | 1 fields string google-fieldmask 6 | 2 namedRange object NamedRange 7 | -------------------------------------------------------------------------------- /R/rectangle.R: -------------------------------------------------------------------------------- 1 | # hack-y implementation of typed pluck with an NA default 2 | glean_lgl <- function(.x, ..., .default = NA) { 3 | map_lgl(list(.x), ..., .default = .default) 4 | } 5 | 6 | glean_chr <- function(.x, ..., .default = NA) { 7 | map_chr(list(.x), ..., .default = .default) 8 | } 9 | 10 | glean_int <- function(.x, ..., .default = NA) { 11 | map_int(list(.x), ..., .default = .default) 12 | } 13 | -------------------------------------------------------------------------------- /data-raw/schemas/NamedRange: -------------------------------------------------------------------------------- 1 | # NamedRange 2 | # A tibble: 3 × 6 3 | property type instance_of array_of format enum 4 | 5 | 1 name string 6 | 2 namedRangeId string 7 | 3 range object GridRange 8 | -------------------------------------------------------------------------------- /data-raw/schemas/GridCoordinate: -------------------------------------------------------------------------------- 1 | # GridCoordinate 2 | # A tibble: 3 × 6 3 | property type instance_of array_of format enum 4 | 5 | 1 columnIndex integer int32 6 | 2 rowIndex integer int32 7 | 3 sheetId integer int32 8 | -------------------------------------------------------------------------------- /tests/testthat/test-request_generate.R: -------------------------------------------------------------------------------- 1 | test_that("can generate a basic request", { 2 | req <- request_generate( 3 | "sheets.spreadsheets.get", 4 | list(spreadsheetId = "abc123"), 5 | token = NULL 6 | ) 7 | expect_identical(req$method, "GET") 8 | expect_match( 9 | req$url, 10 | "^https://sheets.googleapis.com/v4/spreadsheets/abc123\\?key=.+" 11 | ) 12 | expect_null(req$token) 13 | }) 14 | -------------------------------------------------------------------------------- /data-raw/schemas/UpdateSheetPropertiesRequest: -------------------------------------------------------------------------------- 1 | # UpdateSheetPropertiesRequest 2 | # A tibble: 2 × 6 3 | property type instance_of array_of format enum 4 | 5 | 1 fields string google-fieldmask 6 | 2 properties object SheetProperties 7 | -------------------------------------------------------------------------------- /data-raw/schemas/UpdateProtectedRangeRequest: -------------------------------------------------------------------------------- 1 | # UpdateProtectedRangeRequest 2 | # A tibble: 2 × 6 3 | property type instance_of array_of format enum 4 | 5 | 1 fields string google-fieldmask 6 | 2 protectedRange object ProtectedRange 7 | -------------------------------------------------------------------------------- /data-raw/schemas/Color: -------------------------------------------------------------------------------- 1 | # Color 2 | # A tibble: 4 × 6 3 | property type instance_of array_of format enum 4 | 5 | 1 alpha number float 6 | 2 blue number float 7 | 3 green number float 8 | 4 red number float 9 | -------------------------------------------------------------------------------- /data-raw/schemas/RepeatCellRequest: -------------------------------------------------------------------------------- 1 | # RepeatCellRequest 2 | # A tibble: 3 × 6 3 | property type instance_of array_of format enum 4 | 5 | 1 cell object CellData 6 | 2 fields string google-fieldmask 7 | 3 range object GridRange 8 | -------------------------------------------------------------------------------- /data-raw/schemas/DimensionRange: -------------------------------------------------------------------------------- 1 | # DimensionRange 2 | # A tibble: 4 × 6 3 | property type instance_of array_of format enum 4 | 5 | 1 dimension enum 6 | 2 endIndex integer int32 7 | 3 sheetId integer int32 8 | 4 startIndex integer int32 9 | -------------------------------------------------------------------------------- /data-raw/schemas/AutoResizeDimensionsRequest: -------------------------------------------------------------------------------- 1 | # AutoResizeDimensionsRequest 2 | # A tibble: 2 × 6 3 | property type instance_of array_of format enum 4 | 5 | 1 dataSourceSheetDimensions object DataSourceSheetDimensionRange 6 | 2 dimensions object DimensionRange 7 | -------------------------------------------------------------------------------- /tests/testthat/test-gs4_endpoints.R: -------------------------------------------------------------------------------- 1 | test_that("endpoints can be retrieved en masse", { 2 | endpoints <- gs4_endpoints() 3 | expect_true(length(endpoints) >= 14) 4 | expect_match(names(endpoints), "^sheets\\.spreadsheets\\.") 5 | }) 6 | 7 | test_that("a single endpoint can be retrieved", { 8 | nm <- "sheets.spreadsheets.values.batchClear" 9 | endpoint <- gs4_endpoints(nm)[[1]] 10 | expect_true( 11 | all(c("id", "path", "parameters", "scopes") %in% names(endpoint)) 12 | ) 13 | }) 14 | -------------------------------------------------------------------------------- /data-raw/errors/404-nonexistent-sheet.R: -------------------------------------------------------------------------------- 1 | #' --- 2 | #' output: github_document 3 | #' --- 4 | 5 | #+ error = TRUE 6 | 7 | devtools::load_all(".") 8 | 9 | req <- request_generate( 10 | "spreadsheets.get", 11 | ## ID of 'googlesheets4-design-exploration', but replaced last 2 chars w/ '-' 12 | list(spreadsheetId = "1xTUxWGcFLtDIHoYJ1WsjQuLmpUtBf--8Bcu5lQ302--"), 13 | token = NULL 14 | ) 15 | raw_resp <- request_make(req) 16 | response_process(raw_resp) 17 | 18 | ct <- httr::content(raw_resp) 19 | str(ct) 20 | -------------------------------------------------------------------------------- /data-raw/schemas/SetDataValidationRequest: -------------------------------------------------------------------------------- 1 | # SetDataValidationRequest 2 | # A tibble: 3 × 6 3 | property type instance_of array_of format enum 4 | 5 | 1 filteredRowsIncluded boolean 6 | 2 range object GridRange 7 | 3 rule object DataValidationRule 8 | -------------------------------------------------------------------------------- /data-raw/schemas/Editors: -------------------------------------------------------------------------------- 1 | # Editors 2 | # A tibble: 3 × 7 3 | property type instance_of array_of items format enum 4 | 5 | 1 domainUsersCanEdit boolean 6 | 2 groups array 7 | 3 users array 8 | -------------------------------------------------------------------------------- /tests/testthat/test-sheet_rename.R: -------------------------------------------------------------------------------- 1 | # ---- nm_fun ---- 2 | me_ <- nm_fun("TEST-sheet_rename") 3 | 4 | # ---- tests ---- 5 | test_that("internal copy works", { 6 | skip_if_offline() 7 | skip_if_no_token() 8 | 9 | ss <- local_ss( 10 | me_(), 11 | sheets = list(iris = head(iris), chickwts = head(chickwts)) 12 | ) 13 | ss %>% 14 | sheet_rename(2, new_name = "poultry") %>% 15 | sheet_rename(1, new_name = "flowers") 16 | out <- sheet_names(ss) 17 | expect_equal(out, c("flowers", "poultry")) 18 | }) 19 | -------------------------------------------------------------------------------- /data-raw/schemas/DataValidationRule: -------------------------------------------------------------------------------- 1 | # DataValidationRule 2 | # A tibble: 4 × 6 3 | property type instance_of array_of format enum 4 | 5 | 1 condition object BooleanCondition 6 | 2 inputMessage string 7 | 3 showCustomUi boolean 8 | 4 strict boolean 9 | -------------------------------------------------------------------------------- /data-raw/schemas/DuplicateSheetRequest: -------------------------------------------------------------------------------- 1 | # DuplicateSheetRequest 2 | # A tibble: 4 × 6 3 | property type instance_of array_of format enum 4 | 5 | 1 insertSheetIndex integer int32 6 | 2 newSheetId integer int32 7 | 3 newSheetName string 8 | 4 sourceSheetId integer int32 9 | -------------------------------------------------------------------------------- /googlesheets4.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: No 4 | SaveWorkspace: No 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: Yes 9 | NumSpacesForTab: 2 10 | Encoding: UTF-8 11 | 12 | RnwWeave: Sweave 13 | LaTeX: pdfLaTeX 14 | 15 | AutoAppendNewline: Yes 16 | StripTrailingWhitespace: Yes 17 | 18 | BuildType: Package 19 | PackageUseDevtools: Yes 20 | PackageInstallArgs: --no-multiarch --with-keep.source 21 | PackageCheckArgs: --no-examples 22 | PackageRoxygenize: rd,collate,namespace 23 | -------------------------------------------------------------------------------- /data-raw/schemas/AppendCellsRequest: -------------------------------------------------------------------------------- 1 | # AppendCellsRequest 2 | # A tibble: 4 × 6 3 | property type instance_of array_of format enum 4 | 5 | 1 fields string google-fieldmask 6 | 2 rows array RowData 7 | 3 sheetId integer int32 8 | 4 tableId string 9 | -------------------------------------------------------------------------------- /data-raw/schemas/UpdateCellsRequest: -------------------------------------------------------------------------------- 1 | # UpdateCellsRequest 2 | # A tibble: 4 × 6 3 | property type instance_of array_of format enum 4 | 5 | 1 fields string google-fieldmask 6 | 2 range object GridRange 7 | 3 rows array RowData 8 | 4 start object GridCoordinate 9 | -------------------------------------------------------------------------------- /man/googlesheets4-vctrs.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/googlesheets4-package.R, R/gs4_formula.R 3 | \name{googlesheets4-vctrs} 4 | \alias{googlesheets4-vctrs} 5 | \alias{vec_ptype2.googlesheets4_formula} 6 | \alias{vec_cast.googlesheets4_formula} 7 | \title{Internal vctrs methods} 8 | \usage{ 9 | \method{vec_ptype2}{googlesheets4_formula}(x, y, ...) 10 | 11 | \method{vec_cast}{googlesheets4_formula}(x, to, ...) 12 | } 13 | \description{ 14 | Internal vctrs methods 15 | } 16 | \keyword{internal} 17 | -------------------------------------------------------------------------------- /inst/extdata/fake-oauth-client-id-and-secret.json: -------------------------------------------------------------------------------- 1 | { 2 | "installed": { 3 | "client_id": "YOUR_CLIENT_ID_GOES_HERE", 4 | "project_id": "YOUR_PROJECT_ID_GOES_HERE", 5 | "auth_uri": "https://accounts.google.com/o/oauth2/auth", 6 | "token_uri": "https://oauth2.googleapis.com/token", 7 | "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", 8 | "client_secret": "YOUR_SECRET_GOES_HERE", 9 | "redirect_uris": [ 10 | "urn:ietf:wg:oauth:2.0:oob", 11 | "http://localhost" 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /man/gs4_random.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/gs4_create.R 3 | \name{gs4_random} 4 | \alias{gs4_random} 5 | \title{Generate a random Sheet name} 6 | \usage{ 7 | gs4_random(n = 1) 8 | } 9 | \arguments{ 10 | \item{n}{Number of names to generate.} 11 | } 12 | \value{ 13 | A character vector. 14 | } 15 | \description{ 16 | Generates a random name, suitable for a newly created Sheet, using 17 | \code{\link[ids:adjective_animal]{ids::adjective_animal()}}. 18 | } 19 | \examples{ 20 | gs4_random() 21 | } 22 | -------------------------------------------------------------------------------- /tools/test-fixtures/googlesheets4-col-types/na-is-honored.R: -------------------------------------------------------------------------------- 1 | # https://github.com/tidyverse/googlesheets4/issues/73 2 | devtools::load_all() # I assume we're in googlesheets4 source 3 | library(googledrive) 4 | library(tidyverse) 5 | 6 | googlesheets4:::gs4_auth_testing() 7 | 8 | ss <- test_sheet_create("googlesheets4-col-types") 9 | gs4_browse(ss) 10 | 11 | df <- tibble( 12 | A = list(1, "Missing", 3), 13 | B = list(1, "NA", 3), 14 | C = c(1, NA, 3), 15 | D = 1:3 16 | ) 17 | sheet_add(ss, sheet = "NAs") 18 | sheet_write(df, ss, sheet = "NAs") 19 | -------------------------------------------------------------------------------- /tools/test-fixtures/googlesheets4-cell-tests/formulas.R: -------------------------------------------------------------------------------- 1 | library(tidyverse) 2 | devtools::load_all() # I assume we're in googlesheets4 source 3 | library(googledrive) 4 | 5 | gs4_auth_testing() 6 | 7 | ss <- test_sheet_create() 8 | gs4_browse(ss) 9 | 10 | # TODO: I created this worksheet in the browser, by copying from 11 | # gs4_example("formulas-and-formats") 12 | # add code here once possible 13 | # most challenging cell is B4, the one that contains 14 | # =IMAGE("https://www.google.com/images/srpr/logo3w.png") 15 | # i.e. a formula that evaluates to an image 16 | -------------------------------------------------------------------------------- /R/zzz.R: -------------------------------------------------------------------------------- 1 | .onLoad <- function(libname, pkgname) { 2 | # .auth is created in R/gs4_auth.R 3 | # this is to insure we get an instance of gargle's AuthState using the 4 | # current, locally installed version of gargle 5 | utils::assignInMyNamespace( 6 | ".auth", 7 | gargle::init_AuthState(package = "googlesheets4", auth_active = TRUE) 8 | ) 9 | 10 | if (identical(Sys.getenv("IN_PKGDOWN"), "true")) { 11 | tryCatch( 12 | gs4_auth_docs(), 13 | googlesheets4_auth_internal_error = function(e) NULL 14 | ) 15 | } 16 | 17 | invisible() 18 | } 19 | -------------------------------------------------------------------------------- /data-raw/schemas/GridRange: -------------------------------------------------------------------------------- 1 | # GridRange 2 | # A tibble: 5 × 6 3 | property type instance_of array_of format enum 4 | 5 | 1 endColumnIndex integer int32 6 | 2 endRowIndex integer int32 7 | 3 sheetId integer int32 8 | 4 startColumnIndex integer int32 9 | 5 startRowIndex integer int32 10 | -------------------------------------------------------------------------------- /tests/testthat/test-sheet_append.R: -------------------------------------------------------------------------------- 1 | # ---- nm_fun ---- 2 | me_ <- nm_fun("TEST-sheet_append") 3 | 4 | # ---- tests ---- 5 | test_that("sheet_append() works", { 6 | skip_if_offline() 7 | skip_if_no_token() 8 | 9 | dat <- tibble::tibble(x = as.numeric(1:10), y = LETTERS[1:10]) 10 | ss <- local_ss(me_(), sheets = list(test = dat[0, ])) 11 | 12 | sheet_append(ss, dat[1, ], sheet = "test") 13 | out <- range_read(ss, sheet = "test") 14 | expect_equal(out, dat[1, ]) 15 | 16 | sheet_append(ss, dat[2:10, ], sheet = "test") 17 | out <- range_read(ss, sheet = "test") 18 | expect_equal(out, dat) 19 | }) 20 | -------------------------------------------------------------------------------- /man/gs4_has_token.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/gs4_auth.R 3 | \name{gs4_has_token} 4 | \alias{gs4_has_token} 5 | \title{Is there a token on hand?} 6 | \usage{ 7 | gs4_has_token() 8 | } 9 | \value{ 10 | Logical. 11 | } 12 | \description{ 13 | Reports whether googlesheets4 has stored a token, ready for use in downstream 14 | requests. 15 | } 16 | \examples{ 17 | gs4_has_token() 18 | } 19 | \seealso{ 20 | Other low-level API functions: 21 | \code{\link{gs4_token}()}, 22 | \code{\link{request_generate}()}, 23 | \code{\link{request_make}()} 24 | } 25 | \concept{low-level API functions} 26 | -------------------------------------------------------------------------------- /tests/testthat/test-sheet_relocate.R: -------------------------------------------------------------------------------- 1 | # ---- nm_fun ---- 2 | me_ <- nm_fun("TEST-sheet_relocate") 3 | 4 | # ---- tests ---- 5 | test_that("relocation works", { 6 | skip_if_offline() 7 | skip_if_no_token() 8 | 9 | sheet_names <- c("alfa", "bravo", "charlie", "delta", "echo", "foxtrot") 10 | ss <- local_ss(me_(), sheets = sheet_names) 11 | 12 | sheet_relocate(ss, "echo", .before = "bravo") 13 | sheet_relocate(ss, list("foxtrot", 4)) 14 | sheet_relocate(ss, c("bravo", "alfa", "echo"), .after = 10) 15 | expect_equal( 16 | sheet_names(ss), 17 | c("foxtrot", "charlie", "delta", "bravo", "alfa", "echo") 18 | ) 19 | }) 20 | -------------------------------------------------------------------------------- /R/schema_SheetProperties.R: -------------------------------------------------------------------------------- 1 | #' @export 2 | as_tibble.googlesheets4_schema_SheetProperties <- function(x, ...) { 3 | # fmt: skip 4 | tibble::tibble( 5 | # TODO: open question whether I should explicitly unescape title here 6 | name = glean_chr(x, "title"), 7 | index = glean_int(x, "index"), 8 | id = glean_int(x, "sheetId"), 9 | type = glean_chr(x, "sheetType"), 10 | visible = !glean_lgl(x, "hidden", .default = FALSE), 11 | grid_rows = glean_int(x, c("gridProperties", "rowCount")), 12 | grid_columns = glean_int(x, c("gridProperties", "columnCount")) 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /R/gs4_browse.R: -------------------------------------------------------------------------------- 1 | #' Visit a Sheet in a web browser 2 | #' 3 | #' Visits a Google Sheet in your default browser, if session is interactive. 4 | #' 5 | #' @inheritParams read_sheet 6 | #' 7 | #' @return The Sheet's browser URL, invisibly. 8 | #' @export 9 | #' @examples 10 | #' gs4_example("mini-gap") %>% gs4_browse() 11 | gs4_browse <- function(ss) { 12 | ## TO RECONSIDER AFTER AUTH: get the official link, if we're in auth state? 13 | # googledrive::drive_browse(as_sheets_id(ss)) 14 | ssid <- as_sheets_id(ss) 15 | url <- glue("https://docs.google.com/spreadsheets/d/{ssid}") 16 | if (is_interactive()) { 17 | utils::browseURL(url) 18 | } 19 | invisible(url) 20 | } 21 | -------------------------------------------------------------------------------- /R/schema_ProtectedRange.R: -------------------------------------------------------------------------------- 1 | #' @export 2 | as_tibble.googlesheets4_schema_ProtectedRange <- function(x, ...) { 3 | grid_range <- new("GridRange", !!!pluck(x, "range")) 4 | grid_range <- as_tibble(grid_range) 5 | 6 | tibble::tibble( 7 | protected_range_id = glean_int(x, "protectedRangeId"), 8 | description = glean_chr(x, "description"), 9 | requesting_user_can_edit = glean_lgl(x, "requestingUserCanEdit"), 10 | warning_only = glean_lgl(x, "warningOnly"), 11 | has_unprotected_ranges = rlang::has_name(x, "unprotectedRanges"), 12 | editors = x$editors %||% list(), 13 | named_range_id = glean_chr(x, "namedRangeId"), 14 | !!!grid_range 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /tools/test-fixtures/googlesheets4-cell-tests/empties-and-formats.R: -------------------------------------------------------------------------------- 1 | library(tidyverse) 2 | devtools::load_all() # I assume we're in googlesheets4 source 3 | library(googledrive) 4 | 5 | gs4_auth_testing() 6 | 7 | ss <- test_sheet_create() 8 | gs4_browse(ss) 9 | 10 | # TODO: I created this worksheet in the browser; add code here once possible 11 | 12 | # I riffed on the original Sheet provided by @nadnudus in 13 | # https://github.com/tidyverse/googlesheets4/issues/4 14 | # ssid <- as_sheets_id("1UbdlyITXLvsxQt6kpszu5gfiDmF5Q-wOrNC7l4E9jOg") 15 | # gs4_browse(ss) 16 | # I copied the "legend" worksheet. 17 | # Added a note to C1. 18 | # Added a comment (as rstudio jenny) to C2. 19 | -------------------------------------------------------------------------------- /data-raw/see-all-examples.R: -------------------------------------------------------------------------------- 1 | library(devtools) 2 | library(fs) 3 | library(here) 4 | library(purrr) 5 | 6 | # setup and/or clean 7 | here("examples") %>% 8 | dir_create() %>% 9 | dir_ls() %>% 10 | file_delete() 11 | 12 | rd_files <- here("man") %>% dir_ls(regex = "[.][Rr]d$") 13 | 14 | do_one <- function(x) { 15 | print(path_file(x)) 16 | tmp <- file_temp() 17 | tools::Rd2ex(x, out = tmp) 18 | if (file_exists(tmp)) { 19 | cat( 20 | readLines(tmp), 21 | file = here("examples", "googlesheets4-examples.R"), 22 | append = TRUE, 23 | sep = "\n" 24 | ) 25 | } else { 26 | print(" ^ no examples!!") 27 | } 28 | } 29 | walk(rd_files, do_one) 30 | -------------------------------------------------------------------------------- /inst/extdata/example_and_test_sheets.csv: -------------------------------------------------------------------------------- 1 | name,purpose,id 2 | cell-contents-and-formats,example,1peJXEeAp5Qt3ENoTvkhvenQ36N3kLyq6sq9Dh2ufQ6E 3 | chicken-sheet,example,1ct9t1Efv8pAGN9YO5gC2QfRq2wT4XjNoTMXpVeUghJU 4 | deaths,example,1VTJjWoP1nshbyxmL9JqXgdVsimaYty21LGxxs018H2Y 5 | formulas-and-formats,example,1wPLrWOxxEjp3T1nv2YBxn63FX70Mz5W5Tm4tGc-lRms 6 | gapminder,example,1U6Cf_qEOhiR9AZqTqS3mbMF3zt2db48ZP5v3rkrAEJY 7 | Getting started,example,0B0ft3MvMsr66c3RhcnRlcl9maWxl 8 | mini-gap,example,1k94ZVVl6sdj0AXfK9MQOuQ4rOhd1PULqpAu2_kr9MAU 9 | googlesheets4-cell-tests,test,1WRFIb11PJsNwx2tYBRn3uq8uHwWSI5ziSgbGjkOukmE 10 | googlesheets4-col-types,test,1q-iRi1L3JugqHTtcjQ3DQOmOTuDnUsWi2AiG2eNyQkU 11 | -------------------------------------------------------------------------------- /R/schema_NamedRange.R: -------------------------------------------------------------------------------- 1 | #' @export 2 | as_tibble.googlesheets4_schema_NamedRange <- function(x, ...) { 3 | grid_range <- new("GridRange", !!!pluck(x, "range")) 4 | grid_range <- as_tibble(grid_range) 5 | 6 | tibble::tibble( 7 | name = glean_chr(x, "name"), 8 | id = glean_chr(x, "namedRangeId"), 9 | !!!grid_range 10 | ) 11 | } 12 | 13 | as_NamedRange <- function(x, ...) { 14 | UseMethod("as_NamedRange") 15 | } 16 | 17 | #' @export 18 | as_NamedRange.default <- function(x, ...) { 19 | abort_unsupported_conversion(x, to = "NamedRange") 20 | } 21 | 22 | #' @export 23 | as_NamedRange.range_spec <- function(x, ..., name) { 24 | new("NamedRange", name = name, range = as_GridRange(x)) 25 | } 26 | -------------------------------------------------------------------------------- /tests/testthat/test-make_column.R: -------------------------------------------------------------------------------- 1 | test_that("resolve_col_type() passes `ctype` other than 'COL_GUESS' through", { 2 | expect_identical(resolve_col_type("a cell", "COL_ANYTHING"), "COL_ANYTHING") 3 | }) 4 | 5 | test_that("resolve_col_type() implements coercion DAG for 'COL_GUESS'", { 6 | input <- c("l", "D") 7 | expect_identical(resolve_col_type(input, "COL_GUESS"), "COL_LIST") 8 | }) 9 | 10 | test_that("blank cell doesn't trick resolve_col_type() into guessing COL_LIST", { 11 | input <- list( 12 | structure(1, class = c("CELL_BLANK", "SHEETS_CELL")), 13 | structure(1, class = c("CELL_TEXT", "SHEETS_CELL")) 14 | ) 15 | expect_identical(resolve_col_type(input, "COL_GUESS"), "CELL_TEXT") 16 | }) 17 | -------------------------------------------------------------------------------- /man/gs4_user.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/gs4_auth.R 3 | \name{gs4_user} 4 | \alias{gs4_user} 5 | \title{Get info on current user} 6 | \usage{ 7 | gs4_user() 8 | } 9 | \value{ 10 | An email address or, if no token has been loaded, \code{NULL}. 11 | } 12 | \description{ 13 | Reveals the email address of the user associated with the current token. 14 | If no token has been loaded yet, this function does not initiate auth. 15 | } 16 | \examples{ 17 | gs4_user() 18 | } 19 | \seealso{ 20 | \code{\link[gargle:token-info]{gargle::token_userinfo()}}, \code{\link[gargle:token-info]{gargle::token_email()}}, 21 | \code{\link[gargle:token-info]{gargle::token_tokeninfo()}} 22 | } 23 | -------------------------------------------------------------------------------- /man/gs4_oauth_app.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/gs4_auth.R 3 | \name{gs4_oauth_app} 4 | \alias{gs4_oauth_app} 5 | \title{Get currently configured OAuth app (deprecated)} 6 | \usage{ 7 | gs4_oauth_app() 8 | } 9 | \description{ 10 | \ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} 11 | 12 | In light of the new \code{\link[gargle:gargle_oauth_client_from_json]{gargle::gargle_oauth_client()}} constructor and class of 13 | the same name, \code{gs4_oauth_app()} is being replaced by 14 | \code{\link[=gs4_oauth_client]{gs4_oauth_client()}}. 15 | } 16 | \keyword{internal} 17 | -------------------------------------------------------------------------------- /R/sheet_properties.R: -------------------------------------------------------------------------------- 1 | #' Get data about (work)sheets 2 | #' 3 | #' Reveals full metadata or just the names for the (work)sheets inside a 4 | #' (spread)Sheet. 5 | #' 6 | #' @eval param_ss() 7 | #' 8 | #' @return 9 | #' * `sheet_properties()`: A tibble with one row per (work)sheet. 10 | #' * `sheet_names()`: A character vector of (work)sheet names. 11 | #' @export 12 | #' @family worksheet functions 13 | #' @examplesIf gs4_has_token() 14 | #' ss <- gs4_example("gapminder") 15 | #' sheet_properties(ss) 16 | #' sheet_names(ss) 17 | sheet_properties <- function(ss) { 18 | x <- gs4_get(ss) 19 | pluck(x, "sheets") 20 | } 21 | 22 | #' @export 23 | #' @rdname sheet_properties 24 | sheet_names <- function(ss) { 25 | sheet_properties(ss)$name 26 | } 27 | -------------------------------------------------------------------------------- /tests/testthat/test-sheet_delete.R: -------------------------------------------------------------------------------- 1 | # ---- nm_fun ---- 2 | me_ <- nm_fun("TEST-sheet_delete") 3 | 4 | # ---- tests ---- 5 | test_that("sheet_delete() rejects invalid `sheet`", { 6 | expect_error( 7 | sheet_delete(as_sheets_id("123"), sheet = TRUE), 8 | "must be either" 9 | ) 10 | }) 11 | 12 | test_that("sheet_delete() works", { 13 | skip_if_offline() 14 | skip_if_no_token() 15 | 16 | ss <- local_ss(me_()) 17 | 18 | sheet_add(ss, c("alpha", "beta", "gamma", "delta")) 19 | 20 | expect_no_error( 21 | sheet_delete(ss, 1) 22 | ) 23 | expect_no_error( 24 | sheet_delete(ss, "gamma") 25 | ) 26 | expect_no_error( 27 | sheet_delete(ss, list("alpha", 2)) 28 | ) 29 | 30 | sheets_df <- sheet_properties(ss) 31 | 32 | expect_identical(sheets_df$name, "delta") 33 | }) 34 | -------------------------------------------------------------------------------- /data-raw/schemas/GridProperties: -------------------------------------------------------------------------------- 1 | # GridProperties 2 | # A tibble: 7 × 6 3 | property type instance_of array_of format enum 4 | 5 | 1 columnCount integer int32 6 | 2 columnGroupControlAfter boolean 7 | 3 frozenColumnCount integer int32 8 | 4 frozenRowCount integer int32 9 | 5 hideGridlines boolean 10 | 6 rowCount integer int32 11 | 7 rowGroupControlAfter boolean 12 | -------------------------------------------------------------------------------- /tests/testthat/test-utils-ui.R: -------------------------------------------------------------------------------- 1 | test_that("gs4_quiet() falls back to NA if googlesheets4_quiet is unset", { 2 | withr::with_options( 3 | list(googlesheets4_quiet = NULL), 4 | expect_true(is.na(gs4_quiet())) 5 | ) 6 | }) 7 | 8 | test_that("gs4_abort() throws classed condition", { 9 | expect_error(gs4_abort("oops"), class = "googlesheets4_error") 10 | expect_gs4_error(gs4_abort("oops")) 11 | expect_gs4_error(gs4_abort("oops", class = "googlesheets4_foo")) 12 | expect_error( 13 | gs4_abort("oops", class = "googlesheets4_foo"), 14 | class = "googlesheets4_foo" 15 | ) 16 | }) 17 | 18 | test_that("abort_unsupported_conversion() works", { 19 | x <- structure(1, class = c("a", "b", "c")) 20 | expect_snapshot_error( 21 | abort_unsupported_conversion(x, "target_class") 22 | ) 23 | }) 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue_template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report or feature request 3 | about: Describe a bug you've seen or make a case for a new feature 4 | --- 5 | 6 | Please briefly describe your problem and what output you expect. If you have a question, please don't use this form. Instead, ask on or . 7 | 8 | Please include a minimal reproducible example (AKA a reprex). If you've never heard of a [reprex](http://reprex.tidyverse.org/) before, start by reading . 9 | Special tricks for this package are in [How to create a googlesheets4 reprex](https://googlesheets4.tidyverse.org/articles/articles/googlesheets4-reprex.html). 10 | 11 | Brief description of the problem 12 | 13 | ```r 14 | # insert reprex here 15 | ``` 16 | -------------------------------------------------------------------------------- /tools/test-fixtures/googlesheets4-col-types/lots-of-column-types.R: -------------------------------------------------------------------------------- 1 | devtools::load_all() # I assume we're in googlesheets4 source 2 | library(googledrive) 3 | library(tidyverse) 4 | 5 | # googlesheets4:::gs4_auth_testing() 6 | 7 | ss <- test_sheet_create("googlesheets4-col-types") 8 | gs4_browse(ss) 9 | 10 | df <- tibble( 11 | logical = c(TRUE, FALSE, NA, TRUE), 12 | character = c("apple", "banana", "cherry", "durian"), 13 | factor = factor(c("one", "two", "three", "four")), 14 | integer = 1:4, 15 | double = 4:1 - 2.5, 16 | date = as.Date(c("2003-06-06", "1982-12-05", "2014-02-14", "1999-08-27")), 17 | datetime = as.POSIXct(c( 18 | "1978-05-31 04:24:32", 19 | "2006-07-19 23:27:37", 20 | "2003-12-21 09:20:29", 21 | "1975-04-14 13:31:03" 22 | )) 23 | ) 24 | 25 | sheet_write(df, ss, sheet = "lots-of-types") 26 | -------------------------------------------------------------------------------- /man/roxygen/templates/range.R: -------------------------------------------------------------------------------- 1 | #' @param range A cell range to read from. If `NULL`, all non-empty cells are 2 | #' read. Otherwise specify `range` as described in [Sheets A1 3 | #' notation](https://developers.google.com/sheets/api/guides/concepts#a1_notation) 4 | #' or using the helpers documented in [cell-specification]. Sheets uses 5 | #' fairly standard spreadsheet range notation, although a bit different from 6 | #' Excel. Examples of valid ranges: `"Sheet1!A1:B2"`, `"Sheet1!A:A"`, 7 | #' `"Sheet1!1:2"`, `"Sheet1!A5:A"`, `"A1:B2"`, `"Sheet1"`. Interpreted 8 | #' strictly, even if the range forces the inclusion of leading, trailing, or 9 | #' embedded empty rows or columns. Takes precedence over `skip`, `n_max` and 10 | #' `sheet`. Note `range` can be a named range, like `"sales_data"`, without 11 | #' any cell reference. 12 | -------------------------------------------------------------------------------- /data-raw/deaths-example-sheet.R: -------------------------------------------------------------------------------- 1 | library(tidyverse) 2 | library(googledrive) 3 | library(googlesheets4) 4 | library(readxl) 5 | 6 | googlesheets4:::gs4_auth_docs() 7 | gs4_user() 8 | 9 | x <- gs4_find() 10 | 11 | if ("deaths" %in% x$name) { 12 | stop("Yo, 'deaths' already exists. Are you sure you want to do this?") 13 | } 14 | 15 | # to work on an existing 'deaths' Sheet 16 | # ss <- gs4_find("deaths") %>% as_sheets_id() 17 | 18 | # to create one from scratch 19 | deaths_xlsx <- readxl_example("deaths.xlsx") 20 | ss <- drive_upload(deaths_xlsx, name = "deaths", type = "spreadsheet") 21 | 22 | googlesheets4:::gs4_share(ss) 23 | 24 | gs4_browse(ss) 25 | 26 | googlesheets4:::range_add_named(ss, name = "arts_data", range = "arts!A5:F15") 27 | googlesheets4:::range_add_named(ss, name = "other_data", range = "other!A5:F15") 28 | 29 | ss 30 | 31 | unclass(ss) 32 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/schemas.md: -------------------------------------------------------------------------------- 1 | # new() rejects data not expected for schema 2 | 3 | Code 4 | new("Spreadsheet", foofy = "blah") 5 | Condition 6 | Error in `check_against_schema()`: 7 | ! Properties not recognized for the 'Spreadsheet' schema: 8 | x 'foofy' 9 | 10 | --- 11 | 12 | Code 13 | new("Spreadsheet", foofy = "blah", foo = "bar") 14 | Condition 15 | Error in `check_against_schema()`: 16 | ! Properties not recognized for the 'Spreadsheet' schema: 17 | x 'foofy' 18 | x 'foo' 19 | 20 | # check_against_schema() errors when no schema can be found 21 | 22 | Code 23 | check_against_schema(x) 24 | Condition 25 | Error in `check_against_schema()`: 26 | ! Trying to check an object of class , but can't get a schema. 27 | 28 | -------------------------------------------------------------------------------- /data-raw/errors/404-nonexistent-sheet.md: -------------------------------------------------------------------------------- 1 | 404-nonexistent-sheet.R 2 | ================ 3 | jenny 4 | Fri Mar 30 16:05:19 2018 5 | 6 | ``` r 7 | devtools::load_all(".") 8 | ``` 9 | 10 | ## Loading googlesheets4 11 | 12 | ``` r 13 | req <- request_generate( 14 | "spreadsheets.get", 15 | ## ID of 'googlesheets4-design-exploration', but replaced last 2 chars w/ '-' 16 | list(spreadsheetId = "1xTUxWGcFLtDIHoYJ1WsjQuLmpUtBf--8Bcu5lQ302--"), 17 | token = NULL 18 | ) 19 | raw_resp <- request_make(req) 20 | response_process(raw_resp) 21 | ``` 22 | 23 | ## Error: HTTP error 404 24 | ## * message: 'Requested entity was not found.' 25 | ## * status: 'NOT_FOUND' 26 | 27 | ``` r 28 | ct <- httr::content(raw_resp) 29 | str(ct) 30 | ``` 31 | 32 | ## List of 1 33 | ## $ error:List of 3 34 | ## ..$ code : int 404 35 | ## ..$ message: chr "Requested entity was not found." 36 | ## ..$ status : chr "NOT_FOUND" 37 | -------------------------------------------------------------------------------- /data-raw/schemas/ProtectedRange: -------------------------------------------------------------------------------- 1 | # ProtectedRange 2 | # A tibble: 9 × 6 3 | property type instance_of array_of format enum 4 | 5 | 1 description string 6 | 2 editors object Editors 7 | 3 namedRangeId string 8 | 4 protectedRangeId integer int32 9 | 5 range object GridRange 10 | 6 requestingUserCanEdit boolean 11 | 7 tableId string 12 | 8 unprotectedRanges array GridRange 13 | 9 warningOnly boolean 14 | -------------------------------------------------------------------------------- /data-raw/googlesheets4-col-types-NAs.R: -------------------------------------------------------------------------------- 1 | # making some changes to 2 | # test Sheet: googlesheets4-col-types 3 | # sheet: NAs 4 | 5 | # I want the ID to remain the same, so will modify existing Sheet 6 | library(googlesheets4) 7 | library(googledrive) 8 | library(tidyverse) 9 | 10 | gs4_auth( 11 | path = "~/.R/gargle/googlesheets4-sheet-keeper.json", 12 | scopes = "https://www.googleapis.com/auth/drive" 13 | ) 14 | gs4_user() 15 | 16 | examples <- googlesheets4:::.test_sheets 17 | ssid <- examples["googlesheets4-col-types"] 18 | (ss <- gs4_get(ssid)) 19 | 20 | dat <- tribble( 21 | ~...Missing, ~...NA, ~space, ~empty_string, ~truly_empty, ~complete, 22 | "one", "one", "one", "one", "one", "one", 23 | "Missing", "NA", " ", "", NA, "two", 24 | "three", "three", "three", "three", "three", "three" 25 | ) 26 | 27 | write_sheet(dat, ssid, sheet = "NAs") 28 | 29 | range_read(ssid) 30 | -------------------------------------------------------------------------------- /man/figures/lifecycle-retired.svg: -------------------------------------------------------------------------------- 1 | lifecyclelifecycleretiredretired -------------------------------------------------------------------------------- /tests/testthat/test-sheet_copy.R: -------------------------------------------------------------------------------- 1 | # ---- nm_fun ---- 2 | me_ <- nm_fun("TEST-sheet_copy") 3 | 4 | # ---- tests ---- 5 | test_that("internal copy works", { 6 | skip_if_offline() 7 | skip_if_no_token() 8 | 9 | ss <- local_ss( 10 | me_("internal"), 11 | sheets = list(iris = head(iris), chickwts = head(chickwts)) 12 | ) 13 | sheet_copy(ss, to_sheet = "xyz", .after = 1) 14 | out <- sheet_names(ss) 15 | expect_equal(out, c("iris", "xyz", "chickwts")) 16 | }) 17 | 18 | test_that("external copy works", { 19 | skip_if_offline() 20 | skip_if_no_token() 21 | 22 | ss_source <- local_ss( 23 | me_("source"), 24 | sheets = list(iris = head(iris), chickwts = head(chickwts)) 25 | ) 26 | ss_dest <- local_ss(me_("dest")) 27 | 28 | sheet_copy( 29 | ss_source, 30 | from_sheet = "chickwts", 31 | to_ss = ss_dest, 32 | to_sheet = "chicks-two", 33 | .before = 1 34 | ) 35 | out <- sheet_names(ss_dest) 36 | expect_equal(out, c("chicks-two", "Sheet1")) 37 | }) 38 | -------------------------------------------------------------------------------- /man/gs4_browse.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/gs4_browse.R 3 | \name{gs4_browse} 4 | \alias{gs4_browse} 5 | \title{Visit a Sheet in a web browser} 6 | \usage{ 7 | gs4_browse(ss) 8 | } 9 | \arguments{ 10 | \item{ss}{Something that identifies a Google Sheet: 11 | \itemize{ 12 | \item its file id as a string or \code{\link[googledrive:drive_id]{drive_id}} 13 | \item a URL from which we can recover the id 14 | \item a one-row \code{\link[googledrive:dribble]{dribble}}, which is how googledrive 15 | represents Drive files 16 | \item an instance of \code{googlesheets4_spreadsheet}, which is what \code{\link[=gs4_get]{gs4_get()}} 17 | returns 18 | } 19 | 20 | Processed through \code{\link[=as_sheets_id]{as_sheets_id()}}.} 21 | } 22 | \value{ 23 | The Sheet's browser URL, invisibly. 24 | } 25 | \description{ 26 | Visits a Google Sheet in your default browser, if session is interactive. 27 | } 28 | \examples{ 29 | gs4_example("mini-gap") \%>\% gs4_browse() 30 | } 31 | -------------------------------------------------------------------------------- /tests/testthat/test-utils-sheet.R: -------------------------------------------------------------------------------- 1 | test_that("enlist_sheets() works", { 2 | df1 <- data.frame(x = 1L) 3 | df2 <- data.frame(x = 2L) 4 | df_list <- list(df1 = df1, df2 = df2) 5 | f <- function(sheets = NULL) enlist_sheets(enquo(sheets)) 6 | 7 | expect_null(f()) 8 | expect_identical( 9 | f(c("string_1", "string_2")), 10 | list(name = c("string_1", "string_2"), value = list(NULL, NULL)) 11 | ) 12 | expect_identical( 13 | f(df1), 14 | list(name = "df1", value = list(data.frame(x = 1L))) 15 | ) 16 | expect_identical( 17 | f(list(df1, df2)), 18 | list(name = list(NULL, NULL), value = list(df1, df2)) 19 | ) 20 | expect_identical( 21 | f(list(df1 = df1, df2 = df2)), 22 | list(name = c("df1", "df2"), value = list(df1, df2)) 23 | ) 24 | expect_identical( 25 | f(df_list), 26 | f(list(df1 = df1, df2 = df2)) 27 | ) 28 | expect_identical( 29 | f(data.frame(x = 1L)), 30 | list(name = list(NULL), value = list(data.frame(x = 1L))) 31 | ) 32 | }) 33 | -------------------------------------------------------------------------------- /.github/workflows/with-auth.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [main, master] 4 | pull_request: 5 | branches: [main, master] 6 | 7 | name: with-auth 8 | permissions: read-all 9 | 10 | jobs: 11 | with-auth: 12 | runs-on: ubuntu-latest 13 | 14 | env: 15 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 16 | R_KEEP_PKG_SOURCE: yes 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | 21 | - uses: r-lib/actions/setup-pandoc@v2 22 | 23 | - uses: r-lib/actions/setup-r@v2 24 | with: 25 | r-version: release 26 | http-user-agent: ${{ matrix.config.http-user-agent }} 27 | use-public-rspm: true 28 | 29 | - uses: r-lib/actions/setup-r-dependencies@v2 30 | with: 31 | extra-packages: any::rcmdcheck 32 | needs: check 33 | 34 | - uses: r-lib/actions/check-r-package@v2 35 | with: 36 | upload-snapshots: true 37 | env: 38 | GOOGLESHEETS4_KEY: ${{ secrets.GOOGLESHEETS4_KEY }} 39 | -------------------------------------------------------------------------------- /data-raw/schemas/TextFormat: -------------------------------------------------------------------------------- 1 | # TextFormat 2 | # A tibble: 9 × 7 3 | property type instance_of array_of format deprecated enum 4 | 5 | 1 bold boolean NA 6 | 2 fontFamily string NA 7 | 3 fontSize integer int32 NA 8 | 4 foregroundColor object Color TRUE 9 | 5 foregroundColorStyle object ColorStyle NA 10 | 6 italic boolean NA 11 | 7 link object Link NA 12 | 8 strikethrough boolean NA 13 | 9 underline boolean NA 14 | -------------------------------------------------------------------------------- /man/gs4_fodder.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/gs4_fodder.R 3 | \name{gs4_fodder} 4 | \alias{gs4_fodder} 5 | \title{Create useful spreadsheet filler} 6 | \usage{ 7 | gs4_fodder(n = 10, m = n) 8 | } 9 | \arguments{ 10 | \item{n}{Number of rows.} 11 | 12 | \item{m}{Number of columns.} 13 | } 14 | \value{ 15 | A data frame of character vectors. 16 | } 17 | \description{ 18 | Creates a data frame that is useful for filling a spreadsheet, when you just 19 | need a sheet to experiment with. The data frame has \code{n} rows and \code{m} columns 20 | with these properties: 21 | \itemize{ 22 | \item Column names match what Sheets displays: "A", "B", "C", and so on. 23 | \item Inner cell values reflect the coordinates where each value will land in 24 | the sheet, in A1-notation. So the first row is "B2", "C2", and so on. 25 | Note that this \code{n}-row data frame will occupy \code{n + 1} rows in the sheet, 26 | because the column names occupy the first row. 27 | } 28 | } 29 | \examples{ 30 | gs4_fodder() 31 | gs4_fodder(5, 3) 32 | } 33 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/range_read.md: -------------------------------------------------------------------------------- 1 | # col_names must be logical or character and have length 2 | 3 | Code 4 | wrapper_fun(1:3) 5 | Condition 6 | Error in `wrapper_fun()`: 7 | ! `col_names` must be : 8 | x `col_names` has class . 9 | 10 | --- 11 | 12 | Code 13 | wrapper_fun(factor("a")) 14 | Condition 15 | Error in `wrapper_fun()`: 16 | ! `col_names` must be : 17 | x `col_names` has class . 18 | 19 | --- 20 | 21 | Code 22 | wrapper_fun(character()) 23 | Condition 24 | Error in `wrapper_fun()`: 25 | ! `col_names` must have length greater than zero. 26 | 27 | # logical col_names must be TRUE or FALSE 28 | 29 | Code 30 | wrapper_fun(NA) 31 | Condition 32 | Error in `wrapper_fun()`: 33 | ! `col_names` must be either `TRUE` or `FALSE`. 34 | 35 | --- 36 | 37 | Code 38 | wrapper_fun(c(TRUE, FALSE)) 39 | Condition 40 | Error in `wrapper_fun()`: 41 | ! `col_names` must be either `TRUE` or `FALSE`. 42 | 43 | -------------------------------------------------------------------------------- /tests/testthat/test-rectangle.R: -------------------------------------------------------------------------------- 1 | test_that("glean_lgl() works", { 2 | expect_identical(glean_lgl(list(a = TRUE), "a"), TRUE) 3 | expect_identical(glean_lgl(list(b = TRUE), "a"), NA) 4 | expect_identical(glean_lgl(list(), "a"), NA) 5 | expect_identical(glean_lgl(list(b = TRUE), "a", .default = FALSE), FALSE) 6 | expect_error(glean_lgl(list(a = "a"), "a"), "Can't coerce") 7 | }) 8 | 9 | test_that("glean_chr() works", { 10 | expect_identical(glean_chr(list(a = "hi"), "a"), "hi") 11 | expect_identical(glean_chr(list(b = "bye"), "a"), NA_character_) 12 | expect_identical(glean_chr(list(), "a"), NA_character_) 13 | expect_identical(glean_chr(list(b = "bye"), "a", .default = "huh"), "huh") 14 | }) 15 | 16 | test_that("glean_int() works", { 17 | expect_identical(glean_int(list(a = 1L), "a"), 1L) 18 | expect_identical(glean_int(list(b = 1L), "a"), NA_integer_) 19 | expect_identical(glean_int(list(), "a"), NA_integer_) 20 | expect_identical(glean_int(list(b = 1L), "a", .default = 2L), 2L) 21 | expect_error(glean_int(list(a = "a"), "a"), "Can't coerce") 22 | }) 23 | -------------------------------------------------------------------------------- /inst/WORDLIST: -------------------------------------------------------------------------------- 1 | API's 2 | Auth 3 | AuthState 4 | CLI 5 | CMD 6 | Cheatsheet 7 | Codecov 8 | Colaboratory 9 | Computerphile's 10 | Datetime 11 | Datetimes 12 | Feuille 13 | Gapminder 14 | IDEs 15 | JSON 16 | OAuth 17 | OOB 18 | ORCID 19 | PBC 20 | POSIXct 21 | RStudio 22 | SheetN 23 | Shortcodes 24 | Timezones 25 | UI 26 | UpperCamelCase 27 | auth 28 | autogenerates 29 | backoff 30 | behaviour 31 | bigrquery 32 | cci 33 | cellranger 34 | cheatsheet 35 | chickwts 36 | cli 37 | cli's 38 | csv 39 | datetime 40 | datetimes 41 | de 42 | dev 43 | funder 44 | gamechanger 45 | gapminder 46 | gmailr 47 | googleapis 48 | googledrive 49 | googlesheets 50 | https 51 | httr 52 | httr's 53 | js 54 | lubridate 55 | lubridate's 56 | noninteractively 57 | ny 58 | pkgdown 59 | pre 60 | programmatically 61 | readonly 62 | readr 63 | readr's 64 | readxl 65 | reprex 66 | reprexes 67 | rlang's 68 | roundtrip 69 | shortcode 70 | shortcodes 71 | targetted 72 | testthat 73 | tibble 74 | tibble's 75 | tidyverse 76 | tsv 77 | unboundedness 78 | unformatted 79 | unskipped 80 | utc 81 | vctrs 82 | vectorized 83 | withr 84 | www 85 | xls 86 | xlsx 87 | -------------------------------------------------------------------------------- /R/gs4_fodder.R: -------------------------------------------------------------------------------- 1 | #' Create useful spreadsheet filler 2 | #' 3 | #' Creates a data frame that is useful for filling a spreadsheet, when you just 4 | #' need a sheet to experiment with. The data frame has `n` rows and `m` columns 5 | #' with these properties: 6 | #' * Column names match what Sheets displays: "A", "B", "C", and so on. 7 | #' * Inner cell values reflect the coordinates where each value will land in 8 | #' the sheet, in A1-notation. So the first row is "B2", "C2", and so on. 9 | #' Note that this `n`-row data frame will occupy `n + 1` rows in the sheet, 10 | #' because the column names occupy the first row. 11 | #' 12 | #' @param n Number of rows. 13 | #' @param m Number of columns. 14 | #' 15 | #' @return A data frame of character vectors. 16 | #' @export 17 | #' 18 | #' @examples 19 | #' gs4_fodder() 20 | #' gs4_fodder(5, 3) 21 | gs4_fodder <- function(n = 10, m = n) { 22 | columns <- LETTERS[seq_len(m)] 23 | names(columns) <- columns 24 | f <- function(number, letter) paste0(letter, number) 25 | as.data.frame( 26 | outer(seq_len(n) + 1, columns, f), 27 | stringsAsFactors = FALSE 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2023 googlesheets4 authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/testthat/test-range_speedread.R: -------------------------------------------------------------------------------- 1 | test_that("range_spreadread() works", { 2 | skip_if_offline() 3 | skip_if_no_token() 4 | skip_if_not_installed("readr") 5 | 6 | # specify a sheet-qualified cell range 7 | read <- range_read( 8 | test_sheet("googlesheets4-cell-tests"), 9 | range = "'range-experimentation'!B:D" 10 | ) 11 | speedread <- range_speedread( 12 | test_sheet("googlesheets4-cell-tests"), 13 | range = "'range-experimentation'!B:D", 14 | col_types = readr::cols() # suppress col spec printing 15 | ) 16 | expect_equal(read, speedread, ignore_attr = TRUE) 17 | 18 | # specify col_types 19 | read <- range_read( 20 | gs4_example("deaths"), 21 | sheet = "other", 22 | range = "A5:F15", 23 | col_types = "??i?DD" 24 | ) 25 | speedread <- range_speedread( 26 | gs4_example("deaths"), 27 | sheet = "other", 28 | range = "A5:F15", 29 | col_types = readr::cols( 30 | Age = readr::col_integer(), 31 | `Date of birth` = readr::col_date("%m/%d/%Y"), 32 | `Date of death` = readr::col_date("%m/%d/%Y") 33 | ) 34 | ) 35 | expect_equal(read, speedread, ignore_attr = TRUE) 36 | }) 37 | -------------------------------------------------------------------------------- /R/gs4_endpoints.R: -------------------------------------------------------------------------------- 1 | #' List Sheets endpoints 2 | #' 3 | #' Returns a list of selected Sheets API v4 endpoints, as stored inside the 4 | #' googlesheets4 package. The names of this list (or the `id` sub-elements) are 5 | #' the nicknames that can be used to specify an endpoint in 6 | #' [request_generate()]. For each endpoint, we store its nickname or `id`, the 7 | #' associated HTTP `method`, the `path`, and details about the parameters. This 8 | #' list is derived programmatically from the Sheets API v4 Discovery 9 | #' Document (`https://www.googleapis.com/discovery/v1/apis/sheets/v4/rest`). 10 | #' 11 | #' @param i The name(s) or integer index(ices) of the endpoints to return. 12 | #' Optional. By default, the entire list is returned. 13 | #' 14 | #' @return A list containing some or all of the subset of the Sheets API v4 15 | #' endpoints that are used internally by googlesheets4. 16 | #' @export 17 | #' 18 | #' @examples 19 | #' str(gs4_endpoints(), max.level = 2) 20 | #' gs4_endpoints("sheets.spreadsheets.values.get") 21 | #' gs4_endpoints(4) 22 | gs4_endpoints <- function(i = NULL) { 23 | if (is.null(i)) { 24 | i <- seq_along(.endpoints) 25 | } 26 | .endpoints[i] 27 | } 28 | -------------------------------------------------------------------------------- /R/gs4_share.R: -------------------------------------------------------------------------------- 1 | # currently just for development 2 | # I'm generally auth'd as: 3 | # * as a service acct (which means I can't look at anything in the browser) 4 | # * with Drive and Sheets scope 5 | # * with googlesheets4 and googledrive 6 | # so this is helpful for quickly granting anyone or myself specifically 7 | # permission to read or write a Sheet I'm fiddling with in the browser or the 8 | # API explorer 9 | # 10 | # Note defaults: role = "reader", type = "anyone" 11 | # --> "anyone with the link" can view 12 | # 13 | # examples: 14 | # gs4_share(ss) 15 | # gs4_share(ss, type = "user", emailAddress = "jane@example.com") 16 | # gs4_share(ss, type = "user", emailAddress = "jane@example.com", role = "writer") 17 | gs4_share <- function( 18 | ss, 19 | ..., 20 | role = c( 21 | "reader", 22 | "commenter", 23 | "writer", 24 | "owner", 25 | "organizer" 26 | ), 27 | type = c("anyone", "user", "group", "domain") 28 | ) { 29 | check_gs4_email_is_drive_email() 30 | role <- match.arg(role) 31 | type <- match.arg(type) 32 | googledrive::drive_share( 33 | file = as_sheets_id(ss), 34 | role = role, 35 | type = type, 36 | ... 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /data-raw/schemas/Spreadsheet: -------------------------------------------------------------------------------- 1 | # Spreadsheet 2 | # A tibble: 8 × 7 3 | property type instance_of array_of format readOnly enum 4 | 5 | 1 dataSourceSchedules array DataSourceRefreshSchedule TRUE 6 | 2 dataSources array DataSource NA 7 | 3 developerMetadata array DeveloperMetadata NA 8 | 4 namedRanges array NamedRange NA 9 | 5 properties object SpreadsheetProperties NA 10 | 6 sheets array Sheet NA 11 | 7 spreadsheetId string NA 12 | 8 spreadsheetUrl string NA 13 | -------------------------------------------------------------------------------- /R/schema_GridCoordinate.R: -------------------------------------------------------------------------------- 1 | as_GridCoordinate <- function(x, ...) { 2 | UseMethod("as_GridCoordinate") 3 | } 4 | 5 | #' @export 6 | as_GridCoordinate.default <- function(x, ...) { 7 | abort_unsupported_conversion(x, to = "GridCoordinate") 8 | } 9 | 10 | #' @export 11 | as_GridCoordinate.range_spec <- function(x, ..., strict = TRUE) { 12 | grid_range <- as_GridRange(x) 13 | 14 | if (identical(names(grid_range), "sheetId")) { 15 | return(new("GridCoordinate", sheetId = grid_range$sheetId)) 16 | } 17 | 18 | if (strict) { 19 | row_index_diff <- grid_range$endRowIndex - grid_range$startRowIndex 20 | col_index_diff <- grid_range$endColumnIndex - grid_range$startColumnIndex 21 | if (row_index_diff != 1 || col_index_diff != 1) { 22 | gs4_abort(c( 23 | "Range must identify exactly 1 cell:", 24 | x = "Invalid cell range: {.range {x$cell_range}}" 25 | )) 26 | } 27 | } 28 | 29 | grid_range <- grid_range %>% 30 | discard(is.null) %>% 31 | discard(is.na) 32 | new( 33 | "GridCoordinate", 34 | sheetId = grid_range$sheetId, 35 | rowIndex = grid_range$startRowIndex, 36 | columnIndex = grid_range$startColumnIndex 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /data-raw/schemas/SpreadsheetProperties: -------------------------------------------------------------------------------- 1 | # SpreadsheetProperties 2 | # A tibble: 8 × 6 3 | property type instance_of array_of format enum 4 | 5 | 1 autoRecalc enum 6 | 2 defaultFormat object CellFormat 7 | 3 importFunctionsExternalUrlAccessAllowed boolean 8 | 4 iterativeCalculationSettings object IterativeCalculationSettings 9 | 5 locale string 10 | 6 spreadsheetTheme object SpreadsheetTheme 11 | 7 timeZone string 12 | 8 title string 13 | -------------------------------------------------------------------------------- /man/gs4_endpoints.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/gs4_endpoints.R 3 | \name{gs4_endpoints} 4 | \alias{gs4_endpoints} 5 | \title{List Sheets endpoints} 6 | \usage{ 7 | gs4_endpoints(i = NULL) 8 | } 9 | \arguments{ 10 | \item{i}{The name(s) or integer index(ices) of the endpoints to return. 11 | Optional. By default, the entire list is returned.} 12 | } 13 | \value{ 14 | A list containing some or all of the subset of the Sheets API v4 15 | endpoints that are used internally by googlesheets4. 16 | } 17 | \description{ 18 | Returns a list of selected Sheets API v4 endpoints, as stored inside the 19 | googlesheets4 package. The names of this list (or the \code{id} sub-elements) are 20 | the nicknames that can be used to specify an endpoint in 21 | \code{\link[=request_generate]{request_generate()}}. For each endpoint, we store its nickname or \code{id}, the 22 | associated HTTP \code{method}, the \code{path}, and details about the parameters. This 23 | list is derived programmatically from the Sheets API v4 Discovery 24 | Document (\verb{https://www.googleapis.com/discovery/v1/apis/sheets/v4/rest}). 25 | } 26 | \examples{ 27 | str(gs4_endpoints(), max.level = 2) 28 | gs4_endpoints("sheets.spreadsheets.values.get") 29 | gs4_endpoints(4) 30 | } 31 | -------------------------------------------------------------------------------- /data-raw/schemas/CellFormat: -------------------------------------------------------------------------------- 1 | # CellFormat 2 | # A tibble: 12 × 7 3 | property type instance_of array_of format deprecated enum 4 | 5 | 1 backgroundColor object Color TRUE 6 | 2 backgroundColorStyle object ColorStyle NA 7 | 3 borders object Borders NA 8 | 4 horizontalAlignment enum NA 9 | 5 hyperlinkDisplayType enum NA 10 | 6 numberFormat object NumberFormat NA 11 | 7 padding object Padding NA 12 | 8 textDirection enum NA 13 | 9 textFormat object TextFormat NA 14 | 10 textRotation object TextRotation NA 15 | 11 verticalAlignment enum NA 16 | 12 wrapStrategy enum NA 17 | -------------------------------------------------------------------------------- /tests/testthat/helper.R: -------------------------------------------------------------------------------- 1 | auth_success <- tryCatch( 2 | gs4_auth_testing(), 3 | googlesheets4_auth_internal_error = function(e) NULL 4 | ) 5 | 6 | if (!isTRUE(auth_success)) { 7 | gs4_bullets(c( 8 | "!" = "Internal auth failed; calling {.fun gs4_deauth}." 9 | )) 10 | gs4_deauth() 11 | } 12 | 13 | skip_if_no_token <- function() { 14 | if (gs4_has_token()) { 15 | # hack to slow things down in CI 16 | Sys.sleep(3) 17 | } else { 18 | skip("No token") 19 | } 20 | } 21 | 22 | expect_gs4_error <- function(...) { 23 | expect_error(..., class = "googlesheets4_error") 24 | } 25 | 26 | local_ss <- function(name, ..., env = parent.frame()) { 27 | existing <- gs4_find(name) 28 | if (nrow(existing) > 0) { 29 | gs4_abort("A spreadsheet named {.s_sheet name} already exists.") 30 | } 31 | 32 | withr::defer( 33 | { 34 | trash_me <- gs4_find(name) 35 | if (nrow(trash_me) < 1) { 36 | cli::cli_warn( 37 | "The spreadsheet named {.s_sheet name} already seems to be deleted." 38 | ) 39 | } else { 40 | quiet <- gs4_quiet() %|% is_testing() 41 | if (quiet) { 42 | googledrive::local_drive_quiet() 43 | } 44 | googledrive::drive_trash(trash_me) 45 | } 46 | }, 47 | envir = env 48 | ) 49 | gs4_create(name, ...) 50 | } 51 | -------------------------------------------------------------------------------- /man/gs4_deauth.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/gs4_auth.R 3 | \name{gs4_deauth} 4 | \alias{gs4_deauth} 5 | \title{Suspend authorization} 6 | \usage{ 7 | gs4_deauth() 8 | } 9 | \description{ 10 | Put googlesheets4 into a de-authorized state. Instead of sending a token, 11 | googlesheets4 will send an API key. This can be used to access public 12 | resources for which no Google sign-in is required. This is handy for using 13 | googlesheets4 in a non-interactive setting to make requests that do not 14 | require a token. It will prevent the attempt to obtain a token 15 | interactively in the browser. The user can configure their own API key 16 | via \code{\link[=gs4_auth_configure]{gs4_auth_configure()}} and retrieve that key via 17 | \code{\link[=gs4_api_key]{gs4_api_key()}}. 18 | In the absence of a user-configured key, a built-in default key is used. 19 | } 20 | \examples{ 21 | \dontshow{if (rlang::is_interactive()) withAutoprint(\{ # examplesIf} 22 | gs4_deauth() 23 | gs4_user() 24 | 25 | # get metadata on the public 'deaths' spreadsheet 26 | gs4_example("deaths") \%>\% 27 | gs4_get() 28 | \dontshow{\}) # examplesIf} 29 | } 30 | \seealso{ 31 | Other auth functions: 32 | \code{\link{gs4_auth}()}, 33 | \code{\link{gs4_auth_configure}()}, 34 | \code{\link{gs4_scopes}()} 35 | } 36 | \concept{auth functions} 37 | -------------------------------------------------------------------------------- /man/gs4_examples.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/gs4_example.R 3 | \name{gs4_examples} 4 | \alias{gs4_examples} 5 | \alias{gs4_example} 6 | \title{Example Sheets} 7 | \usage{ 8 | gs4_examples(matches) 9 | 10 | gs4_example(matches) 11 | } 12 | \arguments{ 13 | \item{matches}{A regular expression that matches the name of the desired 14 | example Sheet(s). \code{matches} is optional for the plural \code{gs4_examples()} 15 | and, if provided, it can match multiple Sheets. The singular 16 | \code{gs4_example()} requires \code{matches} and it must match exactly one Sheet.} 17 | } 18 | \value{ 19 | \itemize{ 20 | \item \code{gs4_example()}: a \link{sheets_id} 21 | \item \code{gs4_examples()}: a named vector of all built-in examples, with class 22 | \code{\link[googledrive:drive_id]{drive_id}} 23 | } 24 | } 25 | \description{ 26 | googlesheets4 makes a variety of world-readable example Sheets available for 27 | use in documentation and reprexes. These functions help you access the 28 | example Sheets. See \code{vignette("example-sheets", package = "googlesheets4")} 29 | for more. 30 | } 31 | \examples{ 32 | \dontshow{if (gs4_has_token()) withAutoprint(\{ # examplesIf} 33 | gs4_examples() 34 | gs4_examples("gap") 35 | 36 | gs4_example("gapminder") 37 | gs4_example("deaths") 38 | \dontshow{\}) # examplesIf} 39 | } 40 | -------------------------------------------------------------------------------- /man/gs4_token.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/gs4_auth.R 3 | \name{gs4_token} 4 | \alias{gs4_token} 5 | \title{Produce configured token} 6 | \usage{ 7 | gs4_token() 8 | } 9 | \value{ 10 | A \code{request} object (an S3 class provided by \link[httr:httr-package]{httr}). 11 | } 12 | \description{ 13 | For internal use or for those programming around the Sheets API. 14 | Returns a token pre-processed with \code{\link[httr:config]{httr::config()}}. Most users 15 | do not need to handle tokens "by hand" or, even if they need some 16 | control, \code{\link[=gs4_auth]{gs4_auth()}} is what they need. If there is no current 17 | token, \code{\link[=gs4_auth]{gs4_auth()}} is called to either load from cache or 18 | initiate OAuth2.0 flow. 19 | If auth has been deactivated via \code{\link[=gs4_deauth]{gs4_deauth()}}, \code{gs4_token()} 20 | returns \code{NULL}. 21 | } 22 | \examples{ 23 | \dontshow{if (gs4_has_token()) withAutoprint(\{ # examplesIf} 24 | req <- request_generate( 25 | "sheets.spreadsheets.get", 26 | list(spreadsheetId = "abc"), 27 | token = gs4_token() 28 | ) 29 | req 30 | \dontshow{\}) # examplesIf} 31 | } 32 | \seealso{ 33 | Other low-level API functions: 34 | \code{\link{gs4_has_token}()}, 35 | \code{\link{request_generate}()}, 36 | \code{\link{request_make}()} 37 | } 38 | \concept{low-level API functions} 39 | -------------------------------------------------------------------------------- /R/gs4_get.R: -------------------------------------------------------------------------------- 1 | #' Get Sheet metadata 2 | #' 3 | #' Retrieve spreadsheet-specific metadata, such as details on the individual 4 | #' (work)sheets or named ranges. 5 | #' * `gs4_get()` complements [googledrive::drive_get()], which 6 | #' returns metadata that exists for any file on Drive. 7 | #' 8 | #' @eval param_ss() 9 | #' 10 | #' @return A list with S3 class `googlesheets4_spreadsheet`, for printing 11 | #' purposes. 12 | #' @export 13 | #' @seealso Wraps the `spreadsheets.get` endpoint: 14 | #' * 15 | #' 16 | #' @examplesIf gs4_has_token() 17 | #' gs4_get(gs4_example("mini-gap")) 18 | gs4_get <- function(ss) { 19 | resp <- gs4_get_impl_(as_sheets_id(ss)) 20 | new_googlesheets4_spreadsheet(resp) 21 | } 22 | 23 | ## I want a separate worker so there is a version of this available that 24 | ## accepts `fields`, yet I don't want a user-facing function with `fields` arg 25 | gs4_get_impl_ <- function(ssid, fields = NULL) { 26 | fields <- fields %||% 27 | "spreadsheetId,properties,spreadsheetUrl,sheets.properties,sheets.protectedRanges,namedRanges" 28 | req <- request_generate( 29 | "sheets.spreadsheets.get", 30 | params = list( 31 | spreadsheetId = ssid, 32 | fields = fields 33 | ) 34 | ) 35 | raw_resp <- request_make(req) 36 | gargle::response_process(raw_resp) 37 | } 38 | -------------------------------------------------------------------------------- /man/googlesheets4-package.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/googlesheets4-package.R 3 | \docType{package} 4 | \name{googlesheets4-package} 5 | \alias{googlesheets4} 6 | \alias{googlesheets4-package} 7 | \title{googlesheets4: Access Google Sheets using the Sheets API V4} 8 | \description{ 9 | \if{html}{\figure{logo.png}{options: style='float: right' alt='logo' width='120'}} 10 | 11 | Interact with Google Sheets through the Sheets API v4 \url{https://developers.google.com/sheets/api}. "API" is an acronym for "application programming interface"; the Sheets API allows users to interact with Google Sheets programmatically, instead of via a web browser. The "v4" refers to the fact that the Sheets API is currently at version 4. This package can read and write both the metadata and the cell data in a Sheet. 12 | } 13 | \seealso{ 14 | Useful links: 15 | \itemize{ 16 | \item \url{https://googlesheets4.tidyverse.org} 17 | \item \url{https://github.com/tidyverse/googlesheets4} 18 | \item Report bugs at \url{https://github.com/tidyverse/googlesheets4/issues} 19 | } 20 | 21 | } 22 | \author{ 23 | \strong{Maintainer}: Jennifer Bryan \email{jenny@posit.co} (\href{https://orcid.org/0000-0002-6983-2759}{ORCID}) 24 | 25 | Other contributors: 26 | \itemize{ 27 | \item Posit Software, PBC [copyright holder, funder] 28 | } 29 | 30 | } 31 | \keyword{internal} 32 | -------------------------------------------------------------------------------- /man/figures/lifecycle-defunct.svg: -------------------------------------------------------------------------------- 1 | 2 | lifecycle: defunct 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | lifecycle 18 | 19 | defunct 20 | 21 | 22 | -------------------------------------------------------------------------------- /man/figures/lifecycle-archived.svg: -------------------------------------------------------------------------------- 1 | 2 | lifecycle: archived 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | lifecycle 18 | 19 | archived 20 | 21 | 22 | -------------------------------------------------------------------------------- /man/figures/lifecycle-maturing.svg: -------------------------------------------------------------------------------- 1 | 2 | lifecycle: maturing 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | lifecycle 18 | 19 | maturing 20 | 21 | 22 | -------------------------------------------------------------------------------- /man/figures/lifecycle-deprecated.svg: -------------------------------------------------------------------------------- 1 | 2 | lifecycle: deprecated 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | lifecycle 18 | 19 | deprecated 20 | 21 | 22 | -------------------------------------------------------------------------------- /man/figures/lifecycle-superseded.svg: -------------------------------------------------------------------------------- 1 | 2 | lifecycle: superseded 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | lifecycle 18 | 19 | superseded 20 | 21 | 22 | -------------------------------------------------------------------------------- /tests/testthat/test-range_flood.R: -------------------------------------------------------------------------------- 1 | # ---- nm_fun ---- 2 | me_ <- nm_fun("TEST-range_flood") 3 | 4 | # ---- tests ---- 5 | test_that("range_flood() works", { 6 | skip_if_offline() 7 | skip_if_no_token() 8 | 9 | dat <- tibble::tibble(x = rep(1, 3), y = rep(2, 3), z = rep(3, 3)) 10 | ss <- local_ss(me_(), sheets = list(dat)) 11 | 12 | # clear values and format 13 | range_flood(ss, range = "A:A") 14 | 15 | # reset values and reformat 16 | range_flood(ss, range = "B:B", cell = "hi") 17 | 18 | # reset values, leave format unchanged 19 | range_flood(ss, range = "C:C", cell = "bye", reformat = FALSE) 20 | 21 | out <- range_read_cells(ss, cell_data = "full", discard_empty = FALSE) 22 | 23 | expect_equal( 24 | purrr::map_chr(out$cell, "formattedValue", .default = ""), 25 | rep(c("", "hi", "bye"), 4) 26 | ) 27 | 28 | column_A <- out[out$col == 1, ] 29 | fmts <- purrr::map(column_A$cell, "effectiveFormat") 30 | expect_true(all(purrr::map_lgl(fmts, is.null))) 31 | 32 | column_B <- out[out$col == 2, ] 33 | fmts <- purrr::map(column_B$cell, c("effectiveFormat", "backgroundColor")) 34 | expect_true(all(unlist(fmts) == 1)) 35 | 36 | column_C_header <- out[out$col == 3 & out$row == 1, ] 37 | fmt <- purrr::pluck( 38 | column_C_header, 39 | "cell", 40 | 1, 41 | "effectiveFormat", 42 | "backgroundColor" 43 | ) 44 | expect_true(all(unlist(fmt) < 1)) 45 | }) 46 | -------------------------------------------------------------------------------- /man/figures/lifecycle-questioning.svg: -------------------------------------------------------------------------------- 1 | 2 | lifecycle: questioning 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | lifecycle 18 | 19 | questioning 20 | 21 | 22 | -------------------------------------------------------------------------------- /man/figures/lifecycle-experimental.svg: -------------------------------------------------------------------------------- 1 | 2 | lifecycle: experimental 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | lifecycle 18 | 19 | experimental 20 | 21 | 22 | -------------------------------------------------------------------------------- /man/figures/lifecycle-soft-deprecated.svg: -------------------------------------------------------------------------------- 1 | 2 | lifecycle: soft-deprecated 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | lifecycle 18 | 19 | soft-deprecated 20 | 21 | 22 | -------------------------------------------------------------------------------- /R/schema_Sheet.R: -------------------------------------------------------------------------------- 1 | #' @export 2 | as_tibble.googlesheets4_schema_Sheet <- function(x, ...) { 3 | out <- as_tibble(new("SheetProperties", !!!x$properties)) 4 | # TODO: come back to deal with `data` 5 | tibble::add_column(out, data = list(NULL)) 6 | } 7 | 8 | as_Sheet <- function(x, ...) { 9 | UseMethod("as_Sheet") 10 | } 11 | 12 | #' @export 13 | as_Sheet.default <- function(x, ...) { 14 | abort_unsupported_conversion(x, to = "Sheet") 15 | } 16 | 17 | #' @export 18 | as_Sheet.NULL <- function(x, ...) { 19 | return(new(id = "Sheet", properties = NULL)) 20 | } 21 | 22 | #' @export 23 | as_Sheet.character <- function(x, ...) { 24 | check_length_one(x) 25 | new( 26 | "Sheet", 27 | properties = new(id = "SheetProperties", title = x), 28 | ... 29 | ) 30 | } 31 | 32 | #' @export 33 | as_Sheet.data.frame <- function(x, ...) { 34 | # do first, so that gridProperties derived from x overwrite anything passed 35 | # via `...` 36 | sp <- new("SheetProperties", ...) 37 | 38 | sp <- patch( 39 | sp, 40 | gridProperties = new( 41 | "GridProperties", 42 | rowCount = nrow(x) + 1, # make room for column names 43 | columnCount = ncol(x), 44 | ) 45 | ) 46 | 47 | new( 48 | "Sheet", 49 | properties = sp, 50 | data = list( 51 | # an array of instances of GridData 52 | list( 53 | rowData = as_RowData(x) # an array of instances of RowData 54 | ) 55 | ) 56 | ) 57 | } 58 | -------------------------------------------------------------------------------- /tests/testthat/test-sheet_add.R: -------------------------------------------------------------------------------- 1 | # ---- nm_fun ---- 2 | me_ <- nm_fun("TEST-sheet_add") 3 | 4 | # ---- tests ---- 5 | test_that("sheet_add() rejects non-character `sheet`", { 6 | expect_snapshot( 7 | sheet_add(test_sheet("googlesheets4-cell-tests"), sheet = 3), 8 | error = TRUE 9 | ) 10 | }) 11 | 12 | test_that("sheet_add() works", { 13 | skip_if_offline() 14 | skip_if_no_token() 15 | 16 | ss <- local_ss(me_()) 17 | 18 | expect_no_error( 19 | sheet_add(ss) 20 | ) 21 | 22 | expect_no_error( 23 | sheet_add(ss, "apple", .after = 1) 24 | ) 25 | 26 | expect_no_error( 27 | sheet_add(ss, "banana", .after = "apple") 28 | ) 29 | 30 | expect_no_error( 31 | sheet_add(ss, c("coconut", "dragonfruit")) 32 | ) 33 | 34 | expect_no_error( 35 | sheet_add( 36 | ss, 37 | sheet = "eggplant", 38 | .before = 1, 39 | gridProperties = list( 40 | rowCount = 3, 41 | columnCount = 6, 42 | frozenRowCount = 1 43 | ) 44 | ) 45 | ) 46 | 47 | sheets_df <- sheet_properties(ss) 48 | 49 | expect_identical( 50 | sheets_df$name, 51 | c( 52 | "eggplant", 53 | "Sheet1", 54 | "apple", 55 | "banana", 56 | "Sheet2", 57 | "coconut", 58 | "dragonfruit" 59 | ) 60 | ) 61 | expect_identical(vlookup("eggplant", sheets_df, "name", "grid_rows"), 3L) 62 | expect_identical(vlookup("eggplant", sheets_df, "name", "grid_columns"), 6L) 63 | }) 64 | -------------------------------------------------------------------------------- /man/figures/lifecycle-stable.svg: -------------------------------------------------------------------------------- 1 | 2 | lifecycle: stable 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 19 | 20 | lifecycle 21 | 22 | 25 | 26 | stable 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /R/gs4_find.R: -------------------------------------------------------------------------------- 1 | #' Find Google Sheets 2 | #' 3 | #' Finds your Google Sheets. This is a very thin wrapper around 4 | #' [googledrive::drive_find()], that specifies you want to list Drive files 5 | #' where `type = "spreadsheet"`. Therefore, note that this will require auth for 6 | #' googledrive! See the article [Using googlesheets4 with 7 | #' googledrive](https://googlesheets4.tidyverse.org/articles/articles/drive-and-sheets.html) 8 | #' if you want to coordinate auth between googlesheets4 and googledrive. This 9 | #' function will emit an informational message if you are currently logged in 10 | #' with both googlesheets4 and googledrive, but as different users. 11 | #' 12 | #' @param ... Arguments (other than `type`, which is hard-wired as `type = 13 | #' "spreadsheet"`) that are passed along to [googledrive::drive_find()]. 14 | #' 15 | #' @inherit googledrive::drive_find return 16 | #' @export 17 | #' 18 | #' @examplesIf gs4_has_token() 19 | #' # see all your Sheets 20 | #' gs4_find() 21 | #' 22 | #' # see 5 Sheets, prioritized by creation time 23 | #' x <- gs4_find(order_by = "createdTime desc", n_max = 5) 24 | #' x 25 | #' 26 | #' # hoist the creation date, using other packages in the tidyverse 27 | #' # x %>% 28 | #' # tidyr::hoist(drive_resource, created_on = "createdTime") %>% 29 | #' # dplyr::mutate(created_on = as.Date(created_on)) 30 | gs4_find <- function(...) { 31 | check_gs4_email_is_drive_email() 32 | googledrive::drive_find(..., type = "spreadsheet") 33 | } 34 | -------------------------------------------------------------------------------- /data-raw/schemas/SheetProperties: -------------------------------------------------------------------------------- 1 | # SheetProperties 2 | # A tibble: 10 × 8 3 | property type instance_of array_of format deprecated readOnly enum 4 | 5 | 1 dataSourceSheetProperties object DataSourceSheetProperties NA TRUE 6 | 2 gridProperties object GridProperties NA NA 7 | 3 hidden boolean NA NA 8 | 4 index integer int32 NA NA 9 | 5 rightToLeft boolean NA NA 10 | 6 sheetId integer int32 NA NA 11 | 7 sheetType enum NA NA 12 | 8 tabColor object Color TRUE NA 13 | 9 tabColorStyle object ColorStyle NA NA 14 | 10 title string NA NA 15 | -------------------------------------------------------------------------------- /data-raw/schemas/CellData: -------------------------------------------------------------------------------- 1 | # CellData 2 | # A tibble: 13 × 7 3 | property type instance_of array_of format readOnly enum 4 | 5 | 1 chipRuns array ChipRun NA 6 | 2 dataSourceFormula object DataSourceFormula TRUE 7 | 3 dataSourceTable object DataSourceTable NA 8 | 4 dataValidation object DataValidationRule NA 9 | 5 effectiveFormat object CellFormat NA 10 | 6 effectiveValue object ExtendedValue NA 11 | 7 formattedValue string NA 12 | 8 hyperlink string NA 13 | 9 note string NA 14 | 10 pivotTable object PivotTable NA 15 | 11 textFormatRuns array TextFormatRun NA 16 | 12 userEnteredFormat object CellFormat NA 17 | 13 userEnteredValue object ExtendedValue NA 18 | -------------------------------------------------------------------------------- /man/gs4_get.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/gs4_get.R 3 | \name{gs4_get} 4 | \alias{gs4_get} 5 | \title{Get Sheet metadata} 6 | \usage{ 7 | gs4_get(ss) 8 | } 9 | \arguments{ 10 | \item{ss}{Something that identifies a Google Sheet: 11 | \itemize{ 12 | \item its file id as a string or \code{\link[googledrive:drive_id]{drive_id}} 13 | \item a URL from which we can recover the id 14 | \item a one-row \code{\link[googledrive:dribble]{dribble}}, which is how googledrive 15 | represents Drive files 16 | \item an instance of \code{googlesheets4_spreadsheet}, which is what \code{\link[=gs4_get]{gs4_get()}} 17 | returns 18 | } 19 | 20 | Processed through \code{\link[=as_sheets_id]{as_sheets_id()}}.} 21 | } 22 | \value{ 23 | A list with S3 class \code{googlesheets4_spreadsheet}, for printing 24 | purposes. 25 | } 26 | \description{ 27 | Retrieve spreadsheet-specific metadata, such as details on the individual 28 | (work)sheets or named ranges. 29 | \itemize{ 30 | \item \code{gs4_get()} complements \code{\link[googledrive:drive_get]{googledrive::drive_get()}}, which 31 | returns metadata that exists for any file on Drive. 32 | } 33 | } 34 | \examples{ 35 | \dontshow{if (gs4_has_token()) withAutoprint(\{ # examplesIf} 36 | gs4_get(gs4_example("mini-gap")) 37 | \dontshow{\}) # examplesIf} 38 | } 39 | \seealso{ 40 | Wraps the \code{spreadsheets.get} endpoint: 41 | \itemize{ 42 | \item \url{https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/get} 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /data-raw/schemas/Sheet: -------------------------------------------------------------------------------- 1 | # Sheet 2 | # A tibble: 14 × 6 3 | property type instance_of array_of format enum 4 | 5 | 1 bandedRanges array BandedRange 6 | 2 basicFilter object BasicFilter 7 | 3 charts array EmbeddedChart 8 | 4 columnGroups array DimensionGroup 9 | 5 conditionalFormats array ConditionalFormatRule 10 | 6 data array GridData 11 | 7 developerMetadata array DeveloperMetadata 12 | 8 filterViews array FilterView 13 | 9 merges array GridRange 14 | 10 properties object SheetProperties 15 | 11 protectedRanges array ProtectedRange 16 | 12 rowGroups array DimensionGroup 17 | 13 slicers array Slicer 18 | 14 tables array Table 19 | -------------------------------------------------------------------------------- /data-raw/gapminder-example-sheets.R: -------------------------------------------------------------------------------- 1 | library(tidyverse) 2 | library(googledrive) 3 | library(googlesheets4) 4 | library(gapminder) 5 | 6 | googlesheets4:::gs4_auth_docs() 7 | 8 | # if I were making the gapminder sheet from scratch, here's what I would do now: 9 | ss <- gs4_create( 10 | "gapminder-reboot", 11 | sheets = split(gapminder, gapminder$continent) 12 | ) 13 | drive_trash(ss) 14 | # but I am not doing this -- I want to keep the existing Sheet ID 15 | # instead I will edit it (and mini-gap) in situ 16 | 17 | ## Update gapminder example Sheet ---- 18 | ss <- gs4_find("gapminder") %>% as_sheets_id() 19 | # gs4_browse(ss) 20 | 21 | gapminder_split <- split(gapminder, gapminder$continent) 22 | sheet_write(gapminder_split$Africa, ss = ss, sheet = "Africa") 23 | sheet_write(gapminder_split$Americas, ss = ss, sheet = "Americas") 24 | sheet_write(gapminder_split$Asia, ss = ss, sheet = "Asia") 25 | sheet_write(gapminder_split$Europe, ss = ss, sheet = "Europe") 26 | sheet_write(gapminder_split$Oceania, ss = ss, sheet = "Oceania") 27 | 28 | ## Update mini-gap example Sheet ---- 29 | mini_gap <- gapminder %>% 30 | arrange(year) %>% 31 | group_by(continent) %>% 32 | slice(1:5) %>% 33 | split(.$continent) 34 | 35 | (ss <- gs4_find("mini-gap") %>% as_sheets_id()) 36 | # gs4_browse(ss) 37 | 38 | sheet_write(mini_gap$Africa, ss = ss, sheet = "Africa") 39 | sheet_write(mini_gap$Americas, ss = ss, sheet = "Americas") 40 | sheet_write(mini_gap$Asia, ss = ss, sheet = "Asia") 41 | sheet_write(mini_gap$Europe, ss = ss, sheet = "Europe") 42 | sheet_write(mini_gap$Oceania, ss = ss, sheet = "Oceania") 43 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/gs4_auth.md: -------------------------------------------------------------------------------- 1 | # gs4_auth_configure works 2 | 3 | Code 4 | gs4_auth_configure(client = gargle::gargle_client(), path = "PATH") 5 | Condition 6 | Error in `gs4_auth_configure()`: 7 | ! Must supply exactly one of `client` and `path`, not both. 8 | 9 | # gs4_oauth_app() is deprecated 10 | 11 | Code 12 | absorb <- gs4_oauth_app() 13 | Condition 14 | Warning: 15 | `gs4_oauth_app()` was deprecated in googlesheets4 1.1.0. 16 | i Please use `gs4_oauth_client()` instead. 17 | 18 | # gs4_auth_configure(app =) is deprecated in favor of client 19 | 20 | Code 21 | gs4_auth_configure(app = client) 22 | Condition 23 | Warning: 24 | The `app` argument of `gs4_auth_configure()` is deprecated as of googlesheets4 1.1.0. 25 | i Please use the `client` argument instead. 26 | 27 | # gs4_scopes() reveals Sheets scopes 28 | 29 | Code 30 | gs4_scopes() 31 | Output 32 | spreadsheets 33 | "https://www.googleapis.com/auth/spreadsheets" 34 | spreadsheets.readonly 35 | "https://www.googleapis.com/auth/spreadsheets.readonly" 36 | drive 37 | "https://www.googleapis.com/auth/drive" 38 | drive.readonly 39 | "https://www.googleapis.com/auth/drive.readonly" 40 | drive.file 41 | "https://www.googleapis.com/auth/drive.file" 42 | 43 | -------------------------------------------------------------------------------- /man/gs4_find.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/gs4_find.R 3 | \name{gs4_find} 4 | \alias{gs4_find} 5 | \title{Find Google Sheets} 6 | \usage{ 7 | gs4_find(...) 8 | } 9 | \arguments{ 10 | \item{...}{Arguments (other than \code{type}, which is hard-wired as \code{type = "spreadsheet"}) that are passed along to \code{\link[googledrive:drive_find]{googledrive::drive_find()}}.} 11 | } 12 | \value{ 13 | An object of class \code{\link[googledrive]{dribble}}, a tibble with one row per file. 14 | } 15 | \description{ 16 | Finds your Google Sheets. This is a very thin wrapper around 17 | \code{\link[googledrive:drive_find]{googledrive::drive_find()}}, that specifies you want to list Drive files 18 | where \code{type = "spreadsheet"}. Therefore, note that this will require auth for 19 | googledrive! See the article \href{https://googlesheets4.tidyverse.org/articles/articles/drive-and-sheets.html}{Using googlesheets4 with googledrive} 20 | if you want to coordinate auth between googlesheets4 and googledrive. This 21 | function will emit an informational message if you are currently logged in 22 | with both googlesheets4 and googledrive, but as different users. 23 | } 24 | \examples{ 25 | \dontshow{if (gs4_has_token()) withAutoprint(\{ # examplesIf} 26 | # see all your Sheets 27 | gs4_find() 28 | 29 | # see 5 Sheets, prioritized by creation time 30 | x <- gs4_find(order_by = "createdTime desc", n_max = 5) 31 | x 32 | 33 | # hoist the creation date, using other packages in the tidyverse 34 | # x \%>\% 35 | # tidyr::hoist(drive_resource, created_on = "createdTime") \%>\% 36 | # dplyr::mutate(created_on = as.Date(created_on)) 37 | \dontshow{\}) # examplesIf} 38 | } 39 | -------------------------------------------------------------------------------- /tests/testthat/test-schema_GridCoordinate.R: -------------------------------------------------------------------------------- 1 | test_that("we can make a GridCoordinate from a range_spec, simplest case", { 2 | sheets_df <- tibble::tibble(name = "abc", id = 123) 3 | 4 | spec <- new_range_spec(sheet_name = "abc", sheets_df = sheets_df) 5 | out <- as_GridCoordinate(spec) 6 | expect_equal(out$sheetId, 123) 7 | expect_length(out, 1) 8 | 9 | spec <- new_range_spec( 10 | sheet_name = "abc", 11 | cell_range = "G3", 12 | sheets_df = sheets_df 13 | ) 14 | out <- as_GridCoordinate(spec) 15 | expect_equal(out$rowIndex, 2) 16 | expect_equal(out$columnIndex, 6) 17 | }) 18 | 19 | test_that("we can (or won't) make a GridCoordinate from a mutli-cell range", { 20 | sheets_df <- tibble::tibble(name = "abc", id = 123) 21 | 22 | spec <- new_range_spec( 23 | sheet_name = "abc", 24 | cell_range = "A3:B4", 25 | sheets_df = sheets_df 26 | ) 27 | expect_error(as_GridCoordinate(spec), "Invalid cell range") 28 | 29 | spec2 <- new_range_spec( 30 | sheet_name = "abc", 31 | cell_range = "A3", 32 | sheets_df = sheets_df 33 | ) 34 | expect_equal( 35 | as_GridCoordinate(spec, strict = FALSE), 36 | as_GridCoordinate(spec2) 37 | ) 38 | 39 | spec <- new_range_spec( 40 | sheet_name = "abc", 41 | cell_range = "A:B", 42 | sheets_df = sheets_df 43 | ) 44 | out <- as_GridCoordinate(spec, strict = FALSE) 45 | expect_null(out$rowIndex) 46 | expect_equal(out$columnIndex, 0) 47 | 48 | spec <- new_range_spec( 49 | sheet_name = "abc", 50 | cell_range = "2:4", 51 | sheets_df = sheets_df 52 | ) 53 | out <- as_GridCoordinate(spec, strict = FALSE) 54 | expect_equal(out$rowIndex, 1) 55 | expect_null(out$columnIndex) 56 | }) 57 | -------------------------------------------------------------------------------- /man/sheet_properties.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/sheet_properties.R 3 | \name{sheet_properties} 4 | \alias{sheet_properties} 5 | \alias{sheet_names} 6 | \title{Get data about (work)sheets} 7 | \usage{ 8 | sheet_properties(ss) 9 | 10 | sheet_names(ss) 11 | } 12 | \arguments{ 13 | \item{ss}{Something that identifies a Google Sheet: 14 | \itemize{ 15 | \item its file id as a string or \code{\link[googledrive:drive_id]{drive_id}} 16 | \item a URL from which we can recover the id 17 | \item a one-row \code{\link[googledrive:dribble]{dribble}}, which is how googledrive 18 | represents Drive files 19 | \item an instance of \code{googlesheets4_spreadsheet}, which is what \code{\link[=gs4_get]{gs4_get()}} 20 | returns 21 | } 22 | 23 | Processed through \code{\link[=as_sheets_id]{as_sheets_id()}}.} 24 | } 25 | \value{ 26 | \itemize{ 27 | \item \code{sheet_properties()}: A tibble with one row per (work)sheet. 28 | \item \code{sheet_names()}: A character vector of (work)sheet names. 29 | } 30 | } 31 | \description{ 32 | Reveals full metadata or just the names for the (work)sheets inside a 33 | (spread)Sheet. 34 | } 35 | \examples{ 36 | \dontshow{if (gs4_has_token()) withAutoprint(\{ # examplesIf} 37 | ss <- gs4_example("gapminder") 38 | sheet_properties(ss) 39 | sheet_names(ss) 40 | \dontshow{\}) # examplesIf} 41 | } 42 | \seealso{ 43 | Other worksheet functions: 44 | \code{\link{sheet_add}()}, 45 | \code{\link{sheet_append}()}, 46 | \code{\link{sheet_copy}()}, 47 | \code{\link{sheet_delete}()}, 48 | \code{\link{sheet_relocate}()}, 49 | \code{\link{sheet_rename}()}, 50 | \code{\link{sheet_resize}()}, 51 | \code{\link{sheet_write}()} 52 | } 53 | \concept{worksheet functions} 54 | -------------------------------------------------------------------------------- /data-raw/gs4-examples-inventory.R: -------------------------------------------------------------------------------- 1 | # given the files owned by the googlesheets4-sheet-keeper service account, 2 | # create/update an inventory file consulted by gs4_examples() 3 | 4 | library(here) 5 | library(googledrive) 6 | library(tidyverse) 7 | library(googlesheets4) 8 | 9 | # auth with the special-purpose service account 10 | gs4_auth( 11 | path = "~/.R/gargle/googlesheets4-sheet-keeper.json", 12 | scopes = "https://www.googleapis.com/auth/drive" 13 | ) 14 | gs4_user() 15 | drive_auth(token = gs4_token()) 16 | drive_user() 17 | 18 | # exclude the inventory Sheet ... too meta! 19 | dat <- drive_find(q = "not name contains 'gs4_example_and_test_sheets'") 20 | 21 | if (anyDuplicated(dat$name)) { 22 | stop("Duplicated file names! You are making a huge mistake.") 23 | } 24 | 25 | dat <- dat %>% 26 | mutate( 27 | purpose = if_else(str_detect(name, "^googlesheets4"), "test", "example") 28 | ) %>% 29 | select(name, purpose, id) %>% 30 | arrange(purpose, name) 31 | 32 | # record in local csv, because the visibility afforded by a plain old csv file 33 | # is useful to me, e.g. easy to see change over time 34 | write_csv( 35 | dat, 36 | file = here("inst", "extdata", "example_and_test_sheets.csv") 37 | ) 38 | 39 | # initial creation 40 | 41 | # ss <- gs4_create( 42 | # "gs4_example_and_test_sheets", 43 | # sheets = list(gs4_example_and_test_sheets = dat) 44 | # ) 45 | # drive_share_anyone(ss) 46 | # drive_publish(ss) 47 | 48 | # as_id(ss) 49 | # 1dSIZ2NkEPDWiEbsg9G80Hr9Xe7HZglEAPwGhVa-OSyA 50 | 51 | # in future, write over the data to update 52 | ssid <- as_sheets_id("1dSIZ2NkEPDWiEbsg9G80Hr9Xe7HZglEAPwGhVa-OSyA") 53 | ss <- gs4_get(ssid) 54 | ss 55 | 56 | # range_autofit(ss) 57 | drive_browse(ssid) 58 | -------------------------------------------------------------------------------- /R/roxygen.R: -------------------------------------------------------------------------------- 1 | # functions to help reduce duplication and increase consistency in the docs 2 | 3 | ### ss ---- 4 | param_ss <- function(..., pname = "ss") { 5 | template <- glue( 6 | " 7 | @param {pname} \\ 8 | Something that identifies a Google Sheet: 9 | * its file id as a string or [`drive_id`][googledrive::as_id] 10 | * a URL from which we can recover the id 11 | * a one-row [`dribble`][googledrive::dribble], which is how googledrive 12 | represents Drive files 13 | * an instance of `googlesheets4_spreadsheet`, which is what [gs4_get()] 14 | returns 15 | 16 | Processed through [as_sheets_id()]." 17 | ) 18 | dots <- list2(...) 19 | if (length(dots) > 0) { 20 | template <- c(template, dots) 21 | } 22 | glue_collapse(template, sep = " ") 23 | } 24 | 25 | ### sheet ---- 26 | param_sheet <- function(..., action = "act on", pname = "sheet") { 27 | template <- glue( 28 | " 29 | @param {pname} \\ 30 | Sheet to {action}, in the sense of \"worksheet\" or \"tab\". \\ 31 | You can identify a sheet by name, with a string, or by position, \\ 32 | with a number. 33 | " 34 | ) 35 | dots <- list2(...) 36 | if (length(dots) > 0) { 37 | template <- c(template, dots) 38 | } 39 | glue_collapse(template, sep = " ") 40 | } 41 | 42 | param_before_after <- function(sheet_text) { 43 | glue( 44 | " 45 | @param .before,.after \\ 46 | Optional specification of where to put the new {sheet_text}. \\ 47 | Specify, at most, one of `.before` and `.after`. Refer to an existing \\ 48 | sheet by name (via a string) or by position (via a number). If \\ 49 | unspecified, Sheets puts the new {sheet_text} at the end. 50 | " 51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: googlesheets4 2 | Title: Access Google Sheets using the Sheets API V4 3 | Version: 1.1.2.9000 4 | Authors@R: c( 5 | person("Jennifer", "Bryan", , "jenny@posit.co", role = c("cre", "aut"), 6 | comment = c(ORCID = "0000-0002-6983-2759")), 7 | person("Posit Software, PBC", role = c("cph", "fnd")) 8 | ) 9 | Description: Interact with Google Sheets through the Sheets API v4 10 | . "API" is an acronym for 11 | "application programming interface"; the Sheets API allows users to 12 | interact with Google Sheets programmatically, instead of via a web 13 | browser. The "v4" refers to the fact that the Sheets API is currently 14 | at version 4. This package can read and write both the metadata and 15 | the cell data in a Sheet. 16 | License: MIT + file LICENSE 17 | URL: https://googlesheets4.tidyverse.org, 18 | https://github.com/tidyverse/googlesheets4 19 | BugReports: https://github.com/tidyverse/googlesheets4/issues 20 | Depends: 21 | R (>= 3.6) 22 | Imports: 23 | cellranger, 24 | cli (>= 3.0.0), 25 | curl, 26 | gargle (>= 1.6.0), 27 | glue (>= 1.3.0), 28 | googledrive (>= 2.1.0), 29 | httr, 30 | ids, 31 | lifecycle, 32 | magrittr, 33 | methods, 34 | purrr, 35 | rematch2, 36 | rlang (>= 1.0.2), 37 | tibble (>= 2.1.1), 38 | utils, 39 | vctrs (>= 0.2.3), 40 | withr 41 | Suggests: 42 | readr, 43 | rmarkdown, 44 | spelling, 45 | testthat (>= 3.1.7) 46 | ByteCompile: true 47 | Config/Needs/website: 48 | tidyverse, 49 | tidyverse/tidytemplate 50 | Config/testthat/edition: 3 51 | Encoding: UTF-8 52 | Language: en-US 53 | Roxygen: list(markdown = TRUE) 54 | RoxygenNote: 7.3.2.9000 55 | -------------------------------------------------------------------------------- /tests/testthat/test-gs4_formula.R: -------------------------------------------------------------------------------- 1 | test_that("constructors return length-0 vector when called with no arguments", { 2 | expect_length(new_formula(), 0) 3 | expect_length(gs4_formula(), 0) 4 | }) 5 | 6 | test_that("low-level constructor errors for non-character input", { 7 | expect_error(new_formula(1:3), class = "vctrs_error_assert_ptype") 8 | }) 9 | 10 | test_that("user-friendly constructor works for coercible input", { 11 | expect_s3_class( 12 | gs4_formula(factor("=sum(A:A)")), 13 | "googlesheets4_formula" 14 | ) 15 | }) 16 | 17 | test_that("common type of googlesheets4_formula and character is character", { 18 | expect_identical( 19 | vctrs::vec_ptype2(character(), gs4_formula()), 20 | character() 21 | ) 22 | expect_identical( 23 | vctrs::vec_ptype2(gs4_formula(), character()), 24 | character() 25 | ) 26 | }) 27 | 28 | test_that("googlesheets4_formula and character are coercible", { 29 | expect_identical( 30 | vctrs::vec_cast("=sum(A:A)", gs4_formula()), 31 | gs4_formula("=sum(A:A)") 32 | ) 33 | expect_identical( 34 | vctrs::vec_cast(gs4_formula("=sum(A:A)"), character()), 35 | "=sum(A:A)" 36 | ) 37 | expect_identical( 38 | vctrs::vec_cast(gs4_formula("=sum(A:A)"), gs4_formula()), 39 | gs4_formula("=sum(A:A)") 40 | ) 41 | }) 42 | 43 | test_that("can concatenate googlesheets4_formula", { 44 | expect_identical( 45 | vctrs::vec_c( 46 | gs4_formula("=sum(A:A)"), 47 | gs4_formula("=sum(B:B)") 48 | ), 49 | gs4_formula(c("=sum(A:A)", "=sum(B:B)")) 50 | ) 51 | }) 52 | 53 | test_that("googlesheets4_formula can have missing elements", { 54 | out <- vctrs::vec_c( 55 | gs4_formula("=sum(A:A)"), 56 | NA, 57 | gs4_formula("=min(B2:G7"), 58 | NA 59 | ) 60 | expect_s3_class(out, "googlesheets4_formula") 61 | expect_true(all(is.na(out[c(2, 4)]))) 62 | }) 63 | -------------------------------------------------------------------------------- /R/sheet_rename.R: -------------------------------------------------------------------------------- 1 | #' Rename a (work)sheet 2 | #' 3 | #' Changes the name of a (work)sheet. 4 | #' 5 | #' @eval param_ss() 6 | #' @eval param_sheet( 7 | #' action = "rename", 8 | #' "Defaults to the first visible sheet." 9 | #' ) 10 | #' @param new_name New name of the sheet, as a string. This is required. 11 | #' 12 | #' @template ss-return 13 | #' @export 14 | #' @family worksheet functions 15 | #' @seealso Makes an `UpdateSheetPropertiesRequest`: 16 | #' * 17 | #' 18 | #' @examplesIf gs4_has_token() 19 | #' ss <- gs4_create( 20 | #' "sheet-rename-demo", 21 | #' sheets = list(cars = head(cars), chickwts = head(chickwts)) 22 | #' ) 23 | #' sheet_names(ss) 24 | #' 25 | #' ss %>% 26 | #' sheet_rename(1, new_name = "automobiles") %>% 27 | #' sheet_rename("chickwts", new_name = "poultry") 28 | #' 29 | #' # clean up 30 | #' gs4_find("sheet-rename-demo") %>% 31 | #' googledrive::drive_trash() 32 | sheet_rename <- function(ss, sheet = NULL, new_name) { 33 | ssid <- as_sheets_id(ss) 34 | maybe_sheet(sheet) 35 | check_string(new_name) 36 | 37 | x <- gs4_get(ssid) 38 | s <- lookup_sheet(sheet, sheets_df = x$sheets) 39 | gs4_bullets(c( 40 | v = "Renaming sheet {.w_sheet {s$name}} to {.w_sheet {new_name}}." 41 | )) 42 | 43 | sp <- new("SheetProperties", sheetId = s$id, title = new_name) 44 | update_req <- new( 45 | "UpdateSheetPropertiesRequest", 46 | properties = sp, 47 | fields = gargle::field_mask(sp) 48 | ) 49 | 50 | req <- request_generate( 51 | "sheets.spreadsheets.batchUpdate", 52 | params = list( 53 | spreadsheetId = ssid, 54 | requests = list(updateSheetProperties = update_req) 55 | ) 56 | ) 57 | resp_raw <- request_make(req) 58 | gargle::response_process(resp_raw) 59 | 60 | invisible(ssid) 61 | } 62 | -------------------------------------------------------------------------------- /.github/workflows/format-suggest.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/posit-dev/setup-air/tree/main/examples 2 | 3 | on: 4 | # Using `pull_request_target` over `pull_request` for elevated `GITHUB_TOKEN` 5 | # privileges, otherwise we can't set `pull-requests: write` when the pull 6 | # request comes from a fork, which is our main use case (external contributors). 7 | # 8 | # `pull_request_target` runs in the context of the target branch (`main`, usually), 9 | # rather than in the context of the pull request like `pull_request` does. Due 10 | # to this, we must explicitly checkout `ref: ${{ github.event.pull_request.head.sha }}`. 11 | # This is typically frowned upon by GitHub, as it exposes you to potentially running 12 | # untrusted code in a context where you have elevated privileges, but they explicitly 13 | # call out the use case of reformatting and committing back / commenting on the PR 14 | # as a situation that should be safe (because we aren't actually running the untrusted 15 | # code, we are just treating it as passive data). 16 | # https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/ 17 | pull_request_target: 18 | 19 | name: format-suggest.yaml 20 | 21 | jobs: 22 | format-suggest: 23 | name: format-suggest 24 | runs-on: ubuntu-latest 25 | 26 | permissions: 27 | # Required to push suggestion comments to the PR 28 | pull-requests: write 29 | 30 | steps: 31 | - uses: actions/checkout@v4 32 | with: 33 | ref: ${{ github.event.pull_request.head.sha }} 34 | 35 | - name: Install 36 | uses: posit-dev/setup-air@v1 37 | 38 | - name: Format 39 | run: air format . 40 | 41 | - name: Suggest 42 | uses: reviewdog/action-suggester@v1 43 | with: 44 | level: error 45 | fail_level: error 46 | tool_name: air 47 | -------------------------------------------------------------------------------- /data-raw/schema-rectangling.R: -------------------------------------------------------------------------------- 1 | schema_rectangle <- function(s) { 2 | if (!"tidyverse" %in% .packages()) { 3 | stop("Attach the tidyverse package before using schema_rectangle()") 4 | } 5 | schema <- pluck(.schemas, s) 6 | if (schema$type != "object") { 7 | msg <- glue::glue( 8 | "Schema must be of type {sq('object')}, not {sq(schema$type)}" 9 | ) 10 | stop(msg) 11 | } 12 | 13 | properties <- pluck(schema, "properties") 14 | scaffold <- list( 15 | description = "Just a placeholder", 16 | type = "scaffold", 17 | "$ref" = "SCHEMA", 18 | items = list("$ref" = "SCHEMA"), 19 | format = "FORMAT", 20 | enum = letters[1:3], 21 | enumDescriptions = LETTERS[1:3] 22 | ) 23 | df <- tibble(properties = c(scaffold = list(scaffold), properties)) 24 | 25 | df <- df %>% 26 | mutate(property = names(properties)) %>% 27 | select(property, everything()) %>% 28 | unnest_wider(properties) %>% 29 | select(-description) %>% 30 | mutate(type = replace_na(type, "object")) %>% 31 | rename(instance_of = "$ref") 32 | 33 | # workaround for https://github.com/tidyverse/tidyr/issues/806 34 | repair <- function(x) { 35 | map_if(x, ~ inherits(.x, "vctrs_unspecified"), ~ vctrs::unspecified(0)) 36 | } 37 | df <- modify_if(df, is_list, repair) 38 | 39 | df <- df %>% 40 | hoist(items, array_of = "$ref") 41 | 42 | make_enum_tibble <- function(x, y) { 43 | tibble( 44 | enum = x %||% character(), 45 | enumDesc = y %||% character() 46 | ) 47 | } 48 | 49 | df <- df %>% 50 | mutate(new = map2(enum, enumDescriptions, make_enum_tibble)) %>% 51 | select(-starts_with("enum")) %>% 52 | rename(enum = new) %>% 53 | mutate(type = if_else(map_lgl(enum, ~ nrow(.x) > 0), "enum", type)) 54 | 55 | attr(df, "id") <- s 56 | 57 | df %>% 58 | filter(property != "scaffold") %>% 59 | arrange(property) 60 | } 61 | -------------------------------------------------------------------------------- /man/gs4_scopes.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/gs4_auth.R 3 | \name{gs4_scopes} 4 | \alias{gs4_scopes} 5 | \title{Produce scopes specific to the Sheets API} 6 | \usage{ 7 | gs4_scopes(scopes = NULL) 8 | } 9 | \arguments{ 10 | \item{scopes}{One or more API scopes. Each scope can be specified in full or, 11 | for Sheets API-specific scopes, in an abbreviated form that is recognized by 12 | \code{\link[=gs4_scopes]{gs4_scopes()}}: 13 | \itemize{ 14 | \item "spreadsheets" = "https://www.googleapis.com/auth/spreadsheets" 15 | (the default) 16 | \item "spreadsheets.readonly" = 17 | "https://www.googleapis.com/auth/spreadsheets.readonly" 18 | \item "drive" = "https://www.googleapis.com/auth/drive" 19 | \item "drive.readonly" = "https://www.googleapis.com/auth/drive.readonly" 20 | \item "drive.file" = "https://www.googleapis.com/auth/drive.file" 21 | } 22 | 23 | See 24 | \url{https://developers.google.com/identity/protocols/oauth2/scopes#sheets} for 25 | details on the permissions for each scope.} 26 | } 27 | \value{ 28 | A character vector of scopes. 29 | } 30 | \description{ 31 | When called with no arguments, \code{gs4_scopes()} returns a named character 32 | vector of scopes associated with the Sheets API. If \code{gs4_scopes(scopes =)} is 33 | given, an abbreviated entry such as \code{"sheets.readonly"} is expanded to a full 34 | scope (\code{"https://www.googleapis.com/auth/sheets.readonly"} in this case). 35 | Unrecognized scopes are passed through unchanged. 36 | } 37 | \examples{ 38 | gs4_scopes("spreadsheets") 39 | gs4_scopes("spreadsheets.readonly") 40 | gs4_scopes("drive") 41 | gs4_scopes() 42 | } 43 | \seealso{ 44 | \url{https://developers.google.com/identity/protocols/oauth2/scopes#sheets} for 45 | details on the permissions for each scope. 46 | 47 | Other auth functions: 48 | \code{\link{gs4_auth}()}, 49 | \code{\link{gs4_auth_configure}()}, 50 | \code{\link{gs4_deauth}()} 51 | } 52 | \concept{auth functions} 53 | -------------------------------------------------------------------------------- /.github/workflows/pkgdown.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | schedule: 7 | # * is a special character in YAML so we have to quote this string 8 | # 3am Pacific = 11am UTC 9 | # https://crontab.guru is your friend 10 | - cron: '0 11 * * *' 11 | release: 12 | types: [published] 13 | workflow_dispatch: 14 | 15 | name: pkgdown 16 | 17 | permissions: read-all 18 | 19 | jobs: 20 | pkgdown: 21 | runs-on: ubuntu-latest 22 | if: | 23 | github.event_name == 'schedule' || 24 | github.event_name == 'workflow_dispatch' || 25 | github.event_name == 'release' || 26 | contains(github.event.head_commit.message, '[pkgdown]') 27 | # Only restrict concurrency for non-PR jobs 28 | concurrency: 29 | group: pkgdown-${{ github.event_name != 'pull_request' || github.run_id }} 30 | env: 31 | GOOGLESHEETS4_KEY: ${{ secrets.GOOGLESHEETS4_KEY }} 32 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 33 | permissions: 34 | contents: write 35 | steps: 36 | - uses: actions/checkout@v4 37 | 38 | - uses: r-lib/actions/setup-pandoc@v2 39 | 40 | - uses: r-lib/actions/setup-r@v2 41 | with: 42 | use-public-rspm: true 43 | 44 | - uses: r-lib/actions/setup-r-dependencies@v2 45 | with: 46 | extra-packages: any::pkgdown, local::. 47 | needs: website 48 | 49 | - name: Build site 50 | run: pkgdown::build_site_github_pages(new_process = FALSE, install = FALSE) 51 | shell: Rscript {0} 52 | 53 | - name: Deploy to GitHub pages 🚀 54 | if: github.event_name != 'pull_request' 55 | uses: JamesIves/github-pages-deploy-action@v4.5.0 56 | with: 57 | clean: false 58 | branch: gh-pages 59 | folder: docs 60 | -------------------------------------------------------------------------------- /tests/testthat/test-gs4_example.R: -------------------------------------------------------------------------------- 1 | test_that("gs4_examples() lists all examples, in a named drive_id object", { 2 | skip_if_offline() 3 | skip_on_cran() 4 | 5 | examples <- gs4_examples() 6 | expect_true(is.character(examples)) 7 | expect_true(length(examples) > 0) 8 | expect_true(is.character(names(examples))) 9 | expect_s3_class(examples, "drive_id") 10 | }) 11 | 12 | test_that("gs4_example() returns a sheets_id", { 13 | skip_if_offline() 14 | skip_on_cran() 15 | 16 | expect_s3_class(gs4_example("deaths"), "sheets_id") 17 | }) 18 | 19 | test_that("gs4_examples() requires a match if `matches` is supplied", { 20 | skip_if_offline() 21 | skip_on_cran() 22 | 23 | expect_error(gs4_examples("nope"), "Can't find") 24 | }) 25 | 26 | test_that("gs4_example() requires `matches`", { 27 | skip_if_offline() 28 | skip_on_cran() 29 | 30 | expect_error(gs4_example(), "missing") 31 | }) 32 | 33 | test_that("`matches` works in gs4_examples()", { 34 | skip_if_offline() 35 | skip_on_cran() 36 | 37 | examples <- gs4_examples("gap") 38 | expect_true(length(examples) > 0) 39 | expect_s3_class(examples, "drive_id") 40 | }) 41 | 42 | test_that("`matches` works in gs4_example()", { 43 | skip_if_offline() 44 | skip_on_cran() 45 | 46 | expect_no_error( 47 | example <- gs4_example("gapminder") 48 | ) 49 | expect_length(example, 1) 50 | expect_s3_class(example, "drive_id") 51 | expect_s3_class(example, "sheets_id") 52 | }) 53 | 54 | test_that("gs4_example() requires a unique match", { 55 | skip_if_offline() 56 | skip_on_cran() 57 | 58 | expect_error(gs4_example("gap"), "multiple") 59 | }) 60 | 61 | test_that("example functions work when deauth'd", { 62 | skip_if_offline() 63 | skip_on_cran() 64 | 65 | examples <- gs4_examples() 66 | gapminder <- gs4_example("gapminder") 67 | 68 | local_deauth() 69 | env_bind(.googlesheets4, example_and_test_sheets = zap()) 70 | 71 | expect_equal(gs4_examples(), examples) 72 | expect_equal(gs4_example("gapminder"), gapminder) 73 | }) 74 | -------------------------------------------------------------------------------- /tests/testthat/test-utils.R: -------------------------------------------------------------------------------- 1 | test_that("check_length_one() works", { 2 | expect_no_error(check_length_one(1)) 3 | expect_error(check_length_one(1:2), "must have length 1") 4 | expect_error(check_length_one(letters), "letters") 5 | }) 6 | 7 | test_that("check_character() works", { 8 | expect_no_error(check_character(letters)) 9 | expect_error(check_character(1:2), "integer") 10 | }) 11 | 12 | 13 | test_that("vlookup() works", { 14 | df <- tibble::tibble( 15 | i = 1:3, 16 | letters = letters[i], 17 | dupes = c("a", "c", "c"), 18 | fctr = factor(letters) 19 | ) 20 | 21 | ## internal function, therefore it does not support unquoted variable names 22 | ## R <= 3.4.4 error msg is "object 'i' not found" 23 | ## R devel error msg is "is_string(key) is not TRUE" 24 | expect_error(vlookup("c", df, letters, i)) 25 | 26 | expect_identical(vlookup("c", df, "letters", "i"), 3L) 27 | expect_identical(vlookup(c("a", "c"), df, "letters", "i"), c(1L, 3L)) 28 | 29 | ## match() returns position of *first* match 30 | expect_identical(vlookup("c", df, "dupes", "i"), 2L) 31 | expect_identical(vlookup(c("c", "c"), df, "dupes", "i"), c(2L, 2L)) 32 | 33 | expect_identical(vlookup("b", df, "fctr", "i"), 2L) 34 | expect_identical(vlookup(c("b", "c", "a"), df, "fctr", "i"), c(2L, 3L, 1L)) 35 | }) 36 | 37 | test_that("enforce_na() works", { 38 | expect_error(enforce_na(1), "is.character(x) is not TRUE", fixed = TRUE) 39 | expect_error(enforce_na("a", 1), "is.character(na) is not TRUE", fixed = TRUE) 40 | 41 | expect_identical(enforce_na(character()), character()) 42 | 43 | expect_identical( 44 | enforce_na(c("a", "", "c")), 45 | c("a", NA, "c") 46 | ) 47 | expect_identical( 48 | enforce_na(c("a", "", "c"), na = "c"), 49 | c("a", "", NA) 50 | ) 51 | expect_identical( 52 | enforce_na(c("abc", "", "cab"), na = c("abc", "")), 53 | c(NA, NA, "cab") 54 | ) 55 | expect_identical( 56 | enforce_na(c("a", "", "c"), na = character()), 57 | c("a", "", "c") 58 | ) 59 | }) 60 | -------------------------------------------------------------------------------- /man/cell-specification.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/cell-specification.R 3 | \name{cell-specification} 4 | \alias{cell-specification} 5 | \alias{cell_limits} 6 | \alias{cell_rows} 7 | \alias{cell_cols} 8 | \alias{anchored} 9 | \title{Specify cells} 10 | \description{ 11 | Many functions in googlesheets4 use a \code{range} argument to target specific 12 | cells. The Sheets v4 API expects user-specified ranges to be expressed via 13 | \href{https://developers.google.com/sheets/api/guides/concepts#a1_notation}{its A1 notation}, 14 | but googlesheets4 accepts and converts a few alternative specifications 15 | provided by the functions in the \link[cellranger:cellranger]{cellranger::cellranger} package. Of course, 16 | you can always provide A1-style ranges directly to functions like 17 | \code{\link[=read_sheet]{read_sheet()}} or \code{\link[=range_read_cells]{range_read_cells()}}. Why would you use the 18 | \link[cellranger:cellranger]{cellranger::cellranger} helpers? Some ranges are practically impossible to 19 | express in A1 notation, specifically when you want to describe rectangles 20 | with some bounds that are specified and others determined by the data. 21 | } 22 | \examples{ 23 | \dontshow{if (gs4_has_token() && rlang::is_interactive()) withAutoprint(\{ # examplesIf} 24 | ss <- gs4_example("mini-gap") 25 | 26 | # Specify only the rows or only the columns 27 | read_sheet(ss, range = cell_rows(1:3)) 28 | read_sheet(ss, range = cell_cols("C:D")) 29 | read_sheet(ss, range = cell_cols(1)) 30 | 31 | # Specify upper or lower bound on row or column 32 | read_sheet(ss, range = cell_rows(c(NA, 4))) 33 | read_sheet(ss, range = cell_cols(c(NA, "D"))) 34 | read_sheet(ss, range = cell_rows(c(3, NA))) 35 | read_sheet(ss, range = cell_cols(c(2, NA))) 36 | read_sheet(ss, range = cell_cols(c("C", NA))) 37 | 38 | # Specify a partially open rectangle 39 | read_sheet(ss, range = cell_limits(c(2, 3), c(NA, NA)), col_names = FALSE) 40 | read_sheet(ss, range = cell_limits(c(1, 2), c(NA, 4))) 41 | \dontshow{\}) # examplesIf} 42 | } 43 | -------------------------------------------------------------------------------- /.github/workflows/R-CMD-check.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | # 4 | # NOTE: This workflow is overkill for most R packages and 5 | # check-standard.yaml is likely a better choice. 6 | # usethis::use_github_action("check-standard") will install it. 7 | on: 8 | push: 9 | branches: [main, master] 10 | pull_request: 11 | branches: [main, master] 12 | 13 | name: R-CMD-check 14 | 15 | permissions: read-all 16 | 17 | jobs: 18 | R-CMD-check: 19 | runs-on: ${{ matrix.config.os }} 20 | 21 | name: ${{ matrix.config.os }} (${{ matrix.config.r }}) 22 | 23 | strategy: 24 | fail-fast: false 25 | matrix: 26 | config: 27 | - {os: macos-latest, r: 'release'} 28 | 29 | - {os: windows-latest, r: 'release'} 30 | # use 4.1 to check with rtools40's older compiler 31 | - {os: windows-latest, r: '4.1'} 32 | 33 | - {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'} 34 | - {os: ubuntu-latest, r: 'release'} 35 | - {os: ubuntu-latest, r: 'oldrel-1'} 36 | - {os: ubuntu-latest, r: 'oldrel-2'} 37 | - {os: ubuntu-latest, r: 'oldrel-3'} 38 | - {os: ubuntu-latest, r: 'oldrel-4'} 39 | 40 | env: 41 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 42 | R_KEEP_PKG_SOURCE: yes 43 | 44 | steps: 45 | - uses: actions/checkout@v4 46 | 47 | - uses: r-lib/actions/setup-pandoc@v2 48 | 49 | - uses: r-lib/actions/setup-r@v2 50 | with: 51 | r-version: ${{ matrix.config.r }} 52 | http-user-agent: ${{ matrix.config.http-user-agent }} 53 | use-public-rspm: true 54 | 55 | - uses: r-lib/actions/setup-r-dependencies@v2 56 | with: 57 | extra-packages: any::rcmdcheck 58 | needs: check 59 | 60 | - uses: r-lib/actions/check-r-package@v2 61 | with: 62 | upload-snapshots: true 63 | build_args: 'c("--no-manual","--compact-vignettes=gs+qpdf")' 64 | -------------------------------------------------------------------------------- /R/schemas.R: -------------------------------------------------------------------------------- 1 | new <- function(id, ...) { 2 | schema <- .tidy_schemas[[id]] 3 | if (is.null(schema)) { 4 | gs4_abort("Can't find a tidy schema with id {.field {id}}.") 5 | } 6 | dots <- list2(...) 7 | dots <- discard(dots, is.null) 8 | 9 | check_against_schema(dots, schema = schema) 10 | 11 | structure( 12 | dots, 13 | # explicit 'list' class is a bit icky but makes jsonlite happy 14 | # in various vctrs futures, this could need revisiting 15 | class = c(id_as_class(id), "googlesheets4_schema", "list"), 16 | schema = schema 17 | ) 18 | } 19 | 20 | # TODO: if it proves necessary, this could do more meaningful checks 21 | check_against_schema <- function(x, schema = NULL, id = NA_character_) { 22 | schema <- schema %||% 23 | .tidy_schemas[[id %|% id_from_class(x)]] %||% 24 | attr(x, "schema") 25 | if (is.null(schema)) { 26 | gs4_abort( 27 | " 28 | Trying to check an object of class {.cls {class(x)}}, \\ 29 | but can't get a schema." 30 | ) 31 | } 32 | stopifnot(is_dictionaryish(x)) 33 | unexpected <- setdiff(names(x), schema$property) 34 | if (length(unexpected) > 0) { 35 | gs4_abort(c( 36 | "Properties not recognized for the {.field {attr(schema, 'id')}} schema:", 37 | bulletize(gargle_map_cli(unexpected), bullet = "x") 38 | )) 39 | } 40 | x 41 | } 42 | 43 | id_as_class <- function(id) glue("googlesheets4_schema_{id}") 44 | 45 | id_from_class <- function(x) { 46 | m <- grepl("^googlesheets4_schema_", class(x)) 47 | if (!any(m)) { 48 | return(NA_character_) 49 | } 50 | m <- which(m)[1] 51 | sub("^googlesheets4_schema_", "", class(x)[m]) 52 | } 53 | 54 | # patch ---- 55 | patch <- function(x, ...) { 56 | UseMethod("patch") 57 | } 58 | 59 | #' @export 60 | patch.default <- function(x, ...) { 61 | gs4_abort( 62 | " 63 | Don't know how to {.fun patch} an object of class {.cls {class(x)}}." 64 | ) 65 | } 66 | 67 | #' @export 68 | patch.googlesheets4_schema <- function(x, ...) { 69 | dots <- list2(...) 70 | dots <- discard(dots, is.null) 71 | x[names(dots)] <- dots 72 | check_against_schema(x) 73 | } 74 | -------------------------------------------------------------------------------- /R/schema_GridRange.R: -------------------------------------------------------------------------------- 1 | # https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/other#GridRange 2 | # 3 | # All indexes are zero-based. Indexes are half open, e.g the start index is 4 | # inclusive and the end index is exclusive -- [startIndex, endIndex). Missing 5 | # indexes indicate the range is unbounded on that side. 6 | 7 | #' @export 8 | as_tibble.googlesheets4_schema_GridRange <- function(x, ...) { 9 | tibble::tibble( 10 | # if there is only 1 sheet, sheetId might not be sent! 11 | # https://github.com/tidyverse/googlesheets4/issues/29 12 | # don't be shocked if this is NA 13 | sheet_id = glean_int(x, "sheetId"), 14 | # API sends zero-based row and column 15 | # => we add one 16 | # API indices are half-open, i.e. [start, end) 17 | # => we substract one from end_[row|column] 18 | # net effect 19 | # => we add one to start_[row|column] but not to end_[row|column] 20 | start_row = glean_int(x, "startRowIndex") + 1L, 21 | end_row = glean_int(x, "endRowIndex"), 22 | start_column = glean_int(x, "startColumnIndex") + 1L, 23 | end_column = glean_int(x, "endColumnIndex") 24 | ) 25 | } 26 | 27 | as_GridRange <- function(x, ...) { 28 | UseMethod("as_GridRange") 29 | } 30 | 31 | #' @export 32 | as_GridRange.default <- function(x, ...) { 33 | abort_unsupported_conversion(x, to = "GridRange") 34 | } 35 | 36 | #' @export 37 | as_GridRange.range_spec <- function(x, ...) { 38 | if (!is.null(x$named_range)) { 39 | gs4_abort("This function does not accept a named range as {.arg range}.") 40 | } 41 | s <- lookup_sheet(x$sheet_name, sheets_df = x$sheets_df) 42 | out <- new("GridRange", sheetId = s$id) 43 | 44 | if (is.null(x$cell_limits)) { 45 | if (is.null(x$cell_range)) { 46 | return(out) 47 | } 48 | x$cell_limits <- limits_from_range(x$cell_range) 49 | } 50 | 51 | cl <- list( 52 | startRowIndex = x$cell_limits$ul[1] - 1, 53 | endRowIndex = x$cell_limits$lr[1], 54 | startColumnIndex = x$cell_limits$ul[2] - 1, 55 | endColumnIndex = x$cell_limits$lr[2] 56 | ) 57 | cl <- discard(cl, is.na) 58 | patch(out, !!!cl) 59 | } 60 | -------------------------------------------------------------------------------- /man/gs4_formula.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/gs4_formula.R 3 | \name{gs4_formula} 4 | \alias{gs4_formula} 5 | \title{Class for Google Sheets formulas} 6 | \usage{ 7 | gs4_formula(x = character()) 8 | } 9 | \arguments{ 10 | \item{x}{Character.} 11 | } 12 | \value{ 13 | An S3 vector of class \code{googlesheets4_formula}. 14 | } 15 | \description{ 16 | In order to write a formula into Google Sheets, you need to store it as an 17 | object of class \code{googlesheets4_formula}. This is how we distinguish a 18 | "regular" character string from a string that should be interpreted as a 19 | formula. \code{googlesheets4_formula} is an S3 class implemented using the \href{https://vctrs.r-lib.org/articles/s3-vector.html}{vctrs package}. 20 | } 21 | \examples{ 22 | \dontshow{if (gs4_has_token()) withAutoprint(\{ # examplesIf} 23 | dat <- data.frame(x = c(1, 5, 3, 2, 4, 6)) 24 | 25 | ss <- gs4_create("gs4-formula-demo", sheets = dat) 26 | ss 27 | 28 | summaries <- tibble::tribble( 29 | ~desc, ~summaries, 30 | "max", "=max(A:A)", 31 | "sum", "=sum(A:A)", 32 | "min", "=min(A:A)", 33 | "sparkline", "=SPARKLINE(A:A, {\"color\", \"blue\"})" 34 | ) 35 | 36 | # explicitly declare a column as `googlesheets4_formula` 37 | summaries$summaries <- gs4_formula(summaries$summaries) 38 | summaries 39 | 40 | range_write(ss, data = summaries, range = "C1", reformat = FALSE) 41 | 42 | miscellany <- tibble::tribble( 43 | ~desc, ~example, 44 | "hyperlink", "=HYPERLINK(\"http://www.google.com/\",\"Google\")", 45 | "image", "=IMAGE(\"https://www.google.com/images/srpr/logo3w.png\")" 46 | ) 47 | miscellany$example <- gs4_formula(miscellany$example) 48 | miscellany 49 | 50 | sheet_write(miscellany, ss = ss) 51 | 52 | # clean up 53 | gs4_find("gs4-formula-demo") \%>\% 54 | googledrive::drive_trash() 55 | \dontshow{\}) # examplesIf} 56 | } 57 | \seealso{ 58 | Other write functions: 59 | \code{\link{gs4_create}()}, 60 | \code{\link{range_delete}()}, 61 | \code{\link{range_flood}()}, 62 | \code{\link{range_write}()}, 63 | \code{\link{sheet_append}()}, 64 | \code{\link{sheet_write}()} 65 | } 66 | \concept{write functions} 67 | -------------------------------------------------------------------------------- /R/sheet_delete.R: -------------------------------------------------------------------------------- 1 | #' Delete one or more (work)sheets 2 | #' 3 | #' Deletes one or more (work)sheets from a (spread)Sheet. 4 | #' 5 | #' @eval param_ss() 6 | #' @eval param_sheet( 7 | #' action = "delete", 8 | #' "You can pass a vector to delete multiple sheets at once or even a list,", 9 | #' "if you need to mix names and positions." 10 | #' ) 11 | #' 12 | #' @return The input `ss`, as an instance of [`sheets_id`] 13 | #' @export 14 | #' @family worksheet functions 15 | #' @seealso Makes an `DeleteSheetsRequest`: 16 | #' * 17 | #' 18 | #' @examplesIf gs4_has_token() 19 | #' ss <- gs4_create("delete-sheets-from-me") 20 | #' sheet_add(ss, c("alpha", "beta", "gamma", "delta")) 21 | #' 22 | #' # get an overview of the sheets 23 | #' sheet_properties(ss) 24 | #' 25 | #' # delete sheets 26 | #' sheet_delete(ss, 1) 27 | #' sheet_delete(ss, "gamma") 28 | #' sheet_delete(ss, list("alpha", 2)) 29 | #' 30 | #' # get an overview of the sheets 31 | #' sheet_properties(ss) 32 | #' 33 | #' # clean up 34 | #' gs4_find("delete-sheets-from-me") %>% 35 | #' googledrive::drive_trash() 36 | sheet_delete <- function(ss, sheet) { 37 | ssid <- as_sheets_id(ss) 38 | walk(sheet, ~ check_sheet(.x, arg = "sheet")) 39 | 40 | # retrieve spreadsheet metadata ---------------------------------------------- 41 | x <- gs4_get(ssid) 42 | 43 | # capture sheet ids ---------------------------------------------------------- 44 | s <- map( 45 | sheet, 46 | ~ lookup_sheet(.x, sheets_df = x$sheets, call = quote(sheet_delete())) 47 | ) 48 | sheet_names <- map_chr(s, "name") 49 | n <- length(sheet_names) 50 | gs4_bullets(c( 51 | v = "Deleting {n} sheet{?s} from {.s_sheet {x$name}}:", 52 | bulletize(gargle_map_cli(sheet_names, template = "{.field <>}")) 53 | )) 54 | 55 | sid <- map(s, "id") 56 | requests <- map(sid, ~ list(deleteSheet = list(sheetId = .x))) 57 | 58 | req <- request_generate( 59 | "sheets.spreadsheets.batchUpdate", 60 | params = list( 61 | spreadsheetId = ssid, 62 | requests = requests 63 | ) 64 | ) 65 | resp_raw <- request_make(req) 66 | gargle::response_process(resp_raw) 67 | invisible(ssid) 68 | } 69 | -------------------------------------------------------------------------------- /man/sheet_rename.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/sheet_rename.R 3 | \name{sheet_rename} 4 | \alias{sheet_rename} 5 | \title{Rename a (work)sheet} 6 | \usage{ 7 | sheet_rename(ss, sheet = NULL, new_name) 8 | } 9 | \arguments{ 10 | \item{ss}{Something that identifies a Google Sheet: 11 | \itemize{ 12 | \item its file id as a string or \code{\link[googledrive:drive_id]{drive_id}} 13 | \item a URL from which we can recover the id 14 | \item a one-row \code{\link[googledrive:dribble]{dribble}}, which is how googledrive 15 | represents Drive files 16 | \item an instance of \code{googlesheets4_spreadsheet}, which is what \code{\link[=gs4_get]{gs4_get()}} 17 | returns 18 | } 19 | 20 | Processed through \code{\link[=as_sheets_id]{as_sheets_id()}}.} 21 | 22 | \item{sheet}{Sheet to rename, in the sense of "worksheet" or "tab". You can identify a sheet by name, with a string, or by position, with a number. Defaults to the first visible sheet.} 23 | 24 | \item{new_name}{New name of the sheet, as a string. This is required.} 25 | } 26 | \value{ 27 | The input \code{ss}, as an instance of \code{\link{sheets_id}} 28 | } 29 | \description{ 30 | Changes the name of a (work)sheet. 31 | } 32 | \examples{ 33 | \dontshow{if (gs4_has_token()) withAutoprint(\{ # examplesIf} 34 | ss <- gs4_create( 35 | "sheet-rename-demo", 36 | sheets = list(cars = head(cars), chickwts = head(chickwts)) 37 | ) 38 | sheet_names(ss) 39 | 40 | ss \%>\% 41 | sheet_rename(1, new_name = "automobiles") \%>\% 42 | sheet_rename("chickwts", new_name = "poultry") 43 | 44 | # clean up 45 | gs4_find("sheet-rename-demo") \%>\% 46 | googledrive::drive_trash() 47 | \dontshow{\}) # examplesIf} 48 | } 49 | \seealso{ 50 | Makes an \code{UpdateSheetPropertiesRequest}: 51 | \itemize{ 52 | \item \url{https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#UpdateSheetPropertiesRequest} 53 | } 54 | 55 | Other worksheet functions: 56 | \code{\link{sheet_add}()}, 57 | \code{\link{sheet_append}()}, 58 | \code{\link{sheet_copy}()}, 59 | \code{\link{sheet_delete}()}, 60 | \code{\link{sheet_properties}()}, 61 | \code{\link{sheet_relocate}()}, 62 | \code{\link{sheet_resize}()}, 63 | \code{\link{sheet_write}()} 64 | } 65 | \concept{worksheet functions} 66 | -------------------------------------------------------------------------------- /R/cell-specification.R: -------------------------------------------------------------------------------- 1 | ## this file represents the interface with the cellranger package 2 | 3 | #' Specify cells 4 | #' 5 | #' Many functions in googlesheets4 use a `range` argument to target specific 6 | #' cells. The Sheets v4 API expects user-specified ranges to be expressed via 7 | #' [its A1 8 | #' notation](https://developers.google.com/sheets/api/guides/concepts#a1_notation), 9 | #' but googlesheets4 accepts and converts a few alternative specifications 10 | #' provided by the functions in the [cellranger][cellranger] package. Of course, 11 | #' you can always provide A1-style ranges directly to functions like 12 | #' [read_sheet()] or [range_read_cells()]. Why would you use the 13 | #' [cellranger][cellranger] helpers? Some ranges are practically impossible to 14 | #' express in A1 notation, specifically when you want to describe rectangles 15 | #' with some bounds that are specified and others determined by the data. 16 | #' 17 | #' @name cell-specification 18 | #' 19 | #' @examplesIf gs4_has_token() && rlang::is_interactive() 20 | #' ss <- gs4_example("mini-gap") 21 | #' 22 | #' # Specify only the rows or only the columns 23 | #' read_sheet(ss, range = cell_rows(1:3)) 24 | #' read_sheet(ss, range = cell_cols("C:D")) 25 | #' read_sheet(ss, range = cell_cols(1)) 26 | #' 27 | #' # Specify upper or lower bound on row or column 28 | #' read_sheet(ss, range = cell_rows(c(NA, 4))) 29 | #' read_sheet(ss, range = cell_cols(c(NA, "D"))) 30 | #' read_sheet(ss, range = cell_rows(c(3, NA))) 31 | #' read_sheet(ss, range = cell_cols(c(2, NA))) 32 | #' read_sheet(ss, range = cell_cols(c("C", NA))) 33 | #' 34 | #' # Specify a partially open rectangle 35 | #' read_sheet(ss, range = cell_limits(c(2, 3), c(NA, NA)), col_names = FALSE) 36 | #' read_sheet(ss, range = cell_limits(c(1, 2), c(NA, 4))) 37 | NULL 38 | 39 | #' @importFrom cellranger cell_limits 40 | #' @name cell_limits 41 | #' @export 42 | #' @rdname cell-specification 43 | NULL 44 | 45 | #' @importFrom cellranger cell_rows 46 | #' @name cell_rows 47 | #' @export 48 | #' @rdname cell-specification 49 | NULL 50 | 51 | #' @importFrom cellranger cell_cols 52 | #' @name cell_cols 53 | #' @export 54 | #' @rdname cell-specification 55 | NULL 56 | 57 | #' @importFrom cellranger anchored 58 | #' @name anchored 59 | #' @export 60 | #' @rdname cell-specification 61 | NULL 62 | -------------------------------------------------------------------------------- /man/sheet_delete.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/sheet_delete.R 3 | \name{sheet_delete} 4 | \alias{sheet_delete} 5 | \title{Delete one or more (work)sheets} 6 | \usage{ 7 | sheet_delete(ss, sheet) 8 | } 9 | \arguments{ 10 | \item{ss}{Something that identifies a Google Sheet: 11 | \itemize{ 12 | \item its file id as a string or \code{\link[googledrive:drive_id]{drive_id}} 13 | \item a URL from which we can recover the id 14 | \item a one-row \code{\link[googledrive:dribble]{dribble}}, which is how googledrive 15 | represents Drive files 16 | \item an instance of \code{googlesheets4_spreadsheet}, which is what \code{\link[=gs4_get]{gs4_get()}} 17 | returns 18 | } 19 | 20 | Processed through \code{\link[=as_sheets_id]{as_sheets_id()}}.} 21 | 22 | \item{sheet}{Sheet to delete, in the sense of "worksheet" or "tab". You can identify a sheet by name, with a string, or by position, with a number. You can pass a vector to delete multiple sheets at once or even a list, if you need to mix names and positions.} 23 | } 24 | \value{ 25 | The input \code{ss}, as an instance of \code{\link{sheets_id}} 26 | } 27 | \description{ 28 | Deletes one or more (work)sheets from a (spread)Sheet. 29 | } 30 | \examples{ 31 | \dontshow{if (gs4_has_token()) withAutoprint(\{ # examplesIf} 32 | ss <- gs4_create("delete-sheets-from-me") 33 | sheet_add(ss, c("alpha", "beta", "gamma", "delta")) 34 | 35 | # get an overview of the sheets 36 | sheet_properties(ss) 37 | 38 | # delete sheets 39 | sheet_delete(ss, 1) 40 | sheet_delete(ss, "gamma") 41 | sheet_delete(ss, list("alpha", 2)) 42 | 43 | # get an overview of the sheets 44 | sheet_properties(ss) 45 | 46 | # clean up 47 | gs4_find("delete-sheets-from-me") \%>\% 48 | googledrive::drive_trash() 49 | \dontshow{\}) # examplesIf} 50 | } 51 | \seealso{ 52 | Makes an \code{DeleteSheetsRequest}: 53 | \itemize{ 54 | \item \url{https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#DeleteSheetRequest} 55 | } 56 | 57 | Other worksheet functions: 58 | \code{\link{sheet_add}()}, 59 | \code{\link{sheet_append}()}, 60 | \code{\link{sheet_copy}()}, 61 | \code{\link{sheet_properties}()}, 62 | \code{\link{sheet_relocate}()}, 63 | \code{\link{sheet_rename}()}, 64 | \code{\link{sheet_resize}()}, 65 | \code{\link{sheet_write}()} 66 | } 67 | \concept{worksheet functions} 68 | -------------------------------------------------------------------------------- /R/sheet_freeze.R: -------------------------------------------------------------------------------- 1 | #' Freeze rows or columns in a (work)sheet 2 | #' 3 | #' @description 4 | #' *Note: not yet exported.* 5 | #' 6 | #' Sets the number of frozen rows or column for a (work)sheet. 7 | #' 8 | #' @eval param_ss() 9 | #' @eval param_sheet() 10 | #' @param nrow,ncol Desired number of frozen rows or columns, respectively. The 11 | #' default of `NULL` means to leave unchanged. 12 | #' 13 | #' @template ss-return 14 | #' @seealso Makes an `UpdateSheetPropertiesRequest`: 15 | #' * <# https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#UpdateSheetPropertiesRequest> 16 | #' 17 | #' @keywords internal 18 | #' @noRd 19 | #' 20 | #' @examplesIf gs4_has_token() 21 | #' # create a data frame to use as initial data 22 | #' # intentionally has lots of rows and columns 23 | #' dat <- gs4_fodder(25) 24 | #' 25 | #' # create Sheet 26 | #' ss <- gs4_create("sheet-freeze-example", sheets = list(dat)) 27 | #' 28 | #' # look at it in the browser 29 | #' gs4_browse(ss) 30 | #' 31 | #' # freeze first 2 columns 32 | #' sheet_freeze(ss, ncol = 2) 33 | #' 34 | #' # clean up 35 | #' gs4_find("sheet-freeze-example") %>% 36 | #' googledrive::drive_trash() 37 | sheet_freeze <- function(ss, sheet = NULL, nrow = NULL, ncol = NULL) { 38 | ssid <- as_sheets_id(ss) 39 | maybe_sheet(sheet) 40 | maybe_non_negative_integer(nrow) 41 | maybe_non_negative_integer(ncol) 42 | 43 | if (is.null(nrow) && is.null(ncol)) { 44 | gs4_bullets(c(i = "Nothing to be done.")) 45 | return(invisible(ssid)) 46 | } 47 | 48 | dims <- c( 49 | if (!is.null(nrow)) cli::pluralize("{nrow} row{?s}"), 50 | if (!is.null(ncol)) cli::pluralize("{ncol} column{?s}") 51 | ) 52 | dims <- glue_collapse(dims, sep = " and ") 53 | 54 | x <- gs4_get(ssid) 55 | s <- lookup_sheet(sheet, sheets_df = x$sheets) 56 | gs4_bullets(c( 57 | v = "Freezing {dims} on sheet {.w_sheet {s$name}} in {.s_sheet {x$name}}." 58 | )) 59 | 60 | freeze_req <- bureq_set_grid_properties( 61 | sheetId = s$id, 62 | frozenRowCount = nrow, 63 | frozenColumnCount = ncol 64 | ) 65 | 66 | req <- request_generate( 67 | "sheets.spreadsheets.batchUpdate", 68 | params = list( 69 | spreadsheetId = ssid, 70 | requests = freeze_req 71 | ) 72 | ) 73 | resp_raw <- request_make(req) 74 | gargle::response_process(resp_raw) 75 | 76 | invisible(ssid) 77 | } 78 | -------------------------------------------------------------------------------- /vignettes/articles/example-sheets.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Example Sheets" 3 | --- 4 | 5 | ```{r, include = FALSE} 6 | knitr::opts_chunk$set( 7 | collapse = TRUE, 8 | comment = "#>", 9 | error = TRUE 10 | ) 11 | ``` 12 | 13 | An index of the official example and test Sheets. They are world-readable, so we do `gs4_deauth()`. 14 | 15 | ```{r setup} 16 | library(googlesheets4) 17 | 18 | gs4_deauth() 19 | ``` 20 | 21 | ## Example Sheets 22 | 23 | ```{r include = FALSE} 24 | # the non-interactive wrapper prevents tons of browser tabs opening when I 25 | # have to tinker with this code interactively 26 | sheets <- gs4_examples() 27 | rlang::with_interactive( 28 | value = FALSE, 29 | example_urls <- purrr::map_chr(sheets, gs4_browse) 30 | ) 31 | links <- glue::glue("[{names(example_urls)}]({unname(example_urls)})") 32 | dat <- tibble::tibble( 33 | `name (these are links)` = links, 34 | id = unclass(sheets) 35 | ) 36 | ``` 37 | 38 | ```{r echo = FALSE} 39 | knitr::kable(dat) 40 | ``` 41 | 42 | ### How to get hold of the example Sheets yourself 43 | 44 | `gs4_examples()` returns a named vector of Sheet IDs. It is also an instance of `drive_id`, to make good things happen with googledrive. 45 | 46 | ```{r} 47 | gs4_examples() 48 | 49 | gs4_examples("gap") 50 | 51 | gs4_examples("and") 52 | ``` 53 | 54 | `gs4_example()` returns exactly one Sheet ID (or errors). It is an instance of `drive_id`. It is also an instance of `sheets_id`, which means printing will (try to) reveal current metadata about the Sheet. 55 | 56 | ```{r} 57 | gs4_example("chicken") 58 | 59 | gs4_example("gap") 60 | ``` 61 | 62 | Here's a handy snippet to open all the example Sheets in browser tabs. 63 | 64 | ```{r eval = FALSE} 65 | lapply(gs4_examples(), gs4_browse) 66 | 67 | # for tidyversers 68 | gs4_examples() %>% 69 | purrr::walk(gs4_browse) 70 | ``` 71 | 72 | ## Test Sheets 73 | 74 | These are more developer-facing, but it's convenient to do them here too. 75 | 76 | ```{r include = FALSE} 77 | sheets <- googlesheets4:::test_sheets() 78 | rlang::with_interactive( 79 | value = FALSE, 80 | test_urls <- purrr::map_chr(sheets, gs4_browse) 81 | ) 82 | links <- glue::glue("[{names(test_urls)}]({unname(test_urls)})") 83 | dat <- tibble::tibble( 84 | `name (these are links)` = links, 85 | id = unclass(sheets) 86 | ) 87 | ``` 88 | 89 | ```{r echo = FALSE} 90 | knitr::kable(dat) 91 | ``` 92 | -------------------------------------------------------------------------------- /.github/workflows/test-coverage.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | branches: [main, master] 8 | schedule: 9 | # * is a special character in YAML so we have to quote this string 10 | # 2am Pacific = 10am UTC 11 | # https://crontab.guru is your friend 12 | - cron: '0 10 * * *' 13 | 14 | name: test-coverage 15 | 16 | permissions: read-all 17 | 18 | jobs: 19 | test-coverage: 20 | runs-on: ubuntu-latest 21 | if: "github.event_name == 'schedule' || contains(github.event.head_commit.message, '[covr]')" 22 | 23 | env: 24 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 25 | GOOGLESHEETS4_KEY: ${{ secrets.GOOGLESHEETS4_KEY }} 26 | 27 | steps: 28 | - uses: actions/checkout@v4 29 | 30 | - uses: r-lib/actions/setup-r@v2 31 | with: 32 | use-public-rspm: true 33 | 34 | - uses: r-lib/actions/setup-r-dependencies@v2 35 | with: 36 | extra-packages: any::covr, any::xml2 37 | needs: coverage 38 | 39 | - name: Test coverage 40 | run: | 41 | cov <- covr::package_coverage( 42 | quiet = FALSE, 43 | clean = FALSE, 44 | install_path = file.path(normalizePath(Sys.getenv("RUNNER_TEMP"), winslash = "/"), "package") 45 | ) 46 | covr::to_cobertura(cov) 47 | shell: Rscript {0} 48 | 49 | - uses: codecov/codecov-action@v4 50 | with: 51 | fail_ci_if_error: ${{ github.event_name != 'pull_request' && true || false }} 52 | file: ./cobertura.xml 53 | plugin: noop 54 | disable_search: true 55 | token: ${{ secrets.CODECOV_TOKEN }} 56 | 57 | - name: Show testthat output 58 | if: always() 59 | run: | 60 | ## -------------------------------------------------------------------- 61 | find '${{ runner.temp }}/package' -name 'testthat.Rout*' -exec cat '{}' \; || true 62 | shell: bash 63 | 64 | - name: Upload test results 65 | if: failure() 66 | uses: actions/upload-artifact@v4 67 | with: 68 | name: coverage-test-failures 69 | path: ${{ runner.temp }}/package 70 | -------------------------------------------------------------------------------- /tests/testthat/test-get_cells.R: -------------------------------------------------------------------------------- 1 | # x = empty cell = not sent back by API 2 | # O = occupied cell = present in API payload 3 | # A B C D E 4 | # 1 x x x x x 5 | # 2 x O O O x 6 | # 3 x O O x x <-- yes, it is intentional that D3 is empty 7 | # 4 x x x x x 8 | 9 | cell_df <- tibble::tribble( 10 | ~ row, ~ col, ~ cell, 11 | 2, 2, "B2", 12 | 2, 3, "C2", 13 | 2, 4, "D2", 14 | 3, 2, "B3", 15 | 3, 3, "C3" 16 | ) 17 | # row and col should really be integer 18 | cell_df$row <- as.integer(cell_df$row) 19 | cell_df$col <- as.integer(cell_df$col) 20 | # cell has to be a list-column for the tibble::add_row() in insert_shims() 21 | # to work, with increased type-strictness coming to tibble v3.0 22 | # TODO: maybe use an even more realistic cell-type of object here, when the 23 | # helpers are better 24 | cell_df$cell <- as.list(cell_df$cell) 25 | 26 | limitize <- function(df) { 27 | cell_limits(c(min(df$row), min(df$col)), c(max(df$row), max(df$col))) 28 | } 29 | 30 | expect_shim <- function(rg) { 31 | expect_identical( 32 | limitize(insert_shims(cell_df, as_cell_limits(rg))), 33 | as_cell_limits(rg) 34 | ) 35 | } 36 | 37 | test_that("observed data occupies range rectangle --> no shim needed", { 38 | expect_identical( 39 | insert_shims(cell_df, cell_limits = as_cell_limits("B2:D3")), 40 | cell_df 41 | ) 42 | }) 43 | 44 | test_that("can shim a single side", { 45 | ## up 46 | expect_shim("B1:D3") 47 | ## down 48 | expect_shim("B2:D4") 49 | ## left 50 | expect_shim("A2:D3") 51 | ## right 52 | expect_shim("B2:E3") 53 | }) 54 | 55 | test_that("can shim two opposing sides", { 56 | ## row direction 57 | expect_shim("B1:D4") 58 | ## col direction 59 | expect_shim("A2:E3") 60 | }) 61 | 62 | test_that("can shim on two perpendicular sides", { 63 | ## up and left 64 | expect_shim("A1:D3") 65 | ## up and right 66 | expect_shim("B1:E3") 67 | # down and left 68 | expect_shim("A2:D4") 69 | # down and right 70 | expect_shim("B2:E4") 71 | }) 72 | 73 | test_that("can shim three sides", { 74 | ## all but bottom 75 | expect_shim("A1:E3") 76 | ## all but left 77 | expect_shim("B1:E4") 78 | ## all but top 79 | expect_shim("A2:E4") 80 | ## all but right 81 | expect_shim("A1:D4") 82 | }) 83 | 84 | test_that("can shim four sides", { 85 | expect_shim("A1:E4") 86 | }) 87 | -------------------------------------------------------------------------------- /tests/testthat/test-ctype.R: -------------------------------------------------------------------------------- 1 | test_that("ctype() errors for unanticipated inputs", { 2 | expect_error(ctype(NULL)) 3 | expect_error(ctype(data.frame(cell = "cell"))) 4 | }) 5 | 6 | test_that("ctype() works on a SHEET_CELL, when it should", { 7 | expect_identical( 8 | ctype(structure(1, class = c("wut", "SHEETS_CELL"))), 9 | NA_character_ 10 | ) 11 | expect_identical( 12 | ctype(structure(1, class = c("CELL_NUMERIC", "SHEETS_CELL"))), 13 | "CELL_NUMERIC" 14 | ) 15 | }) 16 | 17 | test_that("ctype() works on shortcodes, when it should", { 18 | expect_equal( 19 | unname(ctype(c("?", "-", "n", "z", "D"))), 20 | c("COL_GUESS", "COL_SKIP", "CELL_NUMERIC", NA, "CELL_DATE") 21 | ) 22 | }) 23 | 24 | test_that("ctype() works on lists, when it should", { 25 | list_of_cells <- list( 26 | structure(1, class = c("CELL_NUMERIC", "SHEETS_CELL")), 27 | "nope", 28 | NULL, 29 | structure(1, class = c("wut", "SHEETS_CELL")), 30 | structure(1, class = c("CELL_TEXT", "SHEETS_CELL")) 31 | ) 32 | expect_equal( 33 | ctype(list_of_cells), 34 | c("CELL_NUMERIC", NA, NA, NA, "CELL_TEXT") 35 | ) 36 | }) 37 | 38 | test_that("effective_cell_type() doesn't just pass ctype through", { 39 | ## neither the API nor JSON has a proper way to convey integer-ness 40 | expect_equal(unname(effective_cell_type("CELL_INTEGER")), "CELL_NUMERIC") 41 | ## conversion to date or time is lossy, so never guess that 42 | expect_equal(unname(effective_cell_type("CELL_DATE")), "CELL_DATETIME") 43 | expect_equal(unname(effective_cell_type("CELL_TIME")), "CELL_DATETIME") 44 | }) 45 | 46 | test_that("consensus_col_type() implements our type coercion DAG", { 47 | expect_identical( 48 | consensus_col_type(c("CELL_TEXT", "CELL_TEXT")), 49 | "CELL_TEXT" 50 | ) 51 | expect_identical( 52 | consensus_col_type(c("CELL_LOGICAL", "CELL_NUMERIC")), 53 | "CELL_NUMERIC" 54 | ) 55 | expect_identical( 56 | consensus_col_type(c("CELL_LOGICAL", "CELL_DATE")), 57 | "COL_LIST" 58 | ) 59 | expect_identical( 60 | consensus_col_type(c("CELL_DATE", "CELL_DATETIME")), 61 | "CELL_DATETIME" 62 | ) 63 | expect_identical( 64 | consensus_col_type(c("CELL_TEXT", "CELL_BLANK")), 65 | "CELL_TEXT" 66 | ) 67 | expect_identical(consensus_col_type("CELL_TEXT"), "CELL_TEXT") 68 | expect_identical(consensus_col_type("CELL_BLANK"), "CELL_LOGICAL") 69 | }) 70 | -------------------------------------------------------------------------------- /tests/testthat/test-schema_GridRange.R: -------------------------------------------------------------------------------- 1 | test_that("we can make a GridRange from a range_spec", { 2 | sheets_df <- tibble::tibble(name = "abc", id = 123) 3 | 4 | # test cases are taken from examples given for GridRange schema 5 | # https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/other#GridRange 6 | spec <- new_range_spec(sheet_name = "abc", sheets_df = sheets_df) 7 | out <- as_GridRange(spec) 8 | expect_equal(out$sheetId, 123) 9 | 10 | spec <- new_range_spec( 11 | sheet_name = "abc", 12 | cell_range = "A3:B4", 13 | sheets_df = sheets_df 14 | ) 15 | out <- as_GridRange(spec) 16 | expect_equal(out$sheetId, 123) 17 | expect_equal(out$startRowIndex, 2) 18 | expect_equal(out$endRowIndex, 4) 19 | expect_equal(out$startColumnIndex, 0) 20 | expect_equal(out$endColumnIndex, 2) 21 | 22 | spec <- new_range_spec( 23 | sheet_name = "abc", 24 | cell_range = "A5:B", 25 | sheets_df = sheets_df 26 | ) 27 | out <- as_GridRange(spec) 28 | expect_equal(out$sheetId, 123) 29 | expect_equal(out$startRowIndex, 4) 30 | expect_null(out$endRowIndex) 31 | expect_equal(out$startColumnIndex, 0) 32 | expect_equal(out$endColumnIndex, 2) 33 | 34 | spec <- new_range_spec( 35 | sheet_name = "abc", 36 | cell_range = "A:B", 37 | sheets_df = sheets_df 38 | ) 39 | out <- as_GridRange(spec) 40 | expect_equal(out$sheetId, 123) 41 | expect_null(out$startRowIndex) 42 | expect_null(out$endRowIndex) 43 | expect_equal(out$startColumnIndex, 0) 44 | expect_equal(out$endColumnIndex, 2) 45 | 46 | spec <- new_range_spec( 47 | sheet_name = "abc", 48 | cell_range = "A1:A1", 49 | sheets_df = sheets_df 50 | ) 51 | out <- as_GridRange(spec) 52 | expect_equal(out$sheetId, 123) 53 | expect_equal(out$startRowIndex, 0) 54 | expect_equal(out$endRowIndex, 1) 55 | expect_equal(out$startColumnIndex, 0) 56 | expect_equal(out$endColumnIndex, 1) 57 | 58 | spec1 <- new_range_spec( 59 | sheet_name = "abc", 60 | cell_range = "C3:C3", 61 | sheets_df = sheets_df 62 | ) 63 | spec2 <- new_range_spec( 64 | sheet_name = "abc", 65 | cell_range = "C3", 66 | sheets_df = sheets_df 67 | ) 68 | expect_equal(as_GridRange(spec1), as_GridRange(spec2)) 69 | }) 70 | 71 | test_that("we refuse to make a GridRange from a named_range", { 72 | spec <- new_range_spec(named_range = "thingy") 73 | expect_error(as_GridRange(spec), "does not accept a named range") 74 | }) 75 | -------------------------------------------------------------------------------- /man/gs4_create.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/gs4_create.R 3 | \name{gs4_create} 4 | \alias{gs4_create} 5 | \title{Create a new Sheet} 6 | \usage{ 7 | gs4_create(name = gs4_random(), ..., sheets = NULL) 8 | } 9 | \arguments{ 10 | \item{name}{The name of the new spreadsheet.} 11 | 12 | \item{...}{Optional spreadsheet properties that can be set through this API 13 | endpoint, such as locale and time zone.} 14 | 15 | \item{sheets}{Optional input for initializing (work)sheets. If unspecified, 16 | the Sheets API automatically creates an empty "Sheet1". You can provide a 17 | vector of sheet names, a data frame, or a (possibly named) list of data 18 | frames. See the examples.} 19 | } 20 | \value{ 21 | The input \code{ss}, as an instance of \code{\link{sheets_id}} 22 | } 23 | \description{ 24 | Creates an entirely new (spread)Sheet (or, in Excel-speak, workbook). 25 | Optionally, you can also provide names and/or data for the initial set of 26 | (work)sheets. Any initial data provided via \code{sheets} is styled as a table, 27 | as described in \code{\link[=sheet_write]{sheet_write()}}. 28 | } 29 | \examples{ 30 | \dontshow{if (gs4_has_token()) withAutoprint(\{ # examplesIf} 31 | gs4_create("gs4-create-demo-1") 32 | 33 | gs4_create("gs4-create-demo-2", locale = "en_CA") 34 | 35 | gs4_create( 36 | "gs4-create-demo-3", 37 | locale = "fr_FR", 38 | timeZone = "Europe/Paris" 39 | ) 40 | 41 | gs4_create( 42 | "gs4-create-demo-4", 43 | sheets = c("alpha", "beta") 44 | ) 45 | 46 | my_data <- data.frame(x = 1) 47 | gs4_create( 48 | "gs4-create-demo-5", 49 | sheets = my_data 50 | ) 51 | 52 | gs4_create( 53 | "gs4-create-demo-6", 54 | sheets = list(chickwts = head(chickwts), mtcars = head(mtcars)) 55 | ) 56 | 57 | # Clean up 58 | gs4_find("gs4-create-demo") \%>\% 59 | googledrive::drive_trash() 60 | \dontshow{\}) # examplesIf} 61 | } 62 | \seealso{ 63 | Wraps the \code{spreadsheets.create} endpoint: 64 | \itemize{ 65 | \item \url{https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/create} 66 | } 67 | 68 | There is an article on writing Sheets: 69 | \itemize{ 70 | \item \url{https://googlesheets4.tidyverse.org/articles/articles/write-sheets.html} 71 | } 72 | 73 | Other write functions: 74 | \code{\link{gs4_formula}()}, 75 | \code{\link{range_delete}()}, 76 | \code{\link{range_flood}()}, 77 | \code{\link{range_write}()}, 78 | \code{\link{sheet_append}()}, 79 | \code{\link{sheet_write}()} 80 | } 81 | \concept{write functions} 82 | -------------------------------------------------------------------------------- /tests/testthat/test-sheet_write.R: -------------------------------------------------------------------------------- 1 | # ---- nm_fun ---- 2 | me_ <- nm_fun("TEST-sheet_write") 3 | 4 | # ---- tests ---- 5 | test_that("sheet_write() writes what it should", { 6 | skip_if_offline() 7 | skip_if_no_token() 8 | 9 | dat <- range_read( 10 | test_sheet("googlesheets4-col-types"), 11 | sheet = "lots-of-types", 12 | col_types = "lccinDT" # TODO: revisit when 'f' means factor 13 | ) 14 | dat$factor <- factor(dat$factor) 15 | 16 | ss <- local_ss(me_("datetimes")) 17 | sheet_write(dat, ss) 18 | x <- range_read(ss, sheet = "dat", col_types = "C") 19 | 20 | # the main interesting bit to test is whether we successfully sent 21 | # correct value for the date and datetime, with a sane (= ISO 8601) format 22 | expect_equal( 23 | purrr::pluck(x, "date", 1, "formattedValue"), 24 | format(dat$date[1]) 25 | ) 26 | expect_equal( 27 | purrr::pluck(x, "date", 1, "effectiveFormat", "numberFormat", "type"), 28 | "DATE" 29 | ) 30 | expect_equal( 31 | purrr::pluck(x, "date", 1, "effectiveFormat", "numberFormat", "pattern"), 32 | "yyyy-mm-dd" 33 | ) 34 | 35 | expect_equal( 36 | purrr::pluck(x, "datetime", 1, "formattedValue"), 37 | format(dat$datetime[1]) 38 | ) 39 | expect_equal( 40 | purrr::pluck(x, "datetime", 1, "effectiveFormat", "numberFormat", "type"), 41 | "DATE_TIME" 42 | ) 43 | expect_equal( 44 | purrr::pluck( 45 | x, 46 | "datetime", 47 | 1, 48 | "effectiveFormat", 49 | "numberFormat", 50 | "pattern" 51 | ), 52 | "yyyy-mm-dd hh:mm:ss" 53 | ) 54 | }) 55 | 56 | test_that("sheet_write() can figure out (work)sheet name", { 57 | skip_if_offline() 58 | skip_if_no_token() 59 | 60 | foofy <- data.frame(x = 1:3, y = letters[1:3]) 61 | 62 | ss <- local_ss(me_("sheetnames")) 63 | 64 | # get (work)sheet name from data frame's name 65 | sheet_write(foofy, ss) 66 | expect_equal(tail(sheet_names(ss), 1), "foofy") 67 | 68 | # we don't clobber existing (work)sheet if name was inferred 69 | sheet_write(foofy, ss) 70 | expect_equal(tail(sheet_names(ss), 1), "Sheet2") 71 | 72 | # we do write into existing (work)sheet if name is explicitly given 73 | sheet_write(foofy, ss, sheet = "foofy") 74 | expect_setequal(sheet_names(ss), c("Sheet1", "Sheet2", "foofy")) 75 | 76 | # we do write into existing (work)sheet if position is explicitly given 77 | sheet_write(foofy, ss, sheet = 2) 78 | expect_setequal(sheet_names(ss), c("Sheet1", "Sheet2", "foofy")) 79 | }) 80 | -------------------------------------------------------------------------------- /vignettes/articles/auth.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "googlesheets4 auth" 3 | --- 4 | 5 | ```{r setup, include = FALSE} 6 | can_decrypt <- gargle::secret_has_key("GOOGLESHEETS4_KEY") 7 | knitr::opts_chunk$set( 8 | collapse = TRUE, 9 | comment = "#>", 10 | error = TRUE, 11 | purl = can_decrypt, 12 | eval = can_decrypt 13 | ) 14 | options(tibble.print_min = 4L, tibble.print_max = 4L) 15 | ``` 16 | 17 | ```{r eval = !can_decrypt, echo = FALSE, comment = NA} 18 | message("No token available. Code chunks will not be evaluated.") 19 | ``` 20 | 21 | googlesheets4 will, by default, help you interact with Sheets as an authenticated Google user. The package facilitates this process upon first need. 22 | 23 | ## `gs4_deauth()` 24 | 25 | If you don't need to access private Sheets, use `gs4_deauth()` to indicate there is no need for a token. This puts googlesheets4 into a de-authorized mode. 26 | 27 | Here's how an R script might look if all you plan to do is read Sheets that are world-readable or readable by "anyone with a link": 28 | 29 | ```{r eval = FALSE} 30 | library(googlesheets4) 31 | 32 | gs4_deauth() 33 | 34 | # imagine this is the URL or ID of a Sheet readable by anyone (with a link) 35 | ss <- "?????" 36 | dat <- read_sheet(ss) 37 | ``` 38 | 39 | ## Default auth behaviour and beyond 40 | 41 | As soon as googlesheets4 needs a token, it tries to discover one. If it fails, it engages with you interactively to help you get a token. Once successful, that token is remembered for subsequent use in that R session. 42 | 43 | Users can take control of auth proactively via the [`gs4_auth*()` family of functions](https://googlesheets4.tidyverse.org/reference/index.html#auth). Examples of what you can control or provide: 44 | 45 | * The email address of the Google identity you want to use. 46 | * Whether to cache tokens and where. 47 | * Whether to use out-of-band auth. 48 | * A service account token. 49 | * The OAuth app and/or API key. 50 | 51 | Auth is actually handled by the gargle package ([gargle.r-lib.org](https://gargle.r-lib.org)), similar to googledrive, bigrquery, and gmailr, and gargle's documentation and articles are the definitive guide to more advanced topics. 52 | 53 | ## Multi-package auth 54 | 55 | It is common to use googlesheets4 together with the googledrive package ([googledrive.tidyverse.org](https://googledrive.tidyverse.org)). See the article [Using googlesheets4 with googledrive](https://googlesheets4.tidyverse.org/articles/articles/drive-and-sheets.html) for advice on how to streamline auth in this case. 56 | -------------------------------------------------------------------------------- /tests/testthat/test-sheet_resize.R: -------------------------------------------------------------------------------- 1 | # ---- nm_fun ---- 2 | me_ <- nm_fun("TEST-sheet_resize") 3 | 4 | # ---- tests ---- 5 | test_that("sheet_resize() works", { 6 | skip_if_offline() 7 | skip_if_no_token() 8 | 9 | ss <- local_ss(me_()) 10 | local_gs4_loud() 11 | 12 | # no resize occurs 13 | expect_message(sheet_resize(ss, nrow = 2, ncol = 6), "No need") %>% 14 | suppressMessages() 15 | 16 | # reduce sheet size 17 | suppressMessages(sheet_resize(ss, nrow = 5, ncol = 7, exact = TRUE)) 18 | props <- sheet_properties(ss) 19 | expect_equal(props$grid_rows, 5) 20 | expect_equal(props$grid_columns, 7) 21 | }) 22 | 23 | test_that("prepare_resize_request() works for resize & no resize", { 24 | n <- 3 25 | m <- 5 26 | sheet_info <- list(grid_rows = n, grid_columns = m) 27 | 28 | # (n - 1, n, n + 1) x (m - 1, m, m + 1) x (TRUE, FALSE) 29 | # 3 * 3 * 2 = 18 combinations 30 | 31 | # exact = FALSE 32 | df <- expand.grid( 33 | nrow_needed = n + -1:1, 34 | ncol_needed = m + -1:1, 35 | exact = FALSE 36 | ) 37 | req <- pmap(df, prepare_resize_request, sheet_info = sheet_info) 38 | grid_properties <- purrr::map( 39 | req, 40 | c("updateSheetProperties", "properties", "gridProperties") 41 | ) 42 | 43 | # sheet is big enough --> no resize request 44 | purrr::walk( 45 | grid_properties[df$nrow_needed <= n & df$ncol_needed <= m], 46 | expect_null 47 | ) 48 | 49 | # not enough rows 50 | purrr::walk( 51 | grid_properties[df$nrow_needed > n], 52 | ~ expect_true(has_name(.x, "rowCount")) 53 | ) 54 | 55 | # not enough columns 56 | purrr::walk( 57 | grid_properties[df$ncol_needed > m], 58 | ~ expect_true(has_name(.x, "columnCount")) 59 | ) 60 | 61 | # exact = TRUE 62 | df <- expand.grid( 63 | nrow_needed = n + -1:1, 64 | ncol_needed = m + -1:1, 65 | exact = TRUE 66 | ) 67 | req <- pmap(df, prepare_resize_request, sheet_info = sheet_info) 68 | grid_properties <- purrr::map( 69 | req, 70 | c("updateSheetProperties", "properties", "gridProperties") 71 | ) 72 | 73 | # sheet has correct size --> no resize request 74 | purrr::walk( 75 | grid_properties[df$nrow_needed == n & df$ncol_needed == m], 76 | expect_null 77 | ) 78 | 79 | # not enough rows or too many rows 80 | purrr::walk( 81 | grid_properties[df$nrow_needed != n], 82 | ~ expect_true(has_name(.x, "rowCount")) 83 | ) 84 | 85 | # not enough columns or too many columns 86 | purrr::walk( 87 | grid_properties[df$ncol_needed != m], 88 | ~ expect_true(has_name(.x, "columnCount")) 89 | ) 90 | }) 91 | -------------------------------------------------------------------------------- /R/sheet_append.R: -------------------------------------------------------------------------------- 1 | #' Append rows to a sheet 2 | #' 3 | #' Adds one or more new rows after the last row with data in a (work)sheet, 4 | #' increasing the row dimension of the sheet if necessary. 5 | #' 6 | #' @eval param_ss() 7 | #' @param data A data frame. 8 | #' @eval param_sheet(action = "append to") 9 | #' 10 | #' @template ss-return 11 | #' @export 12 | #' @family write functions 13 | #' @family worksheet functions 14 | #' @seealso Makes an `AppendCellsRequest`: 15 | #' * 16 | #' 17 | #' @examplesIf gs4_has_token() 18 | #' # we will recreate the table of "other" deaths from this example Sheet 19 | #' (deaths <- gs4_example("deaths") %>% 20 | #' range_read(range = "other_data", col_types = "????DD")) 21 | #' 22 | #' # split the data into 3 pieces, which we will send separately 23 | #' deaths_one <- deaths[1:5, ] 24 | #' deaths_two <- deaths[6, ] 25 | #' deaths_three <- deaths[7:10, ] 26 | #' 27 | #' # create a Sheet and send the first chunk of data 28 | #' ss <- gs4_create("sheet-append-demo", sheets = list(deaths = deaths_one)) 29 | #' 30 | #' # append a single row 31 | #' ss %>% sheet_append(deaths_two) 32 | #' 33 | #' # append remaining rows 34 | #' ss %>% sheet_append(deaths_three) 35 | #' 36 | #' # read and check against the original 37 | #' deaths_replica <- range_read(ss, col_types = "????DD") 38 | #' identical(deaths, deaths_replica) 39 | #' 40 | #' # clean up 41 | #' gs4_find("sheet-append-demo") %>% 42 | #' googledrive::drive_trash() 43 | sheet_append <- function(ss, data, sheet = 1) { 44 | check_data_frame(data) 45 | ssid <- as_sheets_id(ss) 46 | check_sheet(sheet) 47 | 48 | x <- gs4_get(ssid) 49 | gs4_bullets(c(v = "Writing to {.s_sheet {x$name}}.")) 50 | 51 | s <- lookup_sheet(sheet, sheets_df = x$sheets) 52 | gs4_bullets(c(v = "Appending {nrow(data)} row{?s} to {.w_sheet {s$name}}.")) 53 | 54 | req <- request_generate( 55 | "sheets.spreadsheets.batchUpdate", 56 | params = list( 57 | spreadsheetId = ssid, 58 | requests = prepare_rows(s$id, data), 59 | responseIncludeGridData = FALSE 60 | ) 61 | ) 62 | resp_raw <- request_make(req) 63 | gargle::response_process(resp_raw) 64 | 65 | invisible(ssid) 66 | } 67 | 68 | prepare_rows <- function(sheet_id, df) { 69 | list( 70 | appendCells = new( 71 | "AppendCellsRequest", 72 | sheetId = sheet_id, 73 | rows = as_RowData(df, col_names = FALSE), # an array of instances of RowData 74 | fields = "userEnteredValue,userEnteredFormat.numberFormat" 75 | ) 76 | ) 77 | } 78 | -------------------------------------------------------------------------------- /R/range_add_named.R: -------------------------------------------------------------------------------- 1 | #' Add a named range 2 | #' 3 | #' Adds a named range. Not really ready for showtime yet, so not exported. But 4 | #' I need it to (re)create the 'deaths' example Sheet. 5 | #' 6 | #' @noRd 7 | #' 8 | #' @eval param_ss() 9 | #' @param name Name for the new named range. 10 | #' @eval param_sheet(action = "SOMETHING") 11 | #' @template range 12 | #' 13 | #' @template ss-return 14 | #' @keywords internal 15 | #' @examplesIf gs4_has_token() 16 | #' dat <- data.frame(x = 1:3, y = letters[1:3]) 17 | #' ss <- gs4_create("range-add-named-demo", sheets = list(alpha = dat)) 18 | #' 19 | #' ss %>% 20 | #' range_add_named("two_rows", sheet = "alpha", range = "A2:B3") 21 | #' 22 | #' # notice the 'two_rows' named range reported here 23 | #' ss 24 | #' 25 | #' # clean up 26 | #' gs4_find("range-add-named-demo") %>% 27 | #' googledrive::drive_trash() 28 | range_add_named <- function(ss, name, sheet = NULL, range = NULL) { 29 | ssid <- as_sheets_id(ss) 30 | name <- check_string(name) 31 | maybe_sheet(sheet) 32 | check_range(range) 33 | 34 | x <- gs4_get(ssid) 35 | gs4_bullets(c(v = "Working in {.s_sheet {x$name}}.")) 36 | 37 | # determine (work)sheet ------------------------------------------------------ 38 | range_spec <- as_range_spec( 39 | range, 40 | sheet = sheet, 41 | sheets_df = x$sheets, 42 | nr_df = x$named_ranges 43 | ) 44 | range_spec$sheet_name <- range_spec$sheet_name %||% 45 | first_visible_name(x$sheets) 46 | 47 | # form batch update request -------------------------------------------------- 48 | req <- list( 49 | addNamedRange = new( 50 | "AddNamedRangeRequest", 51 | namedRange = as_NamedRange(range_spec, name = name) 52 | ) 53 | ) 54 | 55 | # do it ---------------------------------------------------------------------- 56 | req <- request_generate( 57 | "sheets.spreadsheets.batchUpdate", 58 | params = list( 59 | spreadsheetId = ssid, 60 | requests = list(req) 61 | ) 62 | ) 63 | resp_raw <- request_make(req) 64 | reply <- gargle::response_process(resp_raw) 65 | reply <- pluck(reply, "replies", 1, "addNamedRange", "namedRange") 66 | reply <- new("NamedRange", !!!reply) 67 | # TODO: this would not be so janky if new_googlesheets4_spreadsheet() were 68 | # factored in a way I could make better use of its logic 69 | reply <- as.list(as_tibble(reply)) 70 | reply$sheet_name <- vlookup( 71 | reply$sheet_id, 72 | data = x$sheets, 73 | key = "id", 74 | value = "name" 75 | ) 76 | A1_range <- qualified_A1(reply$sheet_name, do.call(make_cell_range, reply)) 77 | gs4_bullets(c( 78 | v = "Created new range named {.range {reply$name}} \\ 79 | representing {.range {A1_range}}." 80 | )) 81 | 82 | invisible(ssid) 83 | } 84 | -------------------------------------------------------------------------------- /.github/workflows/pr-commands.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | issue_comment: 5 | types: [created] 6 | 7 | name: Commands 8 | 9 | permissions: read-all 10 | 11 | jobs: 12 | document: 13 | if: ${{ github.event.issue.pull_request && (github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER') && startsWith(github.event.comment.body, '/document') }} 14 | name: document 15 | runs-on: ubuntu-latest 16 | env: 17 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 18 | steps: 19 | - uses: actions/checkout@v4 20 | 21 | - uses: r-lib/actions/pr-fetch@v2 22 | with: 23 | repo-token: ${{ secrets.GITHUB_TOKEN }} 24 | 25 | - uses: r-lib/actions/setup-r@v2 26 | with: 27 | use-public-rspm: true 28 | 29 | - uses: r-lib/actions/setup-r-dependencies@v2 30 | with: 31 | extra-packages: any::roxygen2 32 | needs: pr-document 33 | 34 | - name: Document 35 | run: roxygen2::roxygenise() 36 | shell: Rscript {0} 37 | 38 | - name: commit 39 | run: | 40 | git config --local user.name "$GITHUB_ACTOR" 41 | git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com" 42 | git add man/\* NAMESPACE 43 | git commit -m 'Document' 44 | 45 | - uses: r-lib/actions/pr-push@v2 46 | with: 47 | repo-token: ${{ secrets.GITHUB_TOKEN }} 48 | 49 | style: 50 | if: ${{ github.event.issue.pull_request && (github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER') && startsWith(github.event.comment.body, '/style') }} 51 | name: style 52 | runs-on: ubuntu-latest 53 | env: 54 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 55 | steps: 56 | - uses: actions/checkout@v4 57 | 58 | - uses: r-lib/actions/pr-fetch@v2 59 | with: 60 | repo-token: ${{ secrets.GITHUB_TOKEN }} 61 | 62 | - uses: r-lib/actions/setup-r@v2 63 | 64 | - name: Install dependencies 65 | run: install.packages("styler") 66 | shell: Rscript {0} 67 | 68 | - name: Style 69 | run: styler::style_pkg() 70 | shell: Rscript {0} 71 | 72 | - name: commit 73 | run: | 74 | git config --local user.name "$GITHUB_ACTOR" 75 | git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com" 76 | git add \*.R 77 | git commit -m 'Style' 78 | 79 | - uses: r-lib/actions/pr-push@v2 80 | with: 81 | repo-token: ${{ secrets.GITHUB_TOKEN }} 82 | -------------------------------------------------------------------------------- /.github/SUPPORT.md: -------------------------------------------------------------------------------- 1 | # Getting help with googlesheets4 2 | 3 | Thanks for using googlesheets4! 4 | Before filing an issue, there are a few places to explore and pieces to put together to make the process as smooth as possible. 5 | 6 | ## Make a reprex 7 | 8 | Start by making a minimal **repr**oducible **ex**ample using the [reprex](https://reprex.tidyverse.org/) package. 9 | If you haven't heard of or used reprex before, you're in for a treat! 10 | Seriously, reprex will make all of your R-question-asking endeavors easier (which is a pretty insane ROI for the five to ten minutes it'll take you to learn what it's all about). 11 | For additional reprex pointers, check out the [Get help!](https://www.tidyverse.org/help/) section of the tidyverse site. 12 | 13 | Special tricks for this package: 14 | [How to create a googlesheets4 reprex](https://googlesheets4.tidyverse.org/articles/articles/googlesheets4-reprex.html) 15 | 16 | ## Where to ask? 17 | 18 | Armed with your reprex, the next step is to figure out [where to ask](https://www.tidyverse.org/help/#where-to-ask). 19 | 20 | * If it's a question: start with [community.rstudio.com](https://community.rstudio.com/), and/or StackOverflow. There are more people there to answer questions. 21 | 22 | * If it's a bug: you're in the right place, [file an issue](https://github.com/tidyverse/googlesheets4/issues/new). 23 | 24 | * If you're not sure: let the community help you figure it out! 25 | If your problem _is_ a bug or a feature request, you can easily return here and report it. 26 | 27 | Before opening a new issue, be sure to [search issues and pull requests](https://github.com/tidyverse/googlesheets4/issues) to make sure the bug hasn't been reported and/or already fixed in the development version. 28 | By default, the search will be pre-populated with `is:issue is:open`. 29 | You can [edit the qualifiers](https://help.github.com/articles/searching-issues-and-pull-requests/) (e.g. `is:pr`, `is:closed`) as needed. 30 | For example, you'd simply remove `is:open` to search _all_ issues in the repo, open or closed. 31 | 32 | ## What happens next? 33 | 34 | To be as efficient as possible, development of tidyverse packages tends to be very bursty, so you shouldn't worry if you don't get an immediate response. 35 | Typically we don't look at a repo until a sufficient quantity of issues accumulates, then there’s a burst of intense activity as we focus our efforts. 36 | That makes development more efficient because it avoids expensive context switching between problems, at the cost of taking longer to get back to you. 37 | This process makes a good reprex particularly important because it might be multiple months between your initial report and when we start working on it. 38 | If we can’t reproduce the bug, we can’t fix it! 39 | -------------------------------------------------------------------------------- /tests/testthat/test-range_autofit.R: -------------------------------------------------------------------------------- 1 | # ---- nm_fun ---- 2 | me_ <- nm_fun("TEST-range_autofit") 3 | 4 | # ---- tests ---- 5 | test_that("range_autofit() works", { 6 | skip_if_offline() 7 | skip_if_no_token() 8 | 9 | dat <- tibble::tribble( 10 | ~x, ~y, ~z, ~a, ~b, ~c, 11 | "abcd", "efgh", "ijkl", "mnop", "qrst", "uvwx" 12 | ) 13 | ss <- local_ss(me_(), sheets = list(dat = dat)) 14 | ssid <- as_sheets_id(ss) 15 | 16 | range_autofit(ss) 17 | before <- gs4_get_impl_( 18 | ssid, 19 | fields = "sheets.data.columnMetadata.pixelSize" 20 | ) 21 | 22 | dat2 <- purrr::modify(dat, ~ paste0(.x, "_", .x)) 23 | dat4 <- purrr::modify(dat2, ~ paste0(.x, "_", .x)) 24 | sheet_append(ss, dat4) 25 | range_autofit(ss) 26 | 27 | after <- gs4_get_impl_( 28 | ssid, 29 | fields = "sheets.data.columnMetadata.pixelSize" 30 | ) 31 | 32 | before <- pluck(before, "sheets", 1, "data", 1, "columnMetadata") 33 | after <- pluck(after, "sheets", 1, "data", 1, "columnMetadata") 34 | expect_true(all(unlist(before) < unlist(after))) 35 | }) 36 | 37 | # ---- helpers ---- 38 | test_that("A1-style ranges can be turned into a request", { 39 | req <- prepare_auto_resize_request(123, as_range_spec("D:H")) 40 | req <- pluck(req, 1, "autoResizeDimensions", "dimensions") 41 | expect_equal(req$dimension, "COLUMNS") 42 | expect_equal(req$startIndex, cellranger::letter_to_num("D") - 1) 43 | expect_equal(req$endIndex, cellranger::letter_to_num("H")) 44 | 45 | req <- prepare_auto_resize_request(123, as_range_spec("3:7")) 46 | req <- pluck(req, 1, "autoResizeDimensions", "dimensions") 47 | expect_equal(req$dimension, "ROWS") 48 | expect_equal(req$startIndex, 3 - 1) 49 | expect_equal(req$endIndex, 7) 50 | }) 51 | 52 | test_that("cell_limits can be turned into a request", { 53 | req <- prepare_auto_resize_request( 54 | 123, 55 | as_range_spec(cell_limits()) 56 | ) 57 | req <- pluck(req, 1, "autoResizeDimensions", "dimensions") 58 | expect_equal(req$dimension, "COLUMNS") 59 | expect_null(req$startIndex) 60 | expect_null(req$endIndex) 61 | 62 | req <- prepare_auto_resize_request( 63 | 123, 64 | as_range_spec(cell_cols(c(3, NA))) 65 | ) 66 | req <- pluck(req, 1, "autoResizeDimensions", "dimensions") 67 | expect_equal(req$dimension, "COLUMNS") 68 | expect_equal(req$startIndex, 3 - 1) 69 | expect_null(req$endIndex) 70 | 71 | req <- prepare_auto_resize_request( 72 | 123, 73 | as_range_spec(cell_cols(c(NA, 5))) 74 | ) 75 | req <- pluck(req, 1, "autoResizeDimensions", "dimensions") 76 | expect_equal(req$dimension, "COLUMNS") 77 | expect_equal(req$endIndex, 5) 78 | }) 79 | 80 | test_that("an invalid range is rejected", { 81 | expect_error( 82 | prepare_auto_resize_request(123, as_range_spec("D3:H")), 83 | "only columns or only rows" 84 | ) 85 | }) 86 | -------------------------------------------------------------------------------- /man/spread_sheet.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/range_read.R 3 | \name{spread_sheet} 4 | \alias{spread_sheet} 5 | \title{Spread a data frame of cells into spreadsheet shape} 6 | \usage{ 7 | spread_sheet( 8 | df, 9 | col_names = TRUE, 10 | col_types = NULL, 11 | na = "", 12 | trim_ws = TRUE, 13 | guess_max = min(1000, max(df$row)), 14 | .name_repair = "unique" 15 | ) 16 | } 17 | \arguments{ 18 | \item{df}{A data frame with one row per (nonempty) cell, integer variables 19 | \code{row} and \code{column} (probably referring to location within the spreadsheet), 20 | and a list-column \code{cell} of \code{SHEET_CELL} objects.} 21 | 22 | \item{col_names}{\code{TRUE} to use the first row as column names, \code{FALSE} to get 23 | default names, or a character vector to provide column names directly. If 24 | user provides \code{col_types}, \code{col_names} can have one entry per column or one 25 | entry per unskipped column.} 26 | 27 | \item{col_types}{Column types. Either \code{NULL} to guess all from the 28 | spreadsheet or a string of readr-style shortcodes, with one character or 29 | code per column. If exactly one \code{col_type} is specified, it is recycled. 30 | See Column Specification for more.} 31 | 32 | \item{na}{Character vector of strings to interpret as missing values. By 33 | default, blank cells are treated as missing data.} 34 | 35 | \item{trim_ws}{Logical. Should leading and trailing whitespace be trimmed 36 | from cell contents?} 37 | 38 | \item{guess_max}{Maximum number of data rows to use for guessing column 39 | types.} 40 | 41 | \item{.name_repair}{Handling of column names. By default, googlesheets4 42 | ensures column names are not empty and are unique. There is full support 43 | for \code{.name_repair} as documented in \code{\link[tibble:tibble]{tibble::tibble()}}.} 44 | } 45 | \value{ 46 | A tibble in the shape of the original spreadsheet, but enforcing 47 | user's wishes regarding column names, column types, \code{NA} strings, and 48 | whitespace trimming. 49 | } 50 | \description{ 51 | Reshapes a data frame of cells (presumably the output of 52 | \code{\link[=range_read_cells]{range_read_cells()}}) into another data frame, i.e., puts it back into the 53 | shape of the source spreadsheet. This function exists primarily for internal 54 | use and for testing. The flagship function \code{\link[=range_read]{range_read()}}, a.k.a. 55 | \code{\link[=read_sheet]{read_sheet()}}, is what most users are looking for. It is basically 56 | \code{\link[=range_read_cells]{range_read_cells()}} + \code{spread_sheet()}. 57 | } 58 | \examples{ 59 | \dontshow{if (gs4_has_token()) withAutoprint(\{ # examplesIf} 60 | df <- gs4_example("mini-gap") \%>\% 61 | range_read_cells() 62 | spread_sheet(df) 63 | 64 | # ^^ gets same result as ... 65 | read_sheet(gs4_example("mini-gap")) 66 | \dontshow{\}) # examplesIf} 67 | } 68 | -------------------------------------------------------------------------------- /vignettes/articles/fun-with-googledrive-and-readxl.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Fun with googledrive and readxl" 3 | --- 4 | 5 | This article demonstrates how to use googlesheets4, googledrive, and readxl together. We demonstrate a roundtrip for data that starts and ends in R, but travels in spreadsheet form, via Google Sheets. 6 | 7 | ```{r setup, include = FALSE} 8 | can_decrypt <- gargle::secret_has_key("GOOGLESHEETS4_KEY") 9 | knitr::opts_chunk$set( 10 | collapse = TRUE, 11 | comment = "#>", 12 | error = TRUE, 13 | purl = can_decrypt, 14 | eval = can_decrypt 15 | ) 16 | ``` 17 | 18 | ```{r eval = !can_decrypt, echo = FALSE, comment = NA} 19 | message("No token available. Code chunks will not be evaluated.") 20 | ``` 21 | 22 | ## Attach packages 23 | 24 | ```{r} 25 | library(googlesheets4) 26 | library(googledrive) 27 | library(readxl) 28 | ``` 29 | 30 | ## Auth 31 | 32 | ```{r include = FALSE} 33 | # happens in .onLoad() when IN_PKGDOWN, but need this for local dev/preview 34 | googlesheets4:::gs4_auth_docs(drive = TRUE) 35 | 36 | # attempt to reduce quota exhaustion problems 37 | if (identical(Sys.getenv("IN_PKGDOWN"), "true")) Sys.sleep(30) 38 | ``` 39 | 40 | As a regular, interactive user, you can just let googlesheets4 prompt you for anything it needs re: auth. 41 | 42 | Since this article is compiled noninteractively on a server, we activate a service token here, in a hidden chunk. We are also using a shared token for Sheets and Drive. You can read how to do that in your own work in the article [Using googlesheets4 with googledrive](https://googlesheets4.tidyverse.org/articles/articles/drive-and-sheets.html). 43 | 44 | ## Create a private Sheet from csv with the Drive API 45 | 46 | Put the iris data into a csv file. 47 | 48 | ```{r} 49 | (iris_tempfile <- tempfile(pattern = "iris-", fileext = ".csv")) 50 | write.csv(iris, iris_tempfile, row.names = FALSE) 51 | ``` 52 | 53 | Use `googledrive::drive_upload()` to upload the csv and simultaneously convert to a Sheet. 54 | 55 | ```{r} 56 | (iris_ss <- drive_upload(iris_tempfile, type = "spreadsheet")) 57 | 58 | # visit the new Sheet in the browser, in an interactive session! 59 | drive_browse(iris_ss) 60 | ``` 61 | 62 | Read data from the private Sheet into R. 63 | ```{r} 64 | read_sheet(iris_ss, range = "B1:D6") 65 | ``` 66 | 67 | ## Create a local xlsx from a Sheet with the Drive API 68 | 69 | Download the Sheet as an Excel workbook. 70 | 71 | ```{r} 72 | (iris_xlsxfile <- sub("[.]csv", ".xlsx", iris_tempfile)) 73 | drive_download(iris_ss, path = iris_xlsxfile, overwrite = TRUE) 74 | ``` 75 | 76 | ## Read xlsx with readxl 77 | 78 | Read the iris data back in via `readxl::read_excel()`. 79 | 80 | ```{r} 81 | if (requireNamespace("readxl", quietly = TRUE)) { 82 | readxl::read_excel(iris_xlsxfile) 83 | } 84 | ``` 85 | 86 | ## Clean up 87 | 88 | ```{r} 89 | file.remove(iris_tempfile, iris_xlsxfile) 90 | drive_trash(iris_ss) 91 | ``` 92 | -------------------------------------------------------------------------------- /man/sheet_resize.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/sheet_resize.R 3 | \name{sheet_resize} 4 | \alias{sheet_resize} 5 | \title{Change the size of a (work)sheet} 6 | \usage{ 7 | sheet_resize(ss, sheet = NULL, nrow = NULL, ncol = NULL, exact = FALSE) 8 | } 9 | \arguments{ 10 | \item{ss}{Something that identifies a Google Sheet: 11 | \itemize{ 12 | \item its file id as a string or \code{\link[googledrive:drive_id]{drive_id}} 13 | \item a URL from which we can recover the id 14 | \item a one-row \code{\link[googledrive:dribble]{dribble}}, which is how googledrive 15 | represents Drive files 16 | \item an instance of \code{googlesheets4_spreadsheet}, which is what \code{\link[=gs4_get]{gs4_get()}} 17 | returns 18 | } 19 | 20 | Processed through \code{\link[=as_sheets_id]{as_sheets_id()}}.} 21 | 22 | \item{sheet}{Sheet to resize, in the sense of "worksheet" or "tab". You can identify a sheet by name, with a string, or by position, with a number.} 23 | 24 | \item{nrow, ncol}{Desired number of rows or columns, respectively. The default 25 | of \code{NULL} means to leave unchanged.} 26 | 27 | \item{exact}{Logical, indicating whether to impose \code{nrow} and \code{ncol} exactly 28 | or to treat them as lower bounds. If \code{exact = FALSE}, 29 | \code{sheet_resize()} can only add cells. If \code{exact = TRUE}, cells can be 30 | deleted and their contents are lost.} 31 | } 32 | \value{ 33 | The input \code{ss}, as an instance of \code{\link{sheets_id}} 34 | } 35 | \description{ 36 | Changes the number of rows and/or columns in a (work)sheet. 37 | } 38 | \examples{ 39 | \dontshow{if (gs4_has_token()) withAutoprint(\{ # examplesIf} 40 | # create a Sheet with the default initial worksheet 41 | (ss <- gs4_create("sheet-resize-demo")) 42 | 43 | # see (work)sheet dims 44 | sheet_properties(ss) 45 | 46 | # no resize occurs 47 | sheet_resize(ss, nrow = 2, ncol = 6) 48 | 49 | # reduce sheet size 50 | sheet_resize(ss, nrow = 5, ncol = 7, exact = TRUE) 51 | 52 | # add rows 53 | sheet_resize(ss, nrow = 7) 54 | 55 | # add columns 56 | sheet_resize(ss, ncol = 10) 57 | 58 | # add rows and columns 59 | sheet_resize(ss, nrow = 9, ncol = 12) 60 | 61 | # re-inspect (work)sheet dims 62 | sheet_properties(ss) 63 | 64 | # clean up 65 | gs4_find("sheet-resize-demo") \%>\% 66 | googledrive::drive_trash() 67 | \dontshow{\}) # examplesIf} 68 | } 69 | \seealso{ 70 | Makes an \code{UpdateSheetPropertiesRequest}: 71 | \itemize{ 72 | \item \url{https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#UpdateSheetPropertiesRequest} 73 | } 74 | 75 | Other worksheet functions: 76 | \code{\link{sheet_add}()}, 77 | \code{\link{sheet_append}()}, 78 | \code{\link{sheet_copy}()}, 79 | \code{\link{sheet_delete}()}, 80 | \code{\link{sheet_properties}()}, 81 | \code{\link{sheet_relocate}()}, 82 | \code{\link{sheet_rename}()}, 83 | \code{\link{sheet_write}()} 84 | } 85 | \concept{worksheet functions} 86 | -------------------------------------------------------------------------------- /tests/testthat/test-range_read_cells.R: -------------------------------------------------------------------------------- 1 | test_that("cells() returns `row` and `col` as integer", { 2 | skip_if_offline() 3 | skip_if_no_token() 4 | 5 | out <- range_read_cells( 6 | test_sheet("googlesheets4-cell-tests"), 7 | range = "'range-experimentation'!A1:B2" 8 | ) 9 | expect_true(is.integer(out$row)) 10 | expect_true(is.integer(out$col)) 11 | }) 12 | 13 | test_that("slightly tricky `range`s work", { 14 | skip_if_offline() 15 | skip_if_no_token() 16 | 17 | out <- range_read_cells( 18 | test_sheet("googlesheets4-cell-tests"), 19 | range = "'range-experimentation'!B:D" 20 | ) 21 | expect_true(all(grepl("^[BCD]", out$loc))) 22 | 23 | out <- range_read_cells( 24 | test_sheet("googlesheets4-cell-tests"), 25 | range = "'range-experimentation'!2:3" 26 | ) 27 | expect_true(all(grepl("[23]$", out$loc))) 28 | 29 | out <- range_read_cells( 30 | test_sheet("googlesheets4-cell-tests"), 31 | range = "'range-experimentation'!B3:C" 32 | ) 33 | expect_true(all(grepl("^[BC]", out$loc))) 34 | expect_true(all(grepl("[3-9]$", out$loc))) 35 | 36 | out <- range_read_cells( 37 | test_sheet("googlesheets4-cell-tests"), 38 | range = "'range-experimentation'!B3:5" 39 | ) 40 | expect_true(all(grepl("^[BCDE]", out$loc))) 41 | expect_true(all(grepl("[345]$", out$loc))) 42 | }) 43 | 44 | # https://github.com/tidyverse/googlesheets4/issues/4 45 | test_that("full cell data and empties are within reach", { 46 | skip_if_offline() 47 | skip_if_no_token() 48 | 49 | out <- range_read_cells( 50 | test_sheet("googlesheets4-cell-tests"), 51 | sheet = "empties-and-formats", 52 | cell_data = "full", 53 | discard_empty = FALSE 54 | ) 55 | 56 | # B2 is empty; make sure it's here 57 | expect_true("B2" %in% out$loc) 58 | 59 | # C2 is empty and orange; make sure it's here and format is available 60 | expect_no_error( 61 | cell <- out$cell[[which(out$loc == "C2")]] 62 | ) 63 | expect_true(!is.null(cell$effectiveFormat)) 64 | 65 | # C1 bears a note 66 | expect_no_error( 67 | cell <- out$cell[[which(out$loc == "C1")]] 68 | ) 69 | note <- cell$note 70 | expect_true(!is.null(note)) 71 | expect_match(note, "Note") 72 | }) 73 | 74 | # https://github.com/tidyverse/googlesheets4/issues/78 75 | test_that("formula cells are parsed based on effectiveValue", { 76 | skip_if_offline() 77 | skip_if_no_token() 78 | 79 | out <- range_read_cells( 80 | test_sheet("googlesheets4-cell-tests"), 81 | sheet = "formulas", 82 | range = "B:B", 83 | cell_data = "full", 84 | discard_empty = FALSE 85 | ) 86 | 87 | expect_s3_class(out$cell[[which(out$loc == "B2")]], "CELL_TEXT") 88 | expect_s3_class(out$cell[[which(out$loc == "B3")]], "CELL_NUMERIC") 89 | expect_s3_class(out$cell[[which(out$loc == "B4")]], "CELL_BLANK") 90 | expect_s3_class(out$cell[[which(out$loc == "B5")]], "CELL_TEXT") 91 | expect_s3_class(out$cell[[which(out$loc == "B6")]], "CELL_BLANK") 92 | }) 93 | -------------------------------------------------------------------------------- /tests/testthat/test-gs4_auth.R: -------------------------------------------------------------------------------- 1 | test_that("gs4_auth_configure works", { 2 | old_client <- gs4_oauth_client() 3 | old_api_key <- gs4_api_key() 4 | withr::defer( 5 | gs4_auth_configure(client = old_client, api_key = old_api_key) 6 | ) 7 | 8 | expect_no_error(gs4_oauth_client()) 9 | expect_no_error(gs4_api_key()) 10 | 11 | expect_snapshot( 12 | gs4_auth_configure(client = gargle::gargle_client(), path = "PATH"), 13 | error = TRUE 14 | ) 15 | 16 | gs4_auth_configure(client = gargle::gargle_client()) 17 | expect_s3_class(gs4_oauth_client(), "gargle_oauth_client") 18 | 19 | path_to_json <- system.file( 20 | "extdata", 21 | "client_secret_installed.googleusercontent.com.json", 22 | package = "gargle" 23 | ) 24 | gs4_auth_configure(path = path_to_json) 25 | expect_s3_class(gs4_oauth_client(), "gargle_oauth_client") 26 | 27 | gs4_auth_configure(client = NULL) 28 | expect_null(gs4_oauth_client()) 29 | 30 | gs4_auth_configure(api_key = "API_KEY") 31 | expect_identical(gs4_api_key(), "API_KEY") 32 | 33 | gs4_auth_configure(api_key = NULL) 34 | expect_null(gs4_api_key()) 35 | }) 36 | 37 | test_that("gs4_oauth_app() is deprecated", { 38 | withr::local_options(lifecycle_verbosity = "warning") 39 | expect_snapshot(absorb <- gs4_oauth_app()) 40 | }) 41 | 42 | test_that("gs4_auth_configure(app =) is deprecated in favor of client", { 43 | withr::local_options(lifecycle_verbosity = "warning") 44 | (original_client <- gs4_oauth_client()) 45 | withr::defer(gs4_auth_configure(client = original_client)) 46 | 47 | client <- gargle::gargle_oauth_client_from_json( 48 | system.file( 49 | "extdata", 50 | "client_secret_installed.googleusercontent.com.json", 51 | package = "gargle" 52 | ), 53 | name = "test-client" 54 | ) 55 | expect_snapshot( 56 | gs4_auth_configure(app = client) 57 | ) 58 | expect_equal(gs4_oauth_client()$name, "test-client") 59 | expect_equal(gs4_oauth_client()$id, "abc.apps.googleusercontent.com") 60 | }) 61 | 62 | # gs4_scopes() ---- 63 | test_that("gs4_scopes() reveals Sheets scopes", { 64 | expect_snapshot(gs4_scopes()) 65 | }) 66 | 67 | test_that("gs4_scopes() substitutes actual scope for short form", { 68 | expect_equal( 69 | gs4_scopes(c( 70 | "spreadsheets", 71 | "drive", 72 | "drive.readonly" 73 | )), 74 | c( 75 | "https://www.googleapis.com/auth/spreadsheets", 76 | "https://www.googleapis.com/auth/drive", 77 | "https://www.googleapis.com/auth/drive.readonly" 78 | ) 79 | ) 80 | }) 81 | 82 | test_that("gs4_scopes() passes unrecognized scopes through", { 83 | expect_equal( 84 | gs4_scopes(c( 85 | "email", 86 | "spreadsheets.readonly", 87 | "https://www.googleapis.com/auth/cloud-platform" 88 | )), 89 | c( 90 | "email", 91 | "https://www.googleapis.com/auth/spreadsheets.readonly", 92 | "https://www.googleapis.com/auth/cloud-platform" 93 | ) 94 | ) 95 | }) 96 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to googlesheets4 2 | 3 | This outlines how to propose a change to googlesheets4. 4 | For more detailed info about contributing to this, and other tidyverse packages, please see the 5 | [**development contributing guide**](https://rstd.io/tidy-contrib). 6 | 7 | ## Fixing typos 8 | 9 | You can fix typos, spelling mistakes, or grammatical errors in the documentation directly using the GitHub web interface, as long as the changes are made in the _source_ file. 10 | This generally means you'll need to edit [roxygen2 comments](https://roxygen2.r-lib.org/articles/roxygen2.html) in an `.R`, not a `.Rd` file. 11 | You can find the `.R` file that generates the `.Rd` by reading the comment in the first line. 12 | 13 | ## Bigger changes 14 | 15 | If you want to make a bigger change, it's a good idea to first file an issue and make sure someone from the team agrees that it’s needed. 16 | If you’ve found a bug, please file an issue that illustrates the bug with a minimal 17 | [reprex](https://www.tidyverse.org/help/#reprex) (this will also help you write a unit test, if needed). 18 | 19 | ### Pull request process 20 | 21 | * Fork the package and clone onto your computer. If you haven't done this before, we recommend using `usethis::create_from_github("tidyverse/googlesheets4", fork = TRUE)`. 22 | 23 | * Install all development dependences with `devtools::install_dev_deps()`, and then make sure the package passes R CMD check by running `devtools::check()`. 24 | If R CMD check doesn't pass cleanly, it's a good idea to ask for help before continuing. 25 | * Create a Git branch for your pull request (PR). We recommend using `usethis::pr_init("brief-description-of-change")`. 26 | 27 | * Make your changes, commit to git, and then create a PR by running `usethis::pr_push()`, and following the prompts in your browser. 28 | The title of your PR should briefly describe the change. 29 | The body of your PR should contain `Fixes #issue-number`. 30 | 31 | * For user-facing changes, add a bullet to the top of `NEWS.md` (i.e. just below the first header). Follow the style described in . 32 | 33 | ### Code style 34 | 35 | * New code should follow the tidyverse [style guide](https://style.tidyverse.org). 36 | You can use the [styler](https://CRAN.R-project.org/package=styler) package to apply these styles, but please don't restyle code that has nothing to do with your PR. 37 | 38 | * We use [roxygen2](https://cran.r-project.org/package=roxygen2), with [Markdown syntax](https://roxygen2.r-lib.org/articles/rd-formatting.html), for documentation. 39 | 40 | * We use [testthat](https://cran.r-project.org/package=testthat) for unit tests. 41 | Contributions with test cases included are easier to accept. 42 | 43 | ## Code of Conduct 44 | 45 | Please note that the googlesheets4 project is released with a 46 | [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By contributing to this 47 | project you agree to abide by its terms. 48 | --------------------------------------------------------------------------------