googlesheets4/0000755000176200001440000000000014076066132013033 5ustar liggesusersgooglesheets4/NAMESPACE0000644000176200001440000001113114076051635014251 0ustar liggesusers# Generated by roxygen2: do not edit by hand S3method(as_CellData,"NULL") S3method(as_CellData,Date) S3method(as_CellData,POSIXct) S3method(as_CellData,character) S3method(as_CellData,default) S3method(as_CellData,factor) S3method(as_CellData,googlesheets4_formula) S3method(as_CellData,googlesheets4_schema_CellData) S3method(as_CellData,list) S3method(as_CellData,logical) S3method(as_CellData,numeric) S3method(as_GridCoordinate,default) S3method(as_GridCoordinate,range_spec) S3method(as_GridRange,default) S3method(as_GridRange,range_spec) S3method(as_NamedRange,default) S3method(as_NamedRange,range_spec) S3method(as_Sheet,"NULL") S3method(as_Sheet,character) S3method(as_Sheet,data.frame) S3method(as_Sheet,default) S3method(as_id,googlesheets4_spreadsheet) S3method(as_id,sheets_id) S3method(as_range_spec,"NULL") S3method(as_range_spec,cell_limits) S3method(as_range_spec,character) S3method(as_range_spec,default) S3method(as_sheets_id,"NULL") S3method(as_sheets_id,character) S3method(as_sheets_id,default) S3method(as_sheets_id,dribble) S3method(as_sheets_id,drive_id) S3method(as_sheets_id,googlesheets4_spreadsheet) S3method(as_sheets_id,sheets_id) S3method(as_tibble,googlesheets4_schema_GridRange) S3method(as_tibble,googlesheets4_schema_NamedRange) S3method(as_tibble,googlesheets4_schema_ProtectedRange) S3method(as_tibble,googlesheets4_schema_Sheet) S3method(as_tibble,googlesheets4_schema_SheetProperties) S3method(ctype,"NULL") S3method(ctype,SHEETS_CELL) S3method(ctype,character) S3method(ctype,default) S3method(ctype,list) S3method(format,googlesheets4_spreadsheet) S3method(format,range_spec) S3method(patch,default) S3method(patch,googlesheets4_schema) S3method(print,googlesheets4_spreadsheet) S3method(print,range_spec) S3method(print,sheets_id) S3method(vec_cast,character.sheets_id) S3method(vec_cast,drive_id.sheets_id) S3method(vec_cast,googlesheets4_formula) S3method(vec_cast,sheets_id.character) S3method(vec_cast,sheets_id.drive_id) S3method(vec_cast,sheets_id.sheets_id) S3method(vec_cast.character,googlesheets4_formula) S3method(vec_cast.googlesheets4_formula,character) S3method(vec_cast.googlesheets4_formula,default) S3method(vec_cast.googlesheets4_formula,googlesheets4_formula) S3method(vec_ptype2,character.sheets_id) S3method(vec_ptype2,drive_id.sheets_id) S3method(vec_ptype2,googlesheets4_formula) S3method(vec_ptype2,sheets_id.character) S3method(vec_ptype2,sheets_id.drive_id) S3method(vec_ptype2,sheets_id.sheets_id) S3method(vec_ptype2.character,googlesheets4_formula) S3method(vec_ptype2.googlesheets4_formula,character) S3method(vec_ptype2.googlesheets4_formula,default) S3method(vec_ptype2.googlesheets4_formula,googlesheets4_formula) S3method(vec_ptype_abbr,googlesheets4_formula) S3method(vec_ptype_abbr,sheets_id) export("%>%") export(anchored) export(as_sheets_id) export(cell_cols) export(cell_limits) export(cell_rows) export(gs4_api_key) export(gs4_auth) export(gs4_auth_configure) export(gs4_browse) export(gs4_create) export(gs4_deauth) export(gs4_endpoints) export(gs4_example) export(gs4_examples) export(gs4_find) export(gs4_fodder) export(gs4_formula) export(gs4_get) export(gs4_has_token) export(gs4_oauth_app) export(gs4_random) export(gs4_token) export(gs4_user) export(local_gs4_quiet) export(range_autofit) export(range_clear) export(range_delete) export(range_flood) export(range_read) export(range_read_cells) export(range_speedread) export(range_write) export(read_sheet) export(request_generate) export(request_make) export(sheet_add) export(sheet_append) export(sheet_copy) export(sheet_delete) export(sheet_names) export(sheet_properties) export(sheet_relocate) export(sheet_rename) export(sheet_resize) export(sheet_write) export(spread_sheet) export(vec_cast.googlesheets4_formula) export(vec_ptype2.googlesheets4_formula) export(with_gs4_quiet) export(write_sheet) import(rlang) import(vctrs) importFrom(cellranger,anchored) importFrom(cellranger,cell_cols) importFrom(cellranger,cell_limits) importFrom(cellranger,cell_rows) importFrom(gargle,bulletize) importFrom(gargle,gargle_map_cli) importFrom(glue,glue) importFrom(glue,glue_collapse) importFrom(glue,glue_data) importFrom(googledrive,as_id) importFrom(magrittr,"%>%") importFrom(methods,setOldClass) importFrom(purrr,"%||%") importFrom(purrr,compact) importFrom(purrr,discard) importFrom(purrr,imap) importFrom(purrr,keep) importFrom(purrr,map) importFrom(purrr,map2) importFrom(purrr,map_chr) importFrom(purrr,map_dbl) importFrom(purrr,map_int) importFrom(purrr,map_lgl) importFrom(purrr,modify_if) importFrom(purrr,pluck) importFrom(purrr,pmap) importFrom(purrr,pmap_chr) importFrom(purrr,transpose) importFrom(purrr,walk) importFrom(tibble,as_tibble) googlesheets4/LICENSE0000644000176200001440000000007013643533245014040 0ustar liggesusersYEAR: 2020 COPYRIGHT HOLDER: Jennifer Bryan and RStudio googlesheets4/README.md0000644000176200001440000002170714075567246014334 0ustar liggesusers # googlesheets4 [![CRAN status](https://www.r-pkg.org/badges/version/googlesheets4)](https://CRAN.R-project.org/package=googlesheets4) [![Codecov test coverage](https://codecov.io/gh/tidyverse/googlesheets4/branch/master/graph/badge.svg)](https://codecov.io/gh/tidyverse/googlesheets4?branch=master) [![R-CMD-check](https://github.com/tidyverse/googlesheets4/workflows/R-CMD-check/badge.svg)](https://github.com/tidyverse/googlesheets4/actions) ## Overview googlesheets4 provides an R interface to [Google Sheets](https://docs.google.com/spreadsheets/) via the [Sheets API v4](https://developers.google.com/sheets/api/). It is a reboot of an earlier package called [googlesheets](https://github.com/jennybc/googlesheets#readme). *Why **4**? Why googlesheets**4**? Did I miss googlesheets1 through 3? No. The idea is to name the package after the corresponding version of the Sheets API. In hindsight, the original googlesheets should have been googlesheets**3**.* ## Installation You can install the released version of googlesheets4 from [CRAN](https://CRAN.R-project.org) with: ``` r install.packages("googlesheets4") ``` And the development version from [GitHub](https://github.com/) with: ``` r # install.packages("devtools") devtools::install_github("tidyverse/googlesheets4") ``` ## Auth googlesheets4 will, by default, help you interact with Sheets as an authenticated Google user. If you don’t plan to write Sheets or to read private Sheets, use `gs4_deauth()` to indicate there is no need for a token. See the article [googlesheets4 auth](https://googlesheets4.tidyverse.org/articles/articles/auth.html) for more. For this overview, we’ve logged into Google as a specific user in a hidden chunk. ## Attach googlesheets4 ``` r library(googlesheets4) ``` ## Read The main “read” function of the googlesheets4 package goes by two names, because we want it to make sense in two contexts: - `read_sheet()` evokes other table-reading functions, like `readr::read_csv()` and `readxl::read_excel()`. The `sheet` in this case refers to a Google (spread)Sheet. - `range_read()` is the right name according to the [naming convention](https://googlesheets4.tidyverse.org/articles/articles/function-class-names.html) used throughout the googlesheets4 package. `read_sheet()` and `range_read()` are synonyms and you can use either one. Here we’ll use `read_sheet()`. googlesheets4 is [pipe-friendly](https://r4ds.had.co.nz/pipes.html) (and reexports `%>%`), but works just fine without the pipe. Read from - a URL - a Sheet ID - a [`dribble`](https://googledrive.tidyverse.org/reference/dribble.html) produced by the googledrive package, which can lookup by file name These all achieve the same thing: ``` r # URL read_sheet("https://docs.google.com/spreadsheets/d/1U6Cf_qEOhiR9AZqTqS3mbMF3zt2db48ZP5v3rkrAEJY/edit#gid=780868077") #> ✓ Reading from "gapminder". #> ✓ Range 'Africa'. #> # A tibble: 624 x 6 #> country continent year lifeExp pop gdpPercap #> #> 1 Algeria Africa 1952 43.1 9279525 2449. #> 2 Algeria Africa 1957 45.7 10270856 3014. #> 3 Algeria Africa 1962 48.3 11000948 2551. #> 4 Algeria Africa 1967 51.4 12760499 3247. #> 5 Algeria Africa 1972 54.5 14760787 4183. #> # … with 619 more rows # Sheet ID read_sheet("1U6Cf_qEOhiR9AZqTqS3mbMF3zt2db48ZP5v3rkrAEJY") #> ✓ Reading from "gapminder". #> ✓ Range 'Africa'. #> # A tibble: 624 x 6 #> country continent year lifeExp pop gdpPercap #> #> 1 Algeria Africa 1952 43.1 9279525 2449. #> 2 Algeria Africa 1957 45.7 10270856 3014. #> 3 Algeria Africa 1962 48.3 11000948 2551. #> 4 Algeria Africa 1967 51.4 12760499 3247. #> 5 Algeria Africa 1972 54.5 14760787 4183. #> # … with 619 more rows # a googledrive "dribble" googledrive::drive_get("gapminder") %>% read_sheet() #> ✓ The input `path` resolved to exactly 1 file. #> ✓ Reading from "gapminder". #> ✓ Range 'Africa'. #> # A tibble: 624 x 6 #> country continent year lifeExp pop gdpPercap #> #> 1 Algeria Africa 1952 43.1 9279525 2449. #> 2 Algeria Africa 1957 45.7 10270856 3014. #> 3 Algeria Africa 1962 48.3 11000948 2551. #> 4 Algeria Africa 1967 51.4 12760499 3247. #> 5 Algeria Africa 1972 54.5 14760787 4183. #> # … with 619 more rows ``` *Note: the only reason we can read a sheet named “gapminder” (the last example) is because the account we’re logged in as has a Sheet named “gapminder”.* See the article [Find and Identify Sheets](https://googlesheets4.tidyverse.org/articles/articles/find-identify-sheets.html) for more about specifying the Sheet you want to address. See the article [Read Sheets](https://googlesheets4.tidyverse.org/articles/articles/find-identify-sheets.html) for more about reading from specific sheets or ranges, setting column type, and getting low-level cell data. ## Write `gs4_create()` creates a brand new Google Sheet and can optionally send some initial data. ``` r (ss <- gs4_create("fluffy-bunny", sheets = list(flowers = head(iris)))) #> ✓ Creating new Sheet: "fluffy-bunny". #> Spreadsheet name: fluffy-bunny #> ID: 1GtTKAaC0R2WI6gaGitsj1v_IGuDC6n6uVBwk8-aFExg #> Locale: en_US #> Time zone: Etc/GMT #> # of sheets: 1 #> #> (Sheet name): (Nominal extent in rows x columns) #> flowers: 7 x 5 ``` `sheet_write()` (over)writes a whole data frame into a (work)sheet within a (spread)Sheet. ``` r head(mtcars) %>% sheet_write(ss, sheet = "autos") #> ✓ Writing to "fluffy-bunny". #> ✓ Writing to sheet 'autos'. ss #> Spreadsheet name: fluffy-bunny #> ID: 1GtTKAaC0R2WI6gaGitsj1v_IGuDC6n6uVBwk8-aFExg #> Locale: en_US #> Time zone: Etc/GMT #> # of sheets: 2 #> #> (Sheet name): (Nominal extent in rows x columns) #> flowers: 7 x 5 #> autos: 7 x 11 ``` `sheet_append()`, `range_write()`, `range_flood()`, and `range_clear()` are more specialized writing functions. See the article [Write Sheets](https://googlesheets4.tidyverse.org/articles/articles/write-sheets.html) for more about writing to Sheets. ## Where to learn more [Get started](https://googlesheets4.tidyverse.org/articles/googlesheets4.html) is a more extensive general introduction to googlesheets4. Browse the [articles index](https://googlesheets4.tidyverse.org/articles/index.html) to find articles that cover various topics in more depth. See the [function index](https://googlesheets4.tidyverse.org/reference/index.html) for an organized, exhaustive listing. ## Contributing If you’d like to contribute to the development of googlesheets4, please read [these guidelines](https://googlesheets4.tidyverse.org/CONTRIBUTING.html). Please note that the googlesheets4 project is released with a [Contributor Code of Conduct](https://googlesheets4.tidyverse.org/CODE_OF_CONDUCT.html). By contributing to this project, you agree to abide by its terms. ## Privacy [Privacy policy](https://www.tidyverse.org/google_privacy_policy) ## Context googlesheets4 draws on and complements / emulates other packages in the tidyverse: - [googlesheets](https://cran.r-project.org/package=googlesheets) is the package that googlesheets4 replaces. Main improvements in googlesheets4: (1) wraps the current, most modern Sheets API; (2) leaves all “whole file” operations to googledrive; and (3) uses shared infrastructure for auth and more, from the gargle package. The v3 API wrapped by googlesheets is deprecated. [Starting in April/May 2020](https://cloud.google.com/blog/products/g-suite/migrate-your-apps-use-latest-sheets-api), features will gradually be disabled and it’s anticipated the API will fully shutdown in September 2020. At that point, the original googlesheets package must be retired. - [googledrive](https://googledrive.tidyverse.org) provides a fully-featured interface to the Google Drive API. Any “whole file” operations can be accomplished with googledrive: upload or download or update a spreadsheet, copy, rename, move, change permission, delete, etc. googledrive supports Team Drives. - [readxl](https://readxl.tidyverse.org) is the tidyverse package for reading Excel files (xls or xlsx) into an R data frame. googlesheets4 takes cues from parts of the readxl interface, especially around specifying which cells to read. - [readr](https://readr.tidyverse.org) is the tidyverse package for reading delimited files (e.g., csv or tsv) into an R data frame. googlesheets4 takes cues from readr with respect to column type specification. googlesheets4/man/0000755000176200001440000000000014075406470013610 5ustar liggesusersgooglesheets4/man/gs4_token.Rd0000644000176200001440000000217514074074641016001 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/gs4_auth.R \name{gs4_token} \alias{gs4_token} \title{Produce configured token} \usage{ gs4_token() } \value{ A \code{request} object (an S3 class provided by \link[httr:httr-package]{httr}). } \description{ For internal use or for those programming around the Sheets API. Returns a token pre-processed with \code{\link[httr:config]{httr::config()}}. Most users do not need to handle tokens "by hand" or, even if they need some control, \code{\link[=gs4_auth]{gs4_auth()}} is what they need. If there is no current token, \code{\link[=gs4_auth]{gs4_auth()}} is called to either load from cache or initiate OAuth2.0 flow. If auth has been deactivated via \code{\link[=gs4_deauth]{gs4_deauth()}}, \code{gs4_token()} returns \code{NULL}. } \examples{ if (gs4_has_token()) { req <- request_generate( "sheets.spreadsheets.get", list(spreadsheetId = "abc"), token = gs4_token() ) req } } \seealso{ Other low-level API functions: \code{\link{gs4_has_token}()}, \code{\link{request_generate}()}, \code{\link{request_make}()} } \concept{low-level API functions} googlesheets4/man/gs4_deauth.Rd0000644000176200001440000000212014074074641016121 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/gs4_auth.R \name{gs4_deauth} \alias{gs4_deauth} \title{Suspend authorization} \usage{ gs4_deauth() } \description{ Put googlesheets4 into a de-authorized state. Instead of sending a token, googlesheets4 will send an API key. This can be used to access public resources for which no Google sign-in is required. This is handy for using googlesheets4 in a non-interactive setting to make requests that do not require a token. It will prevent the attempt to obtain a token interactively in the browser. The user can configure their own API key via \code{\link[=gs4_auth_configure]{gs4_auth_configure()}} and retrieve that key via \code{\link[=gs4_api_key]{gs4_api_key()}}. In the absence of a user-configured key, a built-in default key is used. } \examples{ if (interactive()) { gs4_deauth() gs4_user() # get metadata on the public 'deaths' spreadsheet gs4_example("deaths") \%>\% gs4_get() } } \seealso{ Other auth functions: \code{\link{gs4_auth_configure}()}, \code{\link{gs4_auth}()} } \concept{auth functions} googlesheets4/man/sheet_properties.Rd0000644000176200001440000000260514075054276017471 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/sheet_properties.R \name{sheet_properties} \alias{sheet_properties} \alias{sheet_names} \title{Get data about (work)sheets} \usage{ sheet_properties(ss) sheet_names(ss) } \arguments{ \item{ss}{Something that identifies a Google Sheet: \itemize{ \item its file id as a string or \code{\link[googledrive:as_id]{drive_id}} \item a URL from which we can recover the id \item a one-row \code{\link[googledrive:dribble]{dribble}}, which is how googledrive represents Drive files \item an instance of \code{googlesheets4_spreadsheet}, which is what \code{\link[=gs4_get]{gs4_get()}} returns } Processed through \code{\link[=as_sheets_id]{as_sheets_id()}}.} } \value{ \itemize{ \item \code{sheet_properties()}: A tibble with one row per (work)sheet. \item \code{sheet_names()}: A character vector of (work)sheet names. } } \description{ Reveals full metadata or just the names for the (work)sheets inside a (spread)Sheet. } \examples{ if (gs4_has_token()) { ss <- gs4_example("gapminder") sheet_properties(ss) sheet_names(ss) } } \seealso{ Other worksheet functions: \code{\link{sheet_add}()}, \code{\link{sheet_append}()}, \code{\link{sheet_copy}()}, \code{\link{sheet_delete}()}, \code{\link{sheet_relocate}()}, \code{\link{sheet_rename}()}, \code{\link{sheet_resize}()}, \code{\link{sheet_write}()} } \concept{worksheet functions} googlesheets4/man/sheet_append.Rd0000644000176200001440000000510714075054276016544 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/sheet_append.R \name{sheet_append} \alias{sheet_append} \title{Append rows to a sheet} \usage{ sheet_append(ss, data, sheet = 1) } \arguments{ \item{ss}{Something that identifies a Google Sheet: \itemize{ \item its file id as a string or \code{\link[googledrive:as_id]{drive_id}} \item a URL from which we can recover the id \item a one-row \code{\link[googledrive:dribble]{dribble}}, which is how googledrive represents Drive files \item an instance of \code{googlesheets4_spreadsheet}, which is what \code{\link[=gs4_get]{gs4_get()}} returns } Processed through \code{\link[=as_sheets_id]{as_sheets_id()}}.} \item{data}{A data frame.} \item{sheet}{Sheet to append to, in the sense of "worksheet" or "tab". You can identify a sheet by name, with a string, or by position, with a number.} } \value{ The input \code{ss}, as an instance of \code{\link{sheets_id}} } \description{ Adds one or more new rows after the last row with data in a (work)sheet, increasing the row dimension of the sheet if necessary. } \examples{ if (gs4_has_token()) { # we will recreate the table of "other" deaths from this example Sheet (deaths <- gs4_example("deaths") \%>\% range_read(range = "other_data", col_types = "????DD")) # split the data into 3 pieces, which we will send separately deaths_one <- deaths[ 1:5, ] deaths_two <- deaths[ 6, ] deaths_three <- deaths[7:10, ] # create a Sheet and send the first chunk of data ss <- gs4_create("sheet-append-demo", sheets = list(deaths = deaths_one)) # append a single row ss \%>\% sheet_append(deaths_two) # append remaining rows ss \%>\% sheet_append(deaths_three) # read and check against the original deaths_replica <- range_read(ss, col_types = "????DD") identical(deaths, deaths_replica) # clean up gs4_find("sheet-append-demo") \%>\% googledrive::drive_trash() } } \seealso{ Makes an \code{AppendCellsRequest}: \itemize{ \item \url{https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#AppendCellsRequest} } Other write functions: \code{\link{gs4_create}()}, \code{\link{gs4_formula}()}, \code{\link{range_delete}()}, \code{\link{range_flood}()}, \code{\link{range_write}()}, \code{\link{sheet_write}()} Other worksheet functions: \code{\link{sheet_add}()}, \code{\link{sheet_copy}()}, \code{\link{sheet_delete}()}, \code{\link{sheet_properties}()}, \code{\link{sheet_relocate}()}, \code{\link{sheet_rename}()}, \code{\link{sheet_resize}()}, \code{\link{sheet_write}()} } \concept{worksheet functions} \concept{write functions} googlesheets4/man/googlesheets4-package.Rd0000644000176200001440000000234714074324207020246 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/googlesheets4-package.R \docType{package} \name{googlesheets4-package} \alias{googlesheets4} \alias{googlesheets4-package} \title{googlesheets4: Access Google Sheets using the Sheets API V4} \description{ \if{html}{\figure{logo.png}{options: align='right' alt='logo' width='120'}} Interact with Google Sheets through the Sheets API v4 . "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. } \seealso{ Useful links: \itemize{ \item \url{https://googlesheets4.tidyverse.org} \item \url{https://github.com/tidyverse/googlesheets4} \item Report bugs at \url{https://github.com/tidyverse/googlesheets4/issues} } } \author{ \strong{Maintainer}: Jennifer Bryan \email{jenny@rstudio.com} (\href{https://orcid.org/0000-0002-6983-2759}{ORCID}) Other contributors: \itemize{ \item RStudio [copyright holder, funder] } } \keyword{internal} googlesheets4/man/gs4_auth_configure.Rd0000644000176200001440000000555714074074641017672 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/gs4_auth.R \name{gs4_auth_configure} \alias{gs4_auth_configure} \alias{gs4_api_key} \alias{gs4_oauth_app} \title{Edit and view auth configuration} \usage{ gs4_auth_configure(app, path, api_key) gs4_api_key() gs4_oauth_app() } \arguments{ \item{app}{OAuth app, in the sense of \code{\link[httr:oauth_app]{httr::oauth_app()}}.} \item{path}{JSON downloaded from Google Cloud Platform Console, containing a client id (aka key) and secret, in one of the forms supported for the \code{txt} argument of \code{\link[jsonlite:fromJSON]{jsonlite::fromJSON()}} (typically, a file path or JSON string).} \item{api_key}{API key.} } \value{ \itemize{ \item \code{gs4_auth_configure()}: An object of R6 class \link[gargle:AuthState-class]{gargle::AuthState}, invisibly. \item \code{gs4_oauth_app()}: the current user-configured \code{\link[httr:oauth_app]{httr::oauth_app()}}. \item \code{gs4_api_key()}: the current user-configured API key. } } \description{ These functions give more control over and visibility into the auth configuration than \code{\link[=gs4_auth]{gs4_auth()}} does. \code{gs4_auth_configure()} lets the user specify their own: \itemize{ \item OAuth app, which is used when obtaining a user token. \item API key. If googlesheets4 is de-authorized via \code{\link[=gs4_deauth]{gs4_deauth()}}, all requests are sent with an API key in lieu of a token. See the vignette \href{https://gargle.r-lib.org/articles/get-api-credentials.html}{How to get your own API credentials} for more. If the user does not configure these settings, internal defaults are used. \code{gs4_oauth_app()} and \code{gs4_api_key()} retrieve the currently configured OAuth app and API key, respectively. } } \examples{ # see and store the current user-configured OAuth app (probaby `NULL`) (original_app <- gs4_oauth_app()) # see and store the current user-configured API key (probaby `NULL`) (original_api_key <- gs4_api_key()) if (require(httr)) { # bring your own app via client id (aka key) and secret google_app <- httr::oauth_app( "my-awesome-google-api-wrapping-package", key = "YOUR_CLIENT_ID_GOES_HERE", secret = "YOUR_SECRET_GOES_HERE" ) google_key <- "YOUR_API_KEY" gs4_auth_configure(app = google_app, api_key = google_key) # confirm the changes gs4_oauth_app() gs4_api_key() # bring your own app via JSON downloaded from Google Developers Console # this file has the same structure as the JSON from Google app_path <- system.file( "extdata", "fake-oauth-client-id-and-secret.json", package = "googlesheets4" ) gs4_auth_configure(path = app_path) # confirm the changes gs4_oauth_app() } # restore original auth config gs4_auth_configure(app = original_app, api_key = original_api_key) } \seealso{ Other auth functions: \code{\link{gs4_auth}()}, \code{\link{gs4_deauth}()} } \concept{auth functions} googlesheets4/man/range_speedread.Rd0000644000176200001440000000774214075054276017224 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/range_speedread.R \name{range_speedread} \alias{range_speedread} \title{Read Sheet as CSV} \usage{ range_speedread(ss, sheet = NULL, range = NULL, skip = 0, ...) } \arguments{ \item{ss}{Something that identifies a Google Sheet: \itemize{ \item its file id as a string or \code{\link[googledrive:as_id]{drive_id}} \item a URL from which we can recover the id \item a one-row \code{\link[googledrive:dribble]{dribble}}, which is how googledrive represents Drive files \item an instance of \code{googlesheets4_spreadsheet}, which is what \code{\link[=gs4_get]{gs4_get()}} returns } Processed through \code{\link[=as_sheets_id]{as_sheets_id()}}.} \item{sheet}{Sheet to read, in the sense of "worksheet" or "tab". You can identify a sheet by name, with a string, or by position, with a number. Ignored if the sheet is specified via \code{range}. If neither argument specifies the sheet, defaults to the first visible sheet.} \item{range}{A cell range to read from. If \code{NULL}, all non-empty cells are read. Otherwise specify \code{range} as described in \href{https://developers.google.com/sheets/api/guides/concepts#a1_notation}{Sheets A1 notation} or using the helpers documented in \link{cell-specification}. Sheets uses fairly standard spreadsheet range notation, although a bit different from Excel. Examples of valid ranges: \code{"Sheet1!A1:B2"}, \code{"Sheet1!A:A"}, \code{"Sheet1!1:2"}, \code{"Sheet1!A5:A"}, \code{"A1:B2"}, \code{"Sheet1"}. Interpreted strictly, even if the range forces the inclusion of leading, trailing, or embedded empty rows or columns. Takes precedence over \code{skip}, \code{n_max} and \code{sheet}. Note \code{range} can be a named range, like \code{"sales_data"}, without any cell reference.} \item{skip}{Minimum number of rows to skip before reading anything, be it column names or data. Leading empty rows are automatically skipped, so this is a lower bound. Ignored if \code{range} is given.} \item{...}{Passed along to the CSV parsing function (currently \code{readr::read_csv()}).} } \value{ A \link[tibble:tibble-package]{tibble} } \description{ This function uses a quick-and-dirty method to read a Sheet that bypasses the Sheets API and, instead, parses a CSV representation of the data. This can be much faster than \code{\link[=range_read]{range_read()}} -- noticeably so for "large" spreadsheets. There are real downsides, though, so we recommend this approach only when the speed difference justifies it. Here are the limitations we must accept to get faster reading: \itemize{ \item Only formatted cell values are available, not underlying values or details on the formats. \item We can't target a named range as the \code{range}. \item We have no access to the data type of a cell, i.e. we don't know that it's logical, numeric, or datetime. That must be re-discovered based on the CSV data (or specified by the user). \item Auth and error handling have to be handled a bit differently internally, which may lead to behaviour that differs from other functions in googlesheets4. } Note that the Sheets API is still used to retrieve metadata on the target Sheet, in order to support range specification. \code{range_speedread()} also sends an auth token with the request, unless a previous call to \code{\link[=gs4_deauth]{gs4_deauth()}} has put googlesheets4 into a de-authorized state. } \examples{ if (gs4_has_token()) { if (require("readr")) { # since cell type is not available, use readr's col type specification range_speedread( gs4_example("deaths"), sheet = "other", range = "A5:F15", col_types = cols( Age = col_integer(), `Date of birth` = col_date("\%m/\%d/\%Y"), `Date of death` = col_date("\%m/\%d/\%Y") ) ) } # write a Sheet that, by default, is NOT world-readable (ss <- sheet_write(chickwts)) # demo that range_speedread() sends a token, which is why we can read this range_speedread(ss) # clean up googledrive::drive_trash(ss) } } googlesheets4/man/gs4_user.Rd0000644000176200001440000000115314074074641015632 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/gs4_auth.R \name{gs4_user} \alias{gs4_user} \title{Get info on current user} \usage{ gs4_user() } \value{ An email address or, if no token has been loaded, \code{NULL}. } \description{ Reveals the email address of the user associated with the current token. If no token has been loaded yet, this function does not initiate auth. } \examples{ gs4_user() } \seealso{ \code{\link[gargle:token-info]{gargle::token_userinfo()}}, \code{\link[gargle:token-info]{gargle::token_email()}}, \code{\link[gargle:token-info]{gargle::token_tokeninfo()}} } googlesheets4/man/googlesheets4-vctrs.Rd0000644000176200001440000000071514074074641020015 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/googlesheets4-package.R, R/gs4_formula.R \name{googlesheets4-vctrs} \alias{googlesheets4-vctrs} \alias{vec_ptype2.googlesheets4_formula} \alias{vec_cast.googlesheets4_formula} \title{Internal vctrs methods} \usage{ \method{vec_ptype2}{googlesheets4_formula}(x, y, ...) \method{vec_cast}{googlesheets4_formula}(x, to, ...) } \description{ Internal vctrs methods } \keyword{internal} googlesheets4/man/range_delete.Rd0000644000176200001440000000633214075054276016524 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/range_delete.R \name{range_delete} \alias{range_delete} \title{Delete cells} \usage{ range_delete(ss, sheet = NULL, range, shift = NULL) } \arguments{ \item{ss}{Something that identifies a Google Sheet: \itemize{ \item its file id as a string or \code{\link[googledrive:as_id]{drive_id}} \item a URL from which we can recover the id \item a one-row \code{\link[googledrive:dribble]{dribble}}, which is how googledrive represents Drive files \item an instance of \code{googlesheets4_spreadsheet}, which is what \code{\link[=gs4_get]{gs4_get()}} returns } Processed through \code{\link[=as_sheets_id]{as_sheets_id()}}.} \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. Ignored if the sheet is specified via \code{range}. If neither argument specifies the sheet, defaults to the first visible sheet.} \item{range}{Cells to delete. There are a couple differences between \code{range} here and how it works in other functions (e.g. \code{\link[=range_read]{range_read()}}): \itemize{ \item \code{range} must be specified. \item \code{range} must not be a named range. \item \code{range} must not be the name of a (work) sheet. Instead, use \code{\link[=sheet_delete]{sheet_delete()}} to delete an entire sheet. Row-only and column-only ranges are especially relevant, such as "2:6" or "D". Remember you can also use the helpers in \code{\link{cell-specification}}, such as \code{cell_cols(4:6)}, or \code{cell_rows(5)}. }} \item{shift}{Must be one of "up" or "left", if specified. Required if \code{range} is NOT a rows-only or column-only range (in which case, we can figure it out for you). Determines whether the deleted area is filled by shifting surrounding cells up or to the left.} } \value{ The input \code{ss}, as an instance of \code{\link{sheets_id}} } \description{ Deletes a range of cells and shifts other cells into the deleted area. There are several related tasks that are implemented by other functions: \itemize{ \item To clear cells of their value and/or format, use \code{\link[=range_clear]{range_clear()}}. \item To delete an entire (work)sheet, use \code{\link[=sheet_delete]{sheet_delete()}}. \item To change the dimensions of a (work)sheet, use \code{\link[=sheet_resize]{sheet_resize()}}. } } \examples{ if (gs4_has_token()) { # create a data frame to use as initial data df <- gs4_fodder(10) # create Sheet ss <- gs4_create("range-delete-example", sheets = list(df)) # delete some rows range_delete(ss, range = "2:4") # delete a column range_delete(ss, range = "C") # delete a rectangle and specify how to shift remaining cells range_delete(ss, range = "B3:F4", shift = "left") # clean up gs4_find("range-delete-example") \%>\% googledrive::drive_trash() } } \seealso{ Makes a \code{DeleteRangeRequest}: \itemize{ \item \url{https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#DeleteRangeRequest} } Other write functions: \code{\link{gs4_create}()}, \code{\link{gs4_formula}()}, \code{\link{range_flood}()}, \code{\link{range_write}()}, \code{\link{sheet_append}()}, \code{\link{sheet_write}()} } \concept{write functions} googlesheets4/man/sheet_rename.Rd0000644000176200001440000000361714075054276016550 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/sheet_rename.R \name{sheet_rename} \alias{sheet_rename} \title{Rename a (work)sheet} \usage{ sheet_rename(ss, sheet = NULL, new_name) } \arguments{ \item{ss}{Something that identifies a Google Sheet: \itemize{ \item its file id as a string or \code{\link[googledrive:as_id]{drive_id}} \item a URL from which we can recover the id \item a one-row \code{\link[googledrive:dribble]{dribble}}, which is how googledrive represents Drive files \item an instance of \code{googlesheets4_spreadsheet}, which is what \code{\link[=gs4_get]{gs4_get()}} returns } Processed through \code{\link[=as_sheets_id]{as_sheets_id()}}.} \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.} \item{new_name}{New name of the sheet, as a string. This is required.} } \value{ The input \code{ss}, as an instance of \code{\link{sheets_id}} } \description{ Changes the name of a (work)sheet. } \examples{ if (gs4_has_token()) { ss <- gs4_create( "sheet-rename-demo", sheets = list(cars = head(cars), chickwts = head(chickwts)) ) sheet_names(ss) ss \%>\% sheet_rename(1, new_name = "automobiles") \%>\% sheet_rename("chickwts", new_name = "poultry") # clean up gs4_find("sheet-rename-demo") \%>\% googledrive::drive_trash() } } \seealso{ Makes an \code{UpdateSheetPropertiesRequest}: \itemize{ \item \url{https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#UpdateSheetPropertiesRequest} } Other worksheet functions: \code{\link{sheet_add}()}, \code{\link{sheet_append}()}, \code{\link{sheet_copy}()}, \code{\link{sheet_delete}()}, \code{\link{sheet_properties}()}, \code{\link{sheet_relocate}()}, \code{\link{sheet_resize}()}, \code{\link{sheet_write}()} } \concept{worksheet functions} googlesheets4/man/range_write.Rd0000644000176200001440000001342214075054276016412 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/range_write.R \name{range_write} \alias{range_write} \title{(Over)write new data into a range} \usage{ range_write( ss, data, sheet = NULL, range = NULL, col_names = TRUE, reformat = TRUE ) } \arguments{ \item{ss}{Something that identifies a Google Sheet: \itemize{ \item its file id as a string or \code{\link[googledrive:as_id]{drive_id}} \item a URL from which we can recover the id \item a one-row \code{\link[googledrive:dribble]{dribble}}, which is how googledrive represents Drive files \item an instance of \code{googlesheets4_spreadsheet}, which is what \code{\link[=gs4_get]{gs4_get()}} returns } Processed through \code{\link[=as_sheets_id]{as_sheets_id()}}.} \item{data}{A data frame.} \item{sheet}{Sheet to write into, in the sense of "worksheet" or "tab". You can identify a sheet by name, with a string, or by position, with a number. Ignored if the sheet is specified via \code{range}. If neither argument specifies the sheet, defaults to the first visible sheet.} \item{range}{Where to write. This \code{range} argument has important similarities and differences to \code{range} elsewhere (e.g. \code{\link[=range_read]{range_read()}}): \itemize{ \item Similarities: Can be a cell range, using A1 notation ("A1:D3") or using the helpers in \code{\link{cell-specification}}. Can combine sheet name and cell range ("Sheet1!A5:A") or refer to a sheet by name (\code{range = "Sheet1"}, although \code{sheet = "Sheet1"} is preferred for clarity). \item Difference: Can NOT be a named range. \item Difference: \code{range} can be interpreted as the \emph{start} of the target rectangle (the upper left corner) or, more literally, as the actual target rectangle. See the "Range specification" section for details. }} \item{col_names}{Logical, indicates whether to send the column names of \code{data}.} \item{reformat}{Logical, indicates whether to reformat the affected cells. Currently googlesheets4 provides no real support for formatting, so \code{reformat = TRUE} effectively means that edited cells become unformatted.} } \value{ The input \code{ss}, as an instance of \code{\link{sheets_id}} } \description{ Writes a data frame into a range of cells. Main differences from \code{\link[=sheet_write]{sheet_write()}} (a.k.a. \code{\link[=write_sheet]{write_sheet()}}): \itemize{ \item Narrower scope. \code{range_write()} literally targets some cells, not a whole (work)sheet. \item The edited rectangle is not explicitly styled as a table. Nothing special is done re: formatting a header row or freezing rows. \item Column names can be suppressed. This means that, although \code{data} must be a data frame (at least for now), \code{range_write()} can actually be used to write arbitrary data. \item The target (spread)Sheet and (work)sheet must already exist. There is no ability to create a Sheet or add a worksheet. \item The target sheet dimensions are not "trimmed" to shrink-wrap the \code{data}. However, the sheet might gain rows and/or columns, in order to write \code{data} to the user-specified \code{range}. } If you just want to add rows to an existing table, the function you probably want is \code{\link[=sheet_append]{sheet_append()}}. } \section{Range specification}{ The \code{range} argument of \code{range_write()} is special, because the Sheets API can implement it in 2 different ways: \itemize{ \item If \code{range} represents exactly 1 cell, like "B3", it is taken as the \emph{start} (or upper left corner) of the targeted cell rectangle. The edited cells are determined implicitly by the extent of the \code{data} we are writing. This frees you from doing fiddly range computations based on the dimensions of the \code{data}. \item If \code{range} describes a rectangle with multiple cells, it is interpreted as the \emph{actual} rectangle to edit. It is possible to describe a rectangle that is unbounded on the right (e.g. "B2:4"), on the bottom (e.g. "A4:C"), or on both the right and the bottom (e.g. \code{cell_limits(c(2, 3), c(NA, NA))}. Note that \strong{all cells} inside the rectangle receive updated data and format. Important implication: if the \code{data} object isn't big enough to fill the target rectangle, the cells that don't receive new data are effectively cleared, i.e. the existing value and format are deleted. } } \examples{ if (gs4_has_token()) { # create a Sheet with some initial, empty (work)sheets (ss <- gs4_create("range-write-demo", sheets = c("alpha", "beta"))) df <- data.frame( x = 1:3, y = letters[1:3] ) # write df somewhere other than the "upper left corner" range_write(ss, data = df, range = "D6") # view your magnificent creation in the browser gs4_browse(ss) # send data of disparate types to a 1-row rectangle dat <- tibble::tibble( string = "string", logical = TRUE, datetime = Sys.time() ) range_write(ss, data = dat, sheet = "beta", col_names = FALSE) # send data of disparate types to a 1-column rectangle dat <- tibble::tibble( x = list(Sys.time(), FALSE, "string") ) range_write(ss, data = dat, range = "beta!C5", col_names = FALSE) # clean up gs4_find("range-write-demo") \%>\% googledrive::drive_trash() } } \seealso{ If sheet size needs to change, makes an \code{UpdateSheetPropertiesRequest}: \itemize{ \item \url{https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#UpdateSheetPropertiesRequest} } The main data write is done via an \code{UpdateCellsRequest}: \itemize{ \item \url{https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#updatecellsrequest} } Other write functions: \code{\link{gs4_create}()}, \code{\link{gs4_formula}()}, \code{\link{range_delete}()}, \code{\link{range_flood}()}, \code{\link{sheet_append}()}, \code{\link{sheet_write}()} } \concept{write functions} googlesheets4/man/gs4_formula.Rd0000644000176200001440000000347114074074641016326 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/gs4_formula.R \name{gs4_formula} \alias{gs4_formula} \title{Class for Google Sheets formulas} \usage{ gs4_formula(x = character()) } \arguments{ \item{x}{Character.} } \value{ An S3 vector of class \code{googlesheets4_formula}. } \description{ In order to write a formula into Google Sheets, you need to store it as an object of class \code{googlesheets4_formula}. This is how we distinguish a "regular" character string from a string that should be interpreted as a formula. \code{googlesheets4_formula} is an S3 class implemented using the \href{https://vctrs.r-lib.org/articles/s3-vector.html}{vctrs package}. } \examples{ if (gs4_has_token()) { dat <- data.frame(x = c(1, 5, 3, 2, 4, 6)) ss <- gs4_create("gs4-formula-demo", sheets = dat) ss summaries <- tibble::tribble( ~desc, ~summaries, "max", "=max(A:A)", "sum", "=sum(A:A)", "min", "=min(A:A)", "sparkline", "=SPARKLINE(A:A, {\"color\", \"blue\"})" ) # explicitly declare a column as `googlesheets4_formula` summaries$summaries <- gs4_formula(summaries$summaries) summaries range_write(ss, data = summaries, range = "C1", reformat = FALSE) miscellany <- tibble::tribble( ~desc, ~example, "hyperlink", "=HYPERLINK(\"http://www.google.com/\",\"Google\")", "image", "=IMAGE(\"https://www.google.com/images/srpr/logo3w.png\")" ) miscellany$example <- gs4_formula(miscellany$example) miscellany sheet_write(miscellany, ss = ss) # clean up gs4_find("gs4-formula-demo") \%>\% googledrive::drive_trash() } } \seealso{ Other write functions: \code{\link{gs4_create}()}, \code{\link{range_delete}()}, \code{\link{range_flood}()}, \code{\link{range_write}()}, \code{\link{sheet_append}()}, \code{\link{sheet_write}()} } \concept{write functions} googlesheets4/man/sheet_add.Rd0000644000176200001440000000550014075054276016022 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/sheet_add.R \name{sheet_add} \alias{sheet_add} \title{Add one or more (work)sheets} \usage{ sheet_add(ss, sheet = NULL, ..., .before = NULL, .after = NULL) } \arguments{ \item{ss}{Something that identifies a Google Sheet: \itemize{ \item its file id as a string or \code{\link[googledrive:as_id]{drive_id}} \item a URL from which we can recover the id \item a one-row \code{\link[googledrive:dribble]{dribble}}, which is how googledrive represents Drive files \item an instance of \code{googlesheets4_spreadsheet}, which is what \code{\link[=gs4_get]{gs4_get()}} returns } Processed through \code{\link[=as_sheets_id]{as_sheets_id()}}.} \item{sheet}{One or more new sheet names. If unspecified, one new sheet is added and Sheets autogenerates a name of the form "SheetN".} \item{...}{Optional parameters to specify additional properties, common to all of the new sheet(s). Not relevant to most users. Specify fields of the \href{https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/sheets#SheetProperties}{\code{SheetProperties} schema} in \code{name = value} form.} \item{.before, .after}{Optional specification of where to put the new sheet(s). Specify, at most, one of \code{.before} and \code{.after}. Refer to an existing sheet by name (via a string) or by position (via a number). If unspecified, Sheets puts the new sheet(s) at the end.} } \value{ The input \code{ss}, as an instance of \code{\link{sheets_id}} } \description{ Adds one or more (work)sheets to an existing (spread)Sheet. Note that sheet names must be unique. } \examples{ if (gs4_has_token()) { ss <- gs4_create("add-sheets-to-me") # the only required argument is the target spreadsheet ss \%>\% sheet_add() # but you CAN specify sheet name and/or position ss \%>\% sheet_add("apple", .after = 1) ss \%>\% sheet_add("banana", .after = "apple") # add multiple sheets at once ss \%>\% sheet_add(c("coconut", "dragonfruit")) # keeners can even specify additional sheet properties ss \%>\% sheet_add( sheet = "eggplant", .before = 1, gridProperties = list( rowCount = 3, columnCount = 6, frozenRowCount = 1 ) ) # get an overview of the sheets sheet_properties(ss) # clean up gs4_find("add-sheets-to-me") \%>\% googledrive::drive_trash() } } \seealso{ Makes a batch of \code{AddSheetRequest}s (one per sheet): \itemize{ \item \url{https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#addsheetrequest} } Other worksheet functions: \code{\link{sheet_append}()}, \code{\link{sheet_copy}()}, \code{\link{sheet_delete}()}, \code{\link{sheet_properties}()}, \code{\link{sheet_relocate}()}, \code{\link{sheet_rename}()}, \code{\link{sheet_resize}()}, \code{\link{sheet_write}()} } \concept{worksheet functions} googlesheets4/man/gs4_fodder.Rd0000644000176200001440000000164114074074641016121 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/gs4_fodder.R \name{gs4_fodder} \alias{gs4_fodder} \title{Create useful spreadsheet filler} \usage{ gs4_fodder(n = 10, m = n) } \arguments{ \item{n}{Number of rows.} \item{m}{Number of columns.} } \value{ A data frame of character vectors. } \description{ Creates a data frame that is useful for filling a spreadsheet, when you just need a sheet to experiment with. The data frame has \code{n} rows and \code{m} columns with these properties: \itemize{ \item Column names match what Sheets displays: "A", "B", "C", and so on. \item Inner cell values reflect the coordinates where each value will land in the sheet, in A1-notation. So the first row is "B2", "C2", and so on. Note that this \code{n}-row data frame will occupy \code{n + 1} rows in the sheet, because the column names occupy the first row. } } \examples{ gs4_fodder() gs4_fodder(5, 3) } googlesheets4/man/sheet_delete.Rd0000644000176200001440000000373314075054276016542 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/sheet_delete.R \name{sheet_delete} \alias{sheet_delete} \title{Delete one or more (work)sheets} \usage{ sheet_delete(ss, sheet) } \arguments{ \item{ss}{Something that identifies a Google Sheet: \itemize{ \item its file id as a string or \code{\link[googledrive:as_id]{drive_id}} \item a URL from which we can recover the id \item a one-row \code{\link[googledrive:dribble]{dribble}}, which is how googledrive represents Drive files \item an instance of \code{googlesheets4_spreadsheet}, which is what \code{\link[=gs4_get]{gs4_get()}} returns } Processed through \code{\link[=as_sheets_id]{as_sheets_id()}}.} \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.} } \value{ The input \code{ss}, as an instance of \code{\link{sheets_id}} } \description{ Deletes one or more (work)sheets from a (spread)Sheet. } \examples{ if (gs4_has_token()) { ss <- gs4_create("delete-sheets-from-me") sheet_add(ss, c("alpha", "beta", "gamma", "delta")) # get an overview of the sheets sheet_properties(ss) # delete sheets sheet_delete(ss, 1) sheet_delete(ss, "gamma") sheet_delete(ss, list("alpha", 2)) # get an overview of the sheets sheet_properties(ss) # clean up gs4_find("delete-sheets-from-me") \%>\% googledrive::drive_trash() } } \seealso{ Makes an \code{DeleteSheetsRequest}: \itemize{ \item \url{https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#DeleteSheetRequest} } Other worksheet functions: \code{\link{sheet_add}()}, \code{\link{sheet_append}()}, \code{\link{sheet_copy}()}, \code{\link{sheet_properties}()}, \code{\link{sheet_relocate}()}, \code{\link{sheet_rename}()}, \code{\link{sheet_resize}()}, \code{\link{sheet_write}()} } \concept{worksheet functions} googlesheets4/man/roxygen/0000755000176200001440000000000013635427346015311 5ustar liggesusersgooglesheets4/man/roxygen/templates/0000755000176200001440000000000013643533245017302 5ustar liggesusersgooglesheets4/man/roxygen/templates/ss-return.R0000644000176200001440000000007313635427346021374 0ustar liggesusers#' @return The input `ss`, as an instance of [`sheets_id`] googlesheets4/man/roxygen/templates/range.R0000644000176200001440000000142213635427346020525 0ustar liggesusers#' @param range A cell range to read from. If `NULL`, all non-empty cells are #' read. Otherwise specify `range` as described in [Sheets A1 #' notation](https://developers.google.com/sheets/api/guides/concepts#a1_notation) #' or using the helpers documented in [cell-specification]. Sheets uses #' fairly standard spreadsheet range notation, although a bit different from #' Excel. Examples of valid ranges: `"Sheet1!A1:B2"`, `"Sheet1!A:A"`, #' `"Sheet1!1:2"`, `"Sheet1!A5:A"`, `"A1:B2"`, `"Sheet1"`. Interpreted #' strictly, even if the range forces the inclusion of leading, trailing, or #' embedded empty rows or columns. Takes precedence over `skip`, `n_max` and #' `sheet`. Note `range` can be a named range, like `"sales_data"`, without #' any cell reference. googlesheets4/man/roxygen/templates/reformat.R0000644000176200001440000000034613640673130021242 0ustar liggesusers#' @param reformat Logical, indicates whether to reformat the affected cells. #' Currently googlesheets4 provides no real support for formatting, so #' `reformat = TRUE` effectively means that edited cells become unformatted. googlesheets4/man/roxygen/templates/skip-read.R0000644000176200001440000000032213635427346021306 0ustar liggesusers#' @param skip Minimum number of rows to skip before reading anything, be it #' column names or data. Leading empty rows are automatically skipped, so this #' is a lower bound. Ignored if `range` is given. googlesheets4/man/roxygen/templates/n_max.R0000644000176200001440000000054413635427346020537 0ustar liggesusers#' @param n_max Maximum number of data rows to parse into the returned tibble. #' Trailing empty rows are automatically skipped, so this is an upper bound on #' the number of rows in the result. Ignored if `range` is given. `n_max` is #' imposed locally, after reading all non-empty cells, so, if speed is an #' issue, it is better to use `range`. googlesheets4/man/sheets_id.Rd0000644000176200001440000000621514075052077016052 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/sheets_id-class.R \name{sheets_id} \alias{sheets_id} \alias{as_sheets_id} \title{\code{sheets_id} class} \usage{ as_sheets_id(x, ...) } \arguments{ \item{x}{Something that contains a Google Sheet id: an id string, a \code{\link[googledrive:as_id]{drive_id}}, a URL, a one-row \code{\link[googledrive:dribble]{dribble}}, or a \code{googlesheets4_spreadsheet}.} \item{...}{Other arguments passed down to methods. (Not used.)} } \description{ \code{sheets_id} is an S3 class that marks a string as a Google Sheet's id, which the Sheets API docs refer to as \code{spreadsheetId}. Any object of class \code{sheets_id} also has the \code{\link[googledrive:as_id]{drive_id}} class, which is used by \link{googledrive} for the same purpose. This means you can provide a \code{sheets_id} to \link{googledrive} functions, in order to do anything with your Sheet that has nothing to do with it being a spreadsheet. Examples: change the Sheet's name, parent folder, or permissions. Read more about using \link{googlesheets4} and \link{googledrive} together in \code{vignette("drive-and-sheets")}. Note that a \code{sheets_id} object is intended to hold \strong{just one} id, while the parent class \code{drive_id} can be used for multiple ids. \code{as_sheets_id()} is a generic function that converts various inputs into an instance of \code{sheets_id}. See more below. When you print a \code{sheets_id}, we attempt to reveal the Sheet's current metadata, via \code{\link[=gs4_get]{gs4_get()}}. This can fail for a variety of reasons (e.g. if you're offline), but the input \code{sheets_id} is always revealed and returned, invisibly. } \section{\code{as_sheets_id()}}{ These inputs can be converted to a \code{sheets_id}: \itemize{ \item Spreadsheet id, "a string containing letters, numbers, and some special characters", typically 44 characters long, in our experience. Example: \verb{1qpyC0XzvTcKT6EISywvqESX3A0MwQoFDE8p-Bll4hps}. \item A URL, from which we can excavate a spreadsheet or file id. Example: \code{"https://docs.google.com/spreadsheets/d/1BzfL0kZUz1TsI5zxJF1WNF01IxvC67FbOJUiiGMZ_mQ/edit#gid=1150108545"}. \item A one-row \code{\link[googledrive:dribble]{dribble}}, a "Drive tibble" used by the \link{googledrive} package. In general, a \code{dribble} can represent several files, one row per file. Since googlesheets4 is not vectorized over spreadsheets, we are only prepared to accept a one-row \code{dribble}. \itemize{ \item \code{\link[googledrive:drive_get]{googledrive::drive_get("YOUR_SHEET_NAME")}} is a great way to look up a Sheet via its name. \item \code{\link[=gs4_find]{gs4_find("YOUR_SHEET_NAME")}} is another good way to get your hands on a Sheet. } \item Spreadsheet meta data, as returned by, e.g., \code{\link[=gs4_get]{gs4_get()}}. Literally, this is an object of class \code{googlesheets4_spreadsheet}. } } \examples{ \dontshow{if (gs4_has_token()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} mini_gap_id <- gs4_example("mini-gap") class(mini_gap_id) mini_gap_id as_sheets_id("abc") \dontshow{\}) # examplesIf} } \seealso{ \link[googledrive:as_id]{googledrive::as_id} } googlesheets4/man/sheet_write.Rd0000644000176200001440000001006214075054276016423 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/sheet_write.R \name{sheet_write} \alias{sheet_write} \alias{write_sheet} \title{(Over)write new data into a Sheet} \usage{ sheet_write(data, ss = NULL, sheet = NULL) write_sheet(data, ss = NULL, sheet = NULL) } \arguments{ \item{data}{A data frame. If it has zero rows, we send one empty pseudo-row of data, so that we can apply the usual table styling. This empty row goes away (gets filled, actually) the first time you send more data with \code{\link[=sheet_append]{sheet_append()}}.} \item{ss}{Something that identifies a Google Sheet: \itemize{ \item its file id as a string or \code{\link[googledrive:as_id]{drive_id}} \item a URL from which we can recover the id \item a one-row \code{\link[googledrive:dribble]{dribble}}, which is how googledrive represents Drive files \item an instance of \code{googlesheets4_spreadsheet}, which is what \code{\link[=gs4_get]{gs4_get()}} returns } Processed through \code{\link[=as_sheets_id]{as_sheets_id()}}.} \item{sheet}{Sheet to write into, in the sense of "worksheet" or "tab". You can identify a sheet by name, with a string, or by position, with a number.} } \value{ The input \code{ss}, as an instance of \code{\link{sheets_id}} } \description{ This is one of the main ways to write data with googlesheets4. This function writes a data frame into a (work)sheet inside a (spread)Sheet. The target sheet is styled as a table: \itemize{ \item Special formatting is applied to the header row, which holds column names. \item The first row (header row) is frozen. \item The sheet's dimensions are set to "shrink wrap" the \code{data}. } If no existing Sheet is specified via \code{ss}, this function delegates to \code{\link[=gs4_create]{gs4_create()}} and the new Sheet's name is randomly generated. If that's undesirable, call \code{\link[=gs4_create]{gs4_create()}} directly to get more control. If no \code{sheet} is specified or if \code{sheet} doesn't identify an existing sheet, a new sheet is added to receive the \code{data}. If \code{sheet} specifies an existing sheet, it is effectively overwritten! All pre-existing values, formats, and dimensions are cleared and the targeted sheet gets new values and dimensions from \code{data}. This function goes by two names, because we want it to make sense in two contexts: \itemize{ \item \code{write_sheet()} evokes other table-writing functions, like \code{readr::write_csv()}. The \code{sheet} here technically refers to an individual (work)sheet (but also sort of refers to the associated Google (spread)Sheet). \item \code{sheet_write()} is the right name according to the naming convention used throughout the googlesheets4 package. } \code{write_sheet()} and \code{sheet_write()} are synonyms and you can use either one. } \examples{ if (gs4_has_token()) { df <- data.frame( x = 1:3, y = letters[1:3] ) # specify only a data frame, get a new Sheet, with a random name ss <- write_sheet(df) read_sheet(ss) # clean up googledrive::drive_trash(ss) # create a Sheet with some initial, placeholder data ss <- gs4_create( "sheet-write-demo", sheets = list(alpha = data.frame(x = 1), omega = data.frame(x = 1)) ) # write df into its own, new sheet sheet_write(df, ss = ss) # write mtcars into the sheet named "omega" sheet_write(mtcars, ss = ss, sheet = "omega") # get an overview of the sheets sheet_properties(ss) # view your magnificent creation in the browser gs4_browse(ss) # clean up gs4_find("sheet-write-demo") \%>\% googledrive::drive_trash() } } \seealso{ Other write functions: \code{\link{gs4_create}()}, \code{\link{gs4_formula}()}, \code{\link{range_delete}()}, \code{\link{range_flood}()}, \code{\link{range_write}()}, \code{\link{sheet_append}()} Other worksheet functions: \code{\link{sheet_add}()}, \code{\link{sheet_append}()}, \code{\link{sheet_copy}()}, \code{\link{sheet_delete}()}, \code{\link{sheet_properties}()}, \code{\link{sheet_relocate}()}, \code{\link{sheet_rename}()}, \code{\link{sheet_resize}()} } \concept{worksheet functions} \concept{write functions} googlesheets4/man/googlesheets4-configuration.Rd0000644000176200001440000000511714074074641021524 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/googlesheets4-package.R, R/utils-ui.R \name{googlesheets4-configuration} \alias{googlesheets4-configuration} \alias{local_gs4_quiet} \alias{with_gs4_quiet} \title{googlesheets4 configuration} \usage{ local_gs4_quiet(env = parent.frame()) with_gs4_quiet(code) } \arguments{ \item{env}{The environment to use for scoping} \item{code}{Code to execute quietly} } \description{ Some aspects of googlesheets4 behaviour can be controlled via an option. } \section{Messages}{ The \code{googlesheets4_quiet} option can be used to suppress messages from googlesheets4. By default, googlesheets4 always messages, i.e. it is \emph{not} quiet. Set \code{googlesheets4_quiet} to \code{TRUE} to suppress messages, by one of these means, in order of decreasing scope: \itemize{ \item Put \code{options(googlesheets4_quiet = TRUE)} in a start-up file, such as \code{.Rprofile}, or in your R script \item Use \code{local_gs4_quiet()} to silence googlesheets4 in a specific scope \item Use \code{with_gs4_quiet()} to run a small bit of code silently } \code{local_gs4_quiet()} and \code{with_gs4_quiet()} follow the conventions of the the withr package (\url{https://withr.r-lib.org}). } \section{Auth}{ Read about googlesheets4's main auth function, \code{\link[=gs4_auth]{gs4_auth()}}. It is powered by the gargle package, which consults several options: \itemize{ \item Default Google user or, more precisely, \code{email}: see \code{\link[gargle:gargle_options]{gargle::gargle_oauth_email()}} \item Whether or where to cache OAuth tokens: see \code{\link[gargle:gargle_options]{gargle::gargle_oauth_cache()}} \item Whether to prefer "out-of-band" auth: see \code{\link[gargle:gargle_options]{gargle::gargle_oob_default()}} \item Application Default Credentials: see \code{\link[gargle:credentials_app_default]{gargle::credentials_app_default()}} } } \examples{ if (gs4_has_token()) { # message: "Creating new Sheet ..." (ss <- gs4_create("gs4-quiet-demo", sheets = "alpha")) # message: "Editing ..., Writing ..." range_write(ss, data = data.frame(x = 1, y = "a")) # suppress messages for a small amount of code with_gs4_quiet( ss \%>\% sheet_append(data.frame(x = 2, y = "b")) ) # message: "Writing ..., Appending ..." ss \%>\% sheet_append(data.frame(x = 3, y = "c")) # suppress messages until end of current scope local_gs4_quiet() ss \%>\% sheet_append(data.frame(x = 4, y = "d")) # see that all the data was, in fact, written read_sheet(ss) # clean up gs4_find("gs4-quiet-demo") \%>\% googledrive::drive_trash() } } googlesheets4/man/gs4_create.Rd0000644000176200001440000000411714074074641016122 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/gs4_create.R \name{gs4_create} \alias{gs4_create} \title{Create a new Sheet} \usage{ gs4_create(name = gs4_random(), ..., sheets = NULL) } \arguments{ \item{name}{The name of the new spreadsheet.} \item{...}{Optional spreadsheet properties that can be set through this API endpoint, such as locale and time zone.} \item{sheets}{Optional input for initializing (work)sheets. If unspecified, the Sheets API automatically creates an empty "Sheet1". You can provide a vector of sheet names, a data frame, or a (possibly named) list of data frames. See the examples.} } \value{ The input \code{ss}, as an instance of \code{\link{sheets_id}} } \description{ Creates an entirely new (spread)Sheet (or, in Excel-speak, workbook). Optionally, you can also provide names and/or data for the initial set of (work)sheets. Any initial data provided via \code{sheets} is styled as a table, as described in \code{\link[=sheet_write]{sheet_write()}}. } \examples{ if (gs4_has_token()) { gs4_create("gs4-create-demo-1") gs4_create("gs4-create-demo-2", locale = "en_CA") gs4_create( "gs4-create-demo-3", locale = "fr_FR", timeZone = "Europe/Paris" ) gs4_create( "gs4-create-demo-4", sheets = c("alpha", "beta") ) my_data <- data.frame(x = 1) gs4_create( "gs4-create-demo-5", sheets = my_data ) gs4_create( "gs4-create-demo-6", sheets = list(chickwts = head(chickwts), mtcars = head(mtcars)) ) # clean up gs4_find("gs4-create-demo") \%>\% googledrive::drive_trash() } } \seealso{ Wraps the \code{spreadsheets.create} endpoint: \itemize{ \item \url{https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/create} } There is an article on writing Sheets: \itemize{ \item \url{https://googlesheets4.tidyverse.org/articles/articles/write-sheets.html} } Other write functions: \code{\link{gs4_formula}()}, \code{\link{range_delete}()}, \code{\link{range_flood}()}, \code{\link{range_write}()}, \code{\link{sheet_append}()}, \code{\link{sheet_write}()} } \concept{write functions} googlesheets4/man/gs4_random.Rd0000644000176200001440000000067614074074641016145 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/gs4_create.R \name{gs4_random} \alias{gs4_random} \title{Generate a random Sheet name} \usage{ gs4_random(n = 1) } \arguments{ \item{n}{Number of names to generate.} } \value{ A character vector. } \description{ Generates a random name, suitable for a newly created Sheet, using \code{\link[ids:adjective_animal]{ids::adjective_animal()}}. } \examples{ gs4_random() } googlesheets4/man/gs4_examples.Rd0000644000176200001440000000230114075147074016470 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/gs4_example.R \name{gs4_examples} \alias{gs4_examples} \alias{gs4_example} \title{Example Sheets} \usage{ gs4_examples(matches) gs4_example(matches) } \arguments{ \item{matches}{A regular expression that matches the name of the desired example Sheet(s). \code{matches} is optional for the plural \code{gs4_examples()} and, if provided, it can match multiple Sheets. The singular \code{gs4_example()} requires \code{matches} and it must match exactly one Sheet.} } \value{ \itemize{ \item \code{gs4_example()}: a \link{sheets_id} \item \code{gs4_examples()}: a named vector of all built-in examples, with class \code{\link[googledrive:as_id]{drive_id}} } } \description{ googlesheets4 makes a variety of world-readable example Sheets available for use in documentation and reprexes. These functions help you access the example Sheets. See \code{vignette("example-sheets", package = "googlesheets4")} for more. } \examples{ \dontshow{if (gs4_has_token()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} gs4_examples() gs4_examples("gap") gs4_example("gapminder") gs4_example("deaths") \dontshow{\}) # examplesIf} } googlesheets4/man/figures/0000755000176200001440000000000013635427346015262 5ustar liggesusersgooglesheets4/man/figures/lifecycle-defunct.svg0000644000176200001440000000170413635427346021372 0ustar liggesuserslifecyclelifecycledefunctdefunct googlesheets4/man/figures/lifecycle-maturing.svg0000644000176200001440000000170613635427346021572 0ustar liggesuserslifecyclelifecyclematuringmaturing googlesheets4/man/figures/logo.png0000644000176200001440000010366713635427346016745 0ustar liggesusersPNG  IHDRX?gAMA a cHRMz&u0`:pQ<bKGDtIME%ebIDATxw|wiɖĉGބaQZ:蠻t(I)I٣ BH ;N{owlٱۑd9ztsyJZ=W'qi:aI#"  77\SN8| xx0Fю7Jчgjwo1"N=Q ځ뀯xh #E"|0`- Gƞ(@q 1Dh #rc E17K"NcD^=(Eyc+nP8ۀƈ}{"Q ޅ pP%to wHm{Q>r>yȜ2sU8Р0)Qb6'GcD=F&_z.I$=ѪNCM* K2 w2#r '?@]Gc?":0 vԪ{^"(1=YaaaX6#2_:a#WB`|ad =:;jUޫQA:$Z%g+VCby06Dq9 p"rH%pI 'Z44]ѧLv̒Lʼn feq o1"Gc3!lD ImTߠV{,΃IҰ1"c3fI^}Аmy5*n} MXb$ЇI[\XbD06aYWgWJjf(؍֏#Y 1I)x5(Z4Tzpy8L׏# lVUƻ>֫tf&'),0+wXb16!@zn[gkjM]:42Ja0?@EB~<&Y i~pvyiôE Ӓb73R04?YA%d8+ 2e`24! ^<C[pIGT ;4ȶ [aB0<ߴ1 c5H*OB.x(<Um< T=#}Bp  We!p㱴ų,N¦2xn?\OAGFA;bVB`N$7Lrnu,m1bЇ԰dI{}~x| 8VFqB4x|ty7Q9pdǁǁ1"Y!KZCQh⌳nTt3Ӄ%9Pe«bp8~ci،!i~M]1x qY47q-,AKPyVX.V^π i~n{kALKLL`Wʕ[]Z?O[,N'laK[ ->34?_`=<-./Þ㿝feLlYȓSt2L"-CWKYlG3:}Զv:[|H=j,m1e"A֕‹DPpCJqޑSÑM?.N˰Nci||NAU-ZB`UCfں΢. uSSX=/)N[<ﲟ4k14?_I#<6gg&I Q $Wq{Cp:X8n(¤[0L[<.i~x0u3*:2X,&|Zo^X1,t2?ti䝅%p4B$d ң,I9|:p?8'\W Ayq3s‘淳Zs?JDglBKԶ*3?mqfЏg0ͳL[P8x.L$xpF/Xi>ು6,ͅ''ci`E" R>˕tHu}g9)vbXbx_C]eBgaH$ cutMcXCDc0|g{Ц-4GݨCþ:xv" b4 K"ڈwU jk- F g1EXcutYIGuQ5Pv$(WDP}M)yHtmJ5{(J0ebYF8|rmF)cIB׵a׆AiCn<&1GHC쁵UIAbt6] NwcAV6д rwpӲ\r2ra2nhgx+ys1S.'e-l:H@[ i"m1O1RRuM<ȟxxݍ=wʐI,Iخp5Vq@q09ͅ`-F‘wwޫ.>ԓ|ObIF#Mt! LAoliT-FhB I8>U"vQZ*IZ%}+Oo/dwy J,bQOç>r,Kw4M?lk 7 qD}k]Hg#S<(އy62.Lxӹ8y;QC6%Oׄ,y^d7*++~Gڰ,lqH4|_{p CWMM5~ZC\AҁZdZ4Xx.f'{i#vpm;;1n~Qa̒c/߿LjZoUeE_|`J_`$.IHՏ}?~xͽ?}="I2!p?0Ƃa~@7)B?w-w"2o@.Qx\[q* X<::U) 㚻P1誯WGW}dNy<ٺeK\x({u.^TF֋xF}E KХJTwøOflHCD V"9j+K+QCdeH# 1d|'s꽏?V"")i3tGE2xhiiAӄѪ ΚR3v$Qu$Jų,}>.ޙʠ}x XU:dh%qi~{^1R )E\ RŅdeSEy9olGa]ױdv}LbU4rT%GZNOq+m1tA< Վw Y&G>b3BXZ0wj2mm8=h^O- FPbHIN>lŤ<](f;Ix Io 6f8? ʁ;I[ŵzHH|VK_%R&HV=wXhU#s8skc.\w&4<7nٌb{Q. jJ|3@6AF +O,+aYưe`10ŵY-k:‘y4ߘwU`PRHp:WUVH-ٳmy+*زe+ 0M&2}|Bx${? )"ibu p "x_g#Va|j_T70NSP{7zˮ;(sɎg>KHʹ4cߠ0:Ϡw!G0&Nh>L[Lt-ȃBIC$oWnuUMr. (}4L޸qiVRfcqadYƠ(B>{Ui в&?afC5x'>ǃG.BIDx5(xRoze1 2`N~?TF a6fuQp8p{G*+9T煙βGheԅ9r ʀ#Xf7ƫ"ßvºpc-""/#(mLbuDސi觛_wb'7*Nccꨦ+.Gu$ICʩ`+^CFrǃf"nȊHѼi]Mp-f"|r;zrAvH:_ނ׏YtWDô H@[? >Ç`'"Kt6qz}tݧʡjRh;W^BW%N9 _ mХ-'V :i ѸJ0ha8yQlFرcL6 Mw$] ?O0)uKgk3W^y%'NԔdgD@Hx/tQB녓-}E@-rU7o7 9pCSD+e̒ruQ6T4@Bb' ;$ɨ^7yop8$uYW ;>`I3_M<?|6 O( =w㰻ﯛP_dcta1fqz3./z*44}1XM|swŞ={1}: u8T/U|=$R^UH[~Zi@Y_B(łЇM(atbW9t0N YUUIfK\KX3TYD݆]̭f7fݺu7ȸ+"ޑ*#Eg;ª#AYu[ N[h@dK Ln~GƸМ/bWIJ jmK. 233)//eN_'Ux0Z:Qϻ1Ox=~6&MnG$:;;୷c/n"|mtVyՑq!@z`!Am;c's-ZK $d]Ÿ۫_%m$ uɹ'Ox0Wg珈:TKLgJ~Y&Z;aٲe(JoCdp0i$<^R.=xf&lKmqP@7EQh",F@ ۫fFo&LٌDy%8 zU M$sR{(v<ԩS{}pa***cʔ)Cxxa͘HܔQUypmмb Y\^ 6>~|FKy ^:-A3)>VP\[ZbӦML4kAIy8ltMPx$I0o)=x ?OyIKKC4222Maa!?Oرrʟ 3Q #~5B8\jヰ. rBķᅦbe Z#7q *wΊ2jM^osA^/$&8ʐ>Y4iyyy sJr ݴvK#pDra{#(0aHU"45iǞ?Շ.hrD:R_tt}҃JuCo/qUW'OrwcǎqyrJ|Ij ?0K[% S=E%jX9zhyF#7Y`AwMh\~s-t蠺@!"|.Q4-FۘuȜ6X]vH@WW?j{ߏxyGv4*vZ^b)Sxgx79q\|L48?Nc/C5aGC({GFə# T aEM5w>[nu'xױI&aX(**RD438)/;q:ln7~z*++iiipAtvO[=h{1* =Q1ѐ?X1_fzxf͚=9lBkkic4눉fa2h.$$t]ŜG<rGOb޽{cXp:8Nhhh`ӻ`J64Iv3]0FD ƨ%0]X2aHyTGٴAzj/sw?5:z+z+N& I1zg(&{LBKK : UUUdggsy瑗@e<f?hB5wa4TFƈ5"}h{v"}x6,ܭX477tKY`wrjn'? gpI:Ƹn_M#GJf3+ijj ˦wW_%a-$_1=*,zI &0~}8/=pmI*ɠr( xGxx7$66iӦq-pea 3~ y31{G:~ GOO;wSRRB[[vr&NȆ 8#nSdk,nWdhy5`zC҇nSl@l:C2cƌ~O1sLxn^/v^a{aۛH!$IF|H˿%uU/w?Ə#-=͆bUVe|V'i+bO@4<>CDG]J/B7`1&1=2"زY/MN l6㡹C҂`0:}7N%p>֏L1 "` Ė~u11hjra[Ś],L꺆7Hb48gܫdƬ`]J/|oF͛wʎ ʑ#GصkwTWWсhj8r͝>]H,Mbp$v4VruBܝ(B\\?Gy1g{ݢKGw_Ȫ=({qIaїxru1w\:;;پ};:MMMFrrrX`UUU΢EPgzǛ0+ +h&uu$JJ0aD\S2UD;W+' Q 9E` LFD?_zݸ\X,V\Iuu5111LEf!]9$ۥ:"983D?Uu SX,v @Z־'55I[[fVJ]ѕ:V,%P,rF& I&ɶmXx16[ ]zGrx${]܌R2oUK 3Q>a sbN|!Yێ(466DZG)q?RFDrBAA^nAu>O-jrܫ-6nbԈԚ'#i+Wj5Ҧomʫf…vX*|$%u11y7e̙2^4Ջl4?(v#W轾?W} ~+b7Eީ7o%%%$pʎn<'YQ+F4ROVM7|.JWGk]1!H #Ĉ-k%%5m۶OUUUU(zmHQtL1YhhlX, =Wj(+txhÈ }",>ڰ k[`P08p]um砧h!)%)S&o>dYl`1*hdfMg+#o Q4ʅ Sv9|S0.6h2 V 0elBZZ>E'MPkluds I)lٲs"2&)IT% .vH'=,(]wOg`r(HR}i1H3o&PXX(sSWjűa{ RSSE& ɀMdGI-ڟ`{&5?j>#.e{M@Cl1(Ѐ :H34fJyu-ӦMlNc:SӸ+Yz5&b,ݟ2GҸ°0gv)jxQ`0Mi_WS"xNS``R^^NZZ555̚5WvQug~I&;(nW`\ :bGɑjjj@$q@5ph.'!qKw&Oz9ywp)Jp~ 6^!.弄 HDD1>2:ـ= f=*GmM)n M^Nf@3x;}-g~Su0Xl7 󵻾"..$|BsY8xɓ'GRr nKĥ9 SFk@lv!P;ó2}QEWcl5`v< `Ȯ6M ,O~{xe4VdLF,dd8qMӘ1ci軏 DMtt4uWd1 C&p$W~*Dh>72QjA0PaM8vɲeo1ovFUu W^} +.˯+>8#8ӳ1 Q\\ @Zj* ]ׄX:%Yfu)(YIaC@$Wjg9K )».\MgL+7`Gyyyܹ믿ĤDb-lQkM&)).VYڠG`¥y袋bwrr2FOσOihnV\z)uuu\'鲦*FeoE#od0 p?%Nxg袋x">/Y^&Fֳ`D%4BR dX,m3!օ«JT0nj4Oyu zln8BIzHva~#V*tPݑ~5[̥Cr:j?t:QSX#2lGACuQe4 ܍:xǣ:`6Ty#_{!p M !TC_G#ePVYkbҤIǓ܋-.)? 9`0 ><ˌ,xtMcܸql8cxi:W\jĉ/=W@LҐDT2gS*?6¾Y ,D_VSS8زc/&ii(=tsbv܌ FM[HKL1 qjX-u nlBNv6n,qHTJ7PF.IWï̎djұx@&14S__ɓ'$ لIuq{:Ilb +W UUdD t$$t"K[nv* WbD6d0)F{x:uREeSbnO'q-Vb:v?ġ8rVFj*^IScǎttt2-3A@䕯]C C"GQj wnmD=Z;H0" &,BjJ[V> B=#t {R:FaQHOSW7誗 6t<%%AGcdT齁Mh ԏ<&WS`cL)1ݻ#8UE3^bgTO,:J*caΜ9x<jjjS}Ne$&&rX`0L&p8+NFlW6,8s9x0W&%%ʸdh,6Z&fRXX3U)M )&̎d.F#k֬$sL,&L`2ʤ,19Pހ( rᣋUperc㻛:u HDqA>4ZpI6{5 5x VPl'm())Ddee%!!`d0Dڥ[j?Y!FNt"J\M>.t;Hǔ)SC'cn;!(F+455u?|֓x[$Z-v=ʪU+شi55d$ءd jȑ#̞=08?#( 0ч=)/+cÆ ʄ HZ2xttEZqﮢcP$ ^z%?|9v&N0?k@Σ>J^^(L3)bǎ;B. ,׽BK~1:6c5m&..EQHK ס@[xRؾ{ף:YdxjFL+IӦYh%%%%'A[ Hx},]&$Y¨H[pTEߙ6 y]@6d ;ӧNχ=6,'4"x.g0#YDBDd@Ћ~LgL-Vx+Baa!Md/$EDkc=vN<ɢEaNKMeҊq`qilbg_Nl< ݉[+_KWtflN:UXlJJJ;g6fYuA#fs8DT` oդ>k zG3 AzL9 _BCK7opPQQ/pq4Q0}$222ui)'gbr/fɭ_}!eucM`ҙ:me2Ptm uٽ<1 ߟRUE& S:EÇ+߶q{?3[8t䨿*G54771mr1uxݝ. lJKKl̚6Ӳ-=HfLb(Esgƴ9NKfN.]t!555;躎(b:h$%%ݻwEQw)D up|&i_ Z '&g߸buF1p$gij嗘;w.$%%9gQ{33i;w7D84/vW5Ak] b)Zz~&&prVZE\\$nSsip`3z+ʊ|Dhpn`\M͕g ~K ۿNڄ&iBE;ٳ &Ɍp;4Mƌ%!!ٳK:qEhJ'jiKCQY}h:1LL6 d=6mĒ%KeUBD李ecOInƹC"jҼBgW݇wc$-7%~Xu=eO˯l63|&L]렽Ț2fZZZ2X4g&Ah5Ss bgF.VkOϥΎ;˻ƶ5@1,Xq饘f:::̀^wE=(u&{awz.OkcUv)\t|]qhf;-Y{pS<>c1Y,dͤqX!IW\q9ց(8s0Zm/I?C8(--741Ń,Sbƍx^ƍ^EwW&;3ŹI`4wٟ>zz]I84C\Rl- IbځI>Ci2{Ҭ:w:|&̻J].3h:AFLbbldf.[S[~tƗ/a8*ONNZ`K}yi>a%~7=ۅ8‹z0 nC^Ma }Q vpͷC\Jil$!?;o>ˬY=WK+vܹ+hii{l,W._zq#+$I9Ĭ|dw[6׵kX|хLJf 6gҥKu,e4Gy JVǕ's6=ȭX x: f| =-ud́(Cog9sE} [toH{%\1$Lތż#;$''1Ԗx?>o{nfΜɵ]|m(Xp!^0o Ͽ#E{1 =4j)8f&..V͉Rq&$KW4}şeh)ъg5Hh&;'S1Ō#}4j@U}$fl6zƣMtα]t~H\7\w-ZM;Gb!W 9$'K/ގdbi :%2n YfM!*%%}l!+N"ûH͟NMdffD`Ɩv )>!صml#&&LĉN_e7B~*܃o4VkI$Ryh-5,Y0 @kK+m]>bR@˫ ICX73C{nӓDT޷ɻXd!K?%#klpdR[zwGx9n\)W֯pxĵ߁]ӯ< y‡gFdͥ% ϽV3zs Pz8]8gkcCv'J&eGL܄ʯA E]ϰb4z=cb4V&Ϧ0Eh8ƙK[C5|>{;oOz8JÅ'5ρ~>+!ī,W0A `9f3qҮ69j%9V'$%%~4MCVoo|y+x_Tw {]sNd+ MMM7nCH/Nk]%ޮNqN,$nί_d=*RCN )դ!'8T4U&@|$C|Ds3o"R'L:h"][[Tϟ3?}-_bÑ~&16ظq#`0t?DMuAJJJ<'Nw &(ҝАK)TމK?dz|$gUcW3=^*dˑGA&aRr.$Yleފ4婩0g-ԣn`A}o\%`*njODcͻlNw InO(,&0n2^Yg3fk|,M//rm8 dzsNfΜI{{;NII Na^~y% !`JGV 8ѣ3DxŊb%9#2{)=K3%ˉo.·CFL,6;Y4r:(\|)vk7ՒQ8clk;yclK‡2邽Q;ǙqμYZɝ TֺJR<ؖ}, y!4(5\x,p)7.1Oo|'v؁.R>(/+^DKȢ[$+%+`u+pW|vz267Hɢ{R:.ҭkacH6? Ȟ6=[|ji stMN2gv\]So+iPyp'I9i>IF,OĺE~"ϝ܍;I~GL;ͼ>GIhFr^ e%t47`OLlSuxp9||Wgi 9BY<9$q 7tk*K`IE|λͯK&N@yy9K.&??Mظq#m<-[BRF7K\rssillr1s d 4GJ&MI_ݙ\%v߆ kv2cŭ0{$;}1bwBŁdMCZ*Q}^Zl$] O ۨ/L`Y< :;;YnQTXYTA)=RIFw5CTX#-kHyI4-sibdLO&`U< $HϔBطCE3*" 1i 3$DU!a׳\oWb0H >-Švp4=eF%r2gjiDSx\7ԐM("}rzaL&3_~96?v<7_zV{\7AtM_@V,ln66m&6%۾G&$IpR{z4v;I$\܉.iӈ߶w'h:ۙpHURIHGbSqdZWIFt*n'x D (ZO<{GEVh $A Hla4Վ*W3;;c FYQw'1x51>1x;]"50LCuv5Z[ZhmmfqE(~il\+n镏iLdOEYh$IEW;cbl'`bAwwݧhJF4TwDž\GFV+A1 Gj6 lkFes'rx󛤌@J|MT5UBr3fx}>1<@ vh=a>7+?cK.duosv% YT &KKl\[ 9o"qYz5֭'o(>Cgkk" ϗ-4ؙ/{JKKa2)9bOH|62'IBm{iR-Lv@K0Ym$b0ɞ2#{183pwO  GG!ӎSA.|go]9 bItO ]?bKHzc%+2/-tpǿ9̼O[3̴QS]h$>1Gre{a4[qz}\y/0[chDRHR^0ۍ;}HOđ3)*hr}HgB'B$DKn%ys\MJ|ҳBΔ9} Ɓ7: CpqѲc ARr/DGB1qfaő5u>, yAHD֤ٴ7ZW=!YKlsDB?r #dzu2$Ն躏^85;U$`4uͧ_={G[[ЅSl iL8¢c@y+؅ rQw M4UHRyp!+BeP-f"P7]"hq )Xo9%`;e-yPh¸qLEEE9.CaZ󳟦d5ԇuɄ&bd$)OEa}{Aə5ܮF⧁xy Yq (xքu,Әbbv&9+<3=zr-^̢Ixɐ}CQdTUFL>fHW<>`ĞL]Si?ڨFd"H~F M Hzw^>;uj (-IQ$ذa#55bz;d/b-TUUӅ-|dQ8Y1,$yRwy#` X$ GQr/.hdOEҴ FN'nwA:l_r^:R˻ܶ47lmQ3$t_M\|b '+LQEt8񤃡'PwgqYmt[e%J\tq瓝J<ɽtG] +KIUUUS1 cަOg'/%4U.W 1j^c8g3sIٹ#?/ظa/2Wƙȗ>y1g^MxeQI&Qw.)䥧‰C'p] 6Fk2 DccׁrS3%*L_~zb{EUU;ƍh2[:5aX5Mc#$ r'L#dH{yz6 1F1~IGbt 1q5Vf3%%%|k_cI~kt?]C&$ñQGVvO Zג=!jI*4M#`*e{xMLE7ݩn㇠d  FC"L87*r_^b>ùq{A w<,? 33EEE?^~֟ރ=>qP,mm*_gC76oL%؄Ĩ1`frΣ!t]#>=O) dV865:NoEjvSQ'\ /ؤϽ7\KF|H&5~ͼq ?ϹbXxGxc_#`ru!ɰcSv2|Y"+ruRh I5娱ib ,q}*Z?S$.;g¼ )ٲ|;7R]G^/i)׊Qnz H5ꨙpAV \tw,`u|>bcc$ ׿uu;6>GkSX({*p54lYWELeYKF|@8vt)`#xh9`r!5GEKgs&g" \Ft0Xbzsgs]ߢ)U)jDj߾*KPktj(^ze"" fYP{tzݻfΜYٻ~%o=/j1Z~5swXq7UMfϞ I- gꜨ#0N\taQt^ϚtP~F] RPVBf,$I3ri,;bO{S rHZBŶ7@(C<ȋ?tn@Rȴ%`:bb7s _)//.l6),YyS iؤ4ܮ6 ɸ[Iʙ=[ți }Iꫫ!o!ڊKѯ毯)1zI-Wٙsq)\߲ow~͆(YYf伥,9o)-ڊh"=#$Si*mmmlݲ=^,v{@Nr/`2w¤ebq_d.aʏ[b4b;|>'N]ǞJ{c-)㊨=~PcbDKs36n@Q g?Pi-e3n˨#/TRd 0sus1Y݆hb&SuhIYTuԏF7e}~e7B+y<ԟR1$oࢥL]~cSd$HWLbx:hXNrH/FX0&w%D%$_\0,7>- FEMVw g )si$6){B m \{lkFI#O@dUG=1uX.@C/IV9a Â$!+FȞ<TK^BV@#,w;9XTANJ.Ö[H?QA4F\R*O%61 gF ㊱ 7r OGK.F˿fP0#C` 9ޟHvk!-ohiPHr0tVRN~dhp)h{ c=Ii(ESUy7 ;.8BNXJaL>s?CWG;vv2$ s}r<tb4[' 1k #ixM!!'t "pn#W)&,XAz4@SEx# WK=N:FKL؇81 i9WL2fTՁqŔlYKpdTTBh"(͹eoj[R ,[<&X1|!Iф#5Jr/ĮMOBUjOo{>K@{=qYIɧBPƁ~a_(#%M(=1C_ґ7O%pfp'0}س\pC!uțuyY|[C[C= c#A.Ry*?ho8=B9SD6?>8o=uf=)N0 ߻3X.5V0q6yִւ^[wr4N][%?y>u%?>мs1vO1B5|^}$u$*^:IBR @A]󆎤IC|nBJ2, fӖБ!ST$ŀ5{> , UdC"#WBCuȲB**=g`!,I(T%&hq ji:*],3waB@|8ӆhd8u޿pt0'U{ϸA_< SJ2X! ,O,`" pHR}/mK[-s_D6@BG)0QQ4-I 40DF5R-a!cuCv0p!:pfcv65d ~i4zLA1"K# H@S-= e(X< E`Covs= {$79# 4#/]('߾gwFl'# LUjN wW)>?YX Y 힞Q;'GCmH2U6Oj][[ˁ6XP0VÈޞ>H@!p?y5z^Dn81V) WCRr -cȞ/܃awi\re݇׭]o673F+,;8 ZLIE0> 4]>O5PI> S`F"XEPVx5{:T;LN5h{aa*XPn;VW#b3ti6P*aw NDH6{ *;y4qipqĊo5'b{y68/R!cβyi0 qFh~{O.l(vNX[!]Z<"_D%c ͩb'?xCT KKK u~5F>>Q\!Hx`x53j=X[pJ[d&%O%yB F ~K,jkQ:rMH0<*oUbrkdut^_] +N'AS,4oWOF n7&9OMӝ1qx3|w:|P zvXd}^8/~9f'wyX+|u3YΩc:T*x7(^ϥ~fba|`kaD\~ 6d{B `qxV ]IZG x$(m5t 8-=j[QJj:ڱ9.6)Z뮾h}]-(L)=s`Rb`N$pkBbeblBOA96柈shu?&a]L9/A@pr_t]+o쯃7WALHl '%X1Ầq "Z.KBCrBϽ0xFoZS ~Hlo?.H n[X+AjzƔj/sMPka>m?ր!_F$ǀ5|t3MݰNowO ]Q`z x<NpTzw0ED /qhGF}"?TCi!Vxp/pl+L?_,9^), Nyede߱ǩ(Uh,0anw''jEăCU3 RhTϗ|/xu]b϶*ONVivk&ï+W2gjzwE c< >H}i2,IN _3SAY+Y ]ɛ ʫӂB=+m#]S9? ڗ& wb-϶÷ 2b}"Z즫 !pB.\itvhkmj͘bzURRO/AδHŸ7(c ?UTSH4kǓ$HjXU~8LbWk څJIϧCIZ,Tt -+H΄Xӡ( ;' %tX%v.jD/I} .//z(D&; P3[-v/ä˼^֓yF5R,byUA ^a~w >[i6wz:Hv9|X!_D=uDKI`V2H`K|i#|$>o@Rk ''E tϽJ FԽC r?QRqv(v_/)1/:TB v1|qDS]5?٬ p v[ OqE>CBt/\ ,6Ht pBi*o<~̀vާt!AKE A>{M;5Q訛"")dVs]85OttU\ƛ>jQD|rG7ɂ>Mk+㧺С V&¤D8t ^=󅘨"ָ_PZqEA9!Dnza\"p\7^,Y$ *v 3$M$E\3Uzkc~R 1tG2, ]""`=] uWOi"BBd+S!6R13R%`ssyDsQ4a,|J,h;\{+DWOªExf7[j(CZ:].J[h~'P [LB ]F<fY2ߖ/nO7s]AM?#V>b‚zCVsD$v*y-O\NpYj7g =O? 5lRc+tA>],*^s+7 vA@n?Z/N.}HѓE%Z1$&H&H|JX\~/lM;&>)6 @?o6x|ƴ~;gEAPIWF\>b#^Zn(Bw * %2:UQ><.8*o@y|}]-5ۂ7̌?h~?t= B:xiZ==<0Sj"xcMƹvy?o@dh:u_o6pyp#Tk.aC@v^:qq.W L3~f 2]i|G dt0O^,|y*,Zn7ɒĪ6[H ՝buN0ۯN \SM] nL`&4t=C >? Xz#(V'?} 6PcKܡb8;&~"c?TVrsrzŪJ! cafC63YHSP=8'PЁ$(43f' *%s!"fuS'& @?-aY*ӭ.]#1g@HP!,ߛ/; b1Q$a tV?@ p= e8NְG@ 9er5Ñ#Gp\ڳԀ;-LL2T=׏&iY[}F!N\X{ %IWV3n/Lc@^Bb)܃8\肤_%GqcU]춇WD2u/B!!VGnia0@P>u'Kzz: B%$t#,a;`=(E06AD_2W,h+p ޮyDWuu >j(_abEm]իd(1{~sHĻb5 MPXDk:RNƓ-k=O 2'&ށ%px(0v\ @k`[9A$H> |dIDκ>aNT?Y_Tw t} +pޔseIroc| _n:"E xEZ ~ ,ΡJ%9۝OaCx4lk]v7`ъmF@ `P ":!ª"a$]\?T@`hLo頋٢UN}\~ NOM>qp#`Ɛȯnnw/5ADWŎ*=8`:$vC>TXc7wr²4 a } |dIS³%)55z#߆4Zl±'D1A܅"jĭFr'n>D|IהL'@u@*P$zpg+GctU`y( O\^>Tp{A% Q r >CG5a*C`4YB8 ,An\؄羆xj&n!G ܊XOdI﯃{K[Bm F(-փq6~@ /nSS?gK^1-ySOp)K_S zp0 C-3DRpnʃϬD6ٰj;NIcXaIF(yڢ.K)mq$,|փ0]Ӡ%ҽW#}P.OL'B9"3 XbINS`f"kE.Lpwg£Lf3Mi~HF`D>|xa-jpS,3ΐhTV8/zG0TD点ib1mFY4a7bi~y"8CB|z_ e½Iԙ&ݢ.ڶZQ.H FW_0-w[T97HWyOӳ("kLd8k!XO "]^_ T ?"s.O␻-НXϗFe# H7+0>V ui4!u; :m^=Jmq0ɖ Fz0D<Wo`s5Tn~"Qa$kqnMx2 i#ۍpshD0b'4Vu#@_#BTFX7Ns?/Rpu’X =!J[.=Hno]SO^jA%$I ?1YO{EnY3n7\ .OW qyj҇;/R ,iUAAu["2~JB|<|- EN$xx4#ASb,K$ffy.x< g&Aj p[1u>> o=hp :qb$[߹B +Nm1𹮉 _m&¯@YMM*" 0Xal-sakM?Yݿ.ȀXu‚n^!PN 4hl aK[< S;Q@Ԥ3 KOWg?BuOK&Y"m]eYSK!*ÃE:ߧ߀ W=k!,it[beD`, "¤$8]o,mQB/U|8 +WCcTvG렦K+0wal&Àu[<|r&~.tp[, W׊tfa7u_06aDAv[RdHDw[Gi 3(Sh%Xykg"VMJt-)$a2; ~/kǴ`2_Hh&"mkOr^܎%DͯCm~t3Jbgp33iߺ!m~Д3m}3`m~д["kb,mzzWӶDMVٶ js(bi=i~nQL[i~~Q"R['*Bmb mUҩTURTbD_( I-`4Qb(J hjstČxׁ;i~PΘYVa~ov%tEXtdate:create2020-01-07T13:40:04-08:00Q9%tEXtdate:modify2020-01-07T12:08:37-08:00@5tEXtSoftwareAdobe ImageReadyqe<IENDB`googlesheets4/man/figures/lifecycle-archived.svg0000644000176200001440000000170713635427346021532 0ustar liggesusers lifecyclelifecyclearchivedarchived googlesheets4/man/figures/lifecycle-soft-deprecated.svg0000644000176200001440000000172613635427346023017 0ustar liggesuserslifecyclelifecyclesoft-deprecatedsoft-deprecated googlesheets4/man/figures/lifecycle-questioning.svg0000644000176200001440000000171413635427346022310 0ustar liggesuserslifecyclelifecyclequestioningquestioning googlesheets4/man/figures/lifecycle-stable.svg0000644000176200001440000000167413635427346021222 0ustar liggesuserslifecyclelifecyclestablestable googlesheets4/man/figures/lifecycle-experimental.svg0000644000176200001440000000171613635427346022442 0ustar liggesuserslifecyclelifecycleexperimentalexperimental googlesheets4/man/figures/lifecycle-deprecated.svg0000644000176200001440000000171213635427346022041 0ustar liggesuserslifecyclelifecycledeprecateddeprecated googlesheets4/man/figures/lifecycle-retired.svg0000644000176200001440000000170513635427346021401 0ustar liggesusers lifecyclelifecycleretiredretired googlesheets4/man/range_autofit.Rd0000644000176200001440000000545014075054276016735 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/range_autofit.R \name{range_autofit} \alias{range_autofit} \title{Auto-fit columns or rows to the data} \usage{ range_autofit(ss, sheet = NULL, range = NULL, dimension = c("columns", "rows")) } \arguments{ \item{ss}{Something that identifies a Google Sheet: \itemize{ \item its file id as a string or \code{\link[googledrive:as_id]{drive_id}} \item a URL from which we can recover the id \item a one-row \code{\link[googledrive:dribble]{dribble}}, which is how googledrive represents Drive files \item an instance of \code{googlesheets4_spreadsheet}, which is what \code{\link[=gs4_get]{gs4_get()}} returns } Processed through \code{\link[=as_sheets_id]{as_sheets_id()}}.} \item{sheet}{Sheet to modify, in the sense of "worksheet" or "tab". You can identify a sheet by name, with a string, or by position, with a number. Ignored if the sheet is specified via \code{range}. If neither argument specifies the sheet, defaults to the first visible sheet.} \item{range}{Which columns or rows to resize. Optional. If you want to resize all columns or all rows, use \code{dimension} instead. All the usual \code{range} specifications are accepted, but the targeted range must specify only columns (e.g. "B:F") or only rows (e.g. "2:7").} \item{dimension}{Ignored if \code{range} is given. If consulted, \code{dimension} must be either \code{"columns"} (the default) or \code{"rows"}. This is the simplest way to request auto-resize for all columns or all rows.} } \value{ The input \code{ss}, as an instance of \code{\link{sheets_id}} } \description{ Applies automatic resizing to either columns or rows of a (work)sheet. The width or height of targeted columns or rows, respectively, is determined from the current cell contents. This only affects the appearance of a sheet in the browser and doesn't affect its values or dimensions in any way. } \examples{ if (gs4_has_token()) { dat <- tibble::tibble( fruit = c("date", "lime", "pear", "plum") ) ss <- gs4_create("range-autofit-demo", sheets = dat) ss # open in the browser gs4_browse(ss) # shrink column A to fit the short fruit names range_autofit(ss) # in the browser, notice how the column width shrank # send some longer fruit names dat2 <- tibble::tibble( fruit = c("cucumber", "honeydew") ) ss \%>\% sheet_append(dat2) # in the browser, see that column A is now too narrow to show the data range_autofit(ss) # in the browser, see the column A reveals all the data now # clean up gs4_find("range-autofit-demo") \%>\% googledrive::drive_trash() } } \seealso{ Makes an \code{AutoResizeDimensionsRequest}: \itemize{ \item \url{https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#autoresizedimensionsrequest} } } \concept{formatting functions} googlesheets4/man/gs4_auth.Rd0000644000176200001440000001316414074074641015622 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/gs4_auth.R \name{gs4_auth} \alias{gs4_auth} \title{Authorize googlesheets4} \usage{ gs4_auth( email = gargle::gargle_oauth_email(), path = NULL, scopes = "https://www.googleapis.com/auth/spreadsheets", cache = gargle::gargle_oauth_cache(), use_oob = gargle::gargle_oob_default(), token = NULL ) } \arguments{ \item{email}{Optional. Allows user to target a specific Google identity. If specified, this is used for token lookup, i.e. to determine if a suitable token is already available in the cache. If no such token is found, \code{email} is used to pre-select the targetted Google identity in the OAuth chooser. Note, however, that the email associated with a token when it's cached is always determined from the token itself, never from this argument. Use \code{NA} or \code{FALSE} to match nothing and force the OAuth dance in the browser. Use \code{TRUE} to allow email auto-discovery, if exactly one matching token is found in the cache. Specify just the domain with a glob pattern, e.g. \code{"*@example.com"}, to create code that "just works" for both \code{alice@example.com} and \code{bob@example.com}. Defaults to the option named "gargle_oauth_email", retrieved by \code{\link[gargle:gargle_options]{gargle_oauth_email()}}.} \item{path}{JSON identifying the service account, in one of the forms supported for the \code{txt} argument of \code{\link[jsonlite:fromJSON]{jsonlite::fromJSON()}} (typically, a file path or JSON string).} \item{scopes}{A character vector of scopes to request. Pick from those listed at \url{https://developers.google.com/identity/protocols/oauth2/scopes}. For certain token flows, the \code{"https://www.googleapis.com/auth/userinfo.email"} scope is unconditionally included. This grants permission to retrieve the email address associated with a token; gargle uses this to index cached OAuth tokens. This grants no permission to view or send email and is generally considered a low-value scope.} \item{cache}{Specifies the OAuth token cache. Defaults to the option named "gargle_oauth_cache", retrieved via \code{\link[gargle:gargle_options]{gargle_oauth_cache()}}.} \item{use_oob}{Whether to prefer "out of band" authentication. Defaults to the option named "gargle_oob_default", retrieved via \code{\link[gargle:gargle_options]{gargle_oob_default()}}.} \item{token}{A token with class \link[httr:Token-class]{Token2.0} or an object of httr's class \code{request}, i.e. a token that has been prepared with \code{\link[httr:config]{httr::config()}} and has a \link[httr:Token-class]{Token2.0} in the \code{auth_token} component.} } \description{ Authorize googlesheets4 to view and manage your Google Sheets. This function is a wrapper around \code{\link[gargle:token_fetch]{gargle::token_fetch()}}. By default, you are directed to a web browser, asked to sign in to your Google account, and to grant googlesheets4 permission to operate on your behalf with Google Sheets. By default, with your permission, these user credentials are cached in a folder below your home directory, from where they can be automatically refreshed, as necessary. Storage at the user level means the same token can be used across multiple projects and tokens are less likely to be synced to the cloud by accident. If you are interacting with R within a browser (applies to RStudio Server, RStudio Workbench, and RStudio Cloud), you need a variant of this flow, known as out-of-band auth ("oob"). If this does not happen automatically, you can request it yourself with \code{use_oob = TRUE} or, more persistently, by setting an option via \code{options(gargle_oob_default = TRUE)}. } \details{ Most users, most of the time, do not need to call \code{gs4_auth()} explicitly -- it is triggered by the first action that requires authorization. Even when called, the default arguments often suffice. However, when necessary, this function allows the user to explicitly: \itemize{ \item Declare which Google identity to use, via an email address. If there are multiple cached tokens, this can clarify which one to use. It can also force googlesheets4 to switch from one identity to another. If there's no cached token for the email, this triggers a return to the browser to choose the identity and give consent. You can specify just the domain by using a glob pattern. This means that a script containing \code{email = "*@example.com"} can be run without further tweaks on the machine of either \code{alice@example.com} or \code{bob@example.com}. \item Use a service account token or workload identity federation. \item Bring their own \link[httr:Token-class]{Token2.0}. \item Specify non-default behavior re: token caching and out-of-bound authentication. \item Customize scopes. } For details on the many ways to find a token, see \code{\link[gargle:token_fetch]{gargle::token_fetch()}}. For deeper control over auth, use \code{\link[=gs4_auth_configure]{gs4_auth_configure()}} to bring your own OAuth app or API key. Read more about gargle options, see \link[gargle:gargle_options]{gargle::gargle_options}. } \examples{ if (interactive()) { # load/refresh existing credentials, if available # otherwise, go to browser for authentication and authorization gs4_auth() # force use of a token associated with a specific email gs4_auth(email = "jenny@example.com") # use a 'read only' scope, so it's impossible to edit or delete Sheets gs4_auth( scopes = "https://www.googleapis.com/auth/spreadsheets.readonly" ) # use a service account token gs4_auth(path = "foofy-83ee9e7c9c48.json") } } \seealso{ Other auth functions: \code{\link{gs4_auth_configure}()}, \code{\link{gs4_deauth}()} } \concept{auth functions} googlesheets4/man/range_flood.Rd0000644000176200001440000000770414075054276016371 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/range_flood.R \name{range_flood} \alias{range_flood} \alias{range_clear} \title{Flood or clear a range of cells} \usage{ range_flood(ss, sheet = NULL, range = NULL, cell = NULL, reformat = TRUE) range_clear(ss, sheet = NULL, range = NULL, reformat = TRUE) } \arguments{ \item{ss}{Something that identifies a Google Sheet: \itemize{ \item its file id as a string or \code{\link[googledrive:as_id]{drive_id}} \item a URL from which we can recover the id \item a one-row \code{\link[googledrive:dribble]{dribble}}, which is how googledrive represents Drive files \item an instance of \code{googlesheets4_spreadsheet}, which is what \code{\link[=gs4_get]{gs4_get()}} returns } Processed through \code{\link[=as_sheets_id]{as_sheets_id()}}.} \item{sheet}{Sheet to write into, in the sense of "worksheet" or "tab". You can identify a sheet by name, with a string, or by position, with a number.} \item{range}{A cell range to read from. If \code{NULL}, all non-empty cells are read. Otherwise specify \code{range} as described in \href{https://developers.google.com/sheets/api/guides/concepts#a1_notation}{Sheets A1 notation} or using the helpers documented in \link{cell-specification}. Sheets uses fairly standard spreadsheet range notation, although a bit different from Excel. Examples of valid ranges: \code{"Sheet1!A1:B2"}, \code{"Sheet1!A:A"}, \code{"Sheet1!1:2"}, \code{"Sheet1!A5:A"}, \code{"A1:B2"}, \code{"Sheet1"}. Interpreted strictly, even if the range forces the inclusion of leading, trailing, or embedded empty rows or columns. Takes precedence over \code{skip}, \code{n_max} and \code{sheet}. Note \code{range} can be a named range, like \code{"sales_data"}, without any cell reference.} \item{cell}{The value to fill the cells in the \code{range} with. If unspecified, the default of \code{NULL} results in clearing the existing value.} \item{reformat}{Logical, indicates whether to reformat the affected cells. Currently googlesheets4 provides no real support for formatting, so \code{reformat = TRUE} effectively means that edited cells become unformatted.} } \value{ The input \code{ss}, as an instance of \code{\link{sheets_id}} } \description{ \code{range_flood()} "floods" a range of cells with the same content. \code{range_clear()} is a wrapper that handles the common special case of clearing the cell value. Both functions, by default, also clear the format, but this can be specified via \code{reformat}. } \examples{ if (gs4_has_token()) { # create a data frame to use as initial data df <- gs4_fodder(10) # create Sheet ss <- gs4_create("range-flood-demo", sheets = list(df)) # default behavior (`cell = NULL`): clear value and format range_flood(ss, range = "A1:B3") # clear value but preserve format range_flood(ss, range = "C1:D3", reformat = FALSE) # send new value range_flood(ss, range = "4:5", cell = ";-)") # send formatting # WARNING: use these unexported, internal functions at your own risk! # This not (yet) officially supported, but it's possible. blue_background <- googlesheets4:::CellData( userEnteredFormat = googlesheets4:::new( "CellFormat", backgroundColor = googlesheets4:::new( "Color", red = 159 / 255, green = 183 / 255, blue = 196 / 255 ) ) ) range_flood(ss, range = "I:J", cell = blue_background) # range_clear() is a shortcut where `cell = NULL` always range_clear(ss, range = "9:9") range_clear(ss, range = "10:10", reformat = FALSE) # clean up gs4_find("range-flood-demo") \%>\% googledrive::drive_trash() } } \seealso{ Makes a \code{RepeatCellRequest}: \itemize{ \item \url{https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#repeatcellrequest} } Other write functions: \code{\link{gs4_create}()}, \code{\link{gs4_formula}()}, \code{\link{range_delete}()}, \code{\link{range_write}()}, \code{\link{sheet_append}()}, \code{\link{sheet_write}()} } \concept{write functions} googlesheets4/man/pipe.Rd0000644000176200001440000000040013257602440015022 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/utils-pipe.R \name{\%>\%} \alias{\%>\%} \title{Pipe operator} \usage{ lhs \%>\% rhs } \description{ See \code{magrittr::\link[magrittr]{\%>\%}} for details. } \keyword{internal} googlesheets4/man/gs4_browse.Rd0000644000176200001440000000154214075054276016162 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/gs4_browse.R \name{gs4_browse} \alias{gs4_browse} \title{Visit a Sheet in a web browser} \usage{ gs4_browse(ss) } \arguments{ \item{ss}{Something that identifies a Google Sheet: \itemize{ \item its file id as a string or \code{\link[googledrive:as_id]{drive_id}} \item a URL from which we can recover the id \item a one-row \code{\link[googledrive:dribble]{dribble}}, which is how googledrive represents Drive files \item an instance of \code{googlesheets4_spreadsheet}, which is what \code{\link[=gs4_get]{gs4_get()}} returns } Processed through \code{\link[=as_sheets_id]{as_sheets_id()}}.} } \value{ The Sheet's browser URL, invisibly. } \description{ Visits a Google Sheet in your default browser, if session is interactive. } \examples{ gs4_example("mini-gap") \%>\% gs4_browse() } googlesheets4/man/gs4_endpoints.Rd0000644000176200001440000000220114076034731016650 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/gs4_endpoints.R \name{gs4_endpoints} \alias{gs4_endpoints} \title{List Sheets endpoints} \usage{ gs4_endpoints(i = NULL) } \arguments{ \item{i}{The name(s) or integer index(ices) of the endpoints to return. Optional. By default, the entire list is returned.} } \value{ A list containing some or all of the subset of the Sheets API v4 endpoints that are used internally by googlesheets4. } \description{ Returns a list of selected Sheets API v4 endpoints, as stored inside the googlesheets4 package. The names of this list (or the \code{id} sub-elements) are the nicknames that can be used to specify an endpoint in \code{\link[=request_generate]{request_generate()}}. For each endpoint, we store its nickname or \code{id}, the associated HTTP \code{method}, the \code{path}, and details about the parameters. This list is derived programmatically from the Sheets API v4 Discovery Document (\verb{https://www.googleapis.com/discovery/v1/apis/sheets/v4/rest}). } \examples{ str(gs4_endpoints(), max.level = 2) gs4_endpoints("sheets.spreadsheets.values.get") gs4_endpoints(4) } googlesheets4/man/gs4_find.Rd0000644000176200001440000000272714074074641015604 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/gs4_find.R \name{gs4_find} \alias{gs4_find} \title{Find Google Sheets} \usage{ gs4_find(...) } \arguments{ \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()}}.} } \value{ An object of class \code{\link[googledrive]{dribble}}, a tibble with one row per file. } \description{ Finds your Google Sheets. This is a very thin wrapper around \code{\link[googledrive:drive_find]{googledrive::drive_find()}}, that specifies you want to list Drive files where \code{type = "spreadsheet"}. Therefore, note that this will require auth for googledrive! See the article \href{https://googlesheets4.tidyverse.org/articles/articles/drive-and-sheets.html}{Using googlesheets4 with googledrive} if you want to coordinate auth between googlesheets4 and googledrive. This function will emit an informational message if you are currently logged in with both googlesheets4 and googledrive, but as different users. } \examples{ if (gs4_has_token()) { # see all your Sheets gs4_find() # see 5 Sheets, prioritized by creation time x <- gs4_find(order_by = "createdTime desc", n_max = 5) x # hoist the creation date, using other packages in the tidyverse # x \%>\% # tidyr::hoist(drive_resource, created_on = "createdTime") \%>\% # dplyr::mutate(created_on = as.Date(created_on)) } } googlesheets4/man/range_read_cells.Rd0000644000176200001440000001036314075054276017356 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/range_read_cells.R \name{range_read_cells} \alias{range_read_cells} \title{Read cells from a Sheet} \usage{ range_read_cells( ss, sheet = NULL, range = NULL, skip = 0, n_max = Inf, cell_data = c("default", "full"), discard_empty = TRUE ) } \arguments{ \item{ss}{Something that identifies a Google Sheet: \itemize{ \item its file id as a string or \code{\link[googledrive:as_id]{drive_id}} \item a URL from which we can recover the id \item a one-row \code{\link[googledrive:dribble]{dribble}}, which is how googledrive represents Drive files \item an instance of \code{googlesheets4_spreadsheet}, which is what \code{\link[=gs4_get]{gs4_get()}} returns } Processed through \code{\link[=as_sheets_id]{as_sheets_id()}}.} \item{sheet}{Sheet to read, in the sense of "worksheet" or "tab". You can identify a sheet by name, with a string, or by position, with a number. Ignored if the sheet is specified via \code{range}. If neither argument specifies the sheet, defaults to the first visible sheet.} \item{range}{A cell range to read from. If \code{NULL}, all non-empty cells are read. Otherwise specify \code{range} as described in \href{https://developers.google.com/sheets/api/guides/concepts#a1_notation}{Sheets A1 notation} or using the helpers documented in \link{cell-specification}. Sheets uses fairly standard spreadsheet range notation, although a bit different from Excel. Examples of valid ranges: \code{"Sheet1!A1:B2"}, \code{"Sheet1!A:A"}, \code{"Sheet1!1:2"}, \code{"Sheet1!A5:A"}, \code{"A1:B2"}, \code{"Sheet1"}. Interpreted strictly, even if the range forces the inclusion of leading, trailing, or embedded empty rows or columns. Takes precedence over \code{skip}, \code{n_max} and \code{sheet}. Note \code{range} can be a named range, like \code{"sales_data"}, without any cell reference.} \item{skip}{Minimum number of rows to skip before reading anything, be it column names or data. Leading empty rows are automatically skipped, so this is a lower bound. Ignored if \code{range} is given.} \item{n_max}{Maximum number of data rows to parse into the returned tibble. Trailing empty rows are automatically skipped, so this is an upper bound on the number of rows in the result. Ignored if \code{range} is given. \code{n_max} is imposed locally, after reading all non-empty cells, so, if speed is an issue, it is better to use \code{range}.} \item{cell_data}{How much detail to get for each cell. \code{"default"} retrieves the fields actually used when googlesheets4 guesses or imposes cell and column types. \code{"full"} retrieves all fields in the \href{https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/cells#CellData}{\code{CellData} schema}. The main differences relate to cell formatting.} \item{discard_empty}{Whether to discard cells that have no data. Literally, we check for an \code{effectiveValue}, which is one of the fields in the \href{https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/cells#CellData}{\code{CellData} schema}.} } \value{ A tibble with one row per cell in the \code{range}. } \description{ This low-level function returns cell data in a tibble with one row per cell. This tibble has integer variables \code{row} and \code{col} (referring to location with the Google Sheet), an A1-style reference \code{loc}, and a \code{cell} list-column. The flagship function \code{\link[=read_sheet]{read_sheet()}}, a.k.a. \code{\link[=range_read]{range_read()}}, is what most users are looking for, rather than \code{range_read_cells()}. \code{\link[=read_sheet]{read_sheet()}} is basically \code{range_read_cells()} (this function), followed by \code{\link[=spread_sheet]{spread_sheet()}}, which looks after reshaping and column typing. But if you really want raw cell data from the API, \code{range_read_cells()} is for you! } \examples{ if (gs4_has_token()) { range_read_cells(gs4_example("deaths"), range = "arts_data") # if you want detailed and exhaustive cell data, do this range_read_cells( gs4_example("formulas-and-formats"), cell_data = "full", discard_empty = FALSE ) } } \seealso{ Wraps the \code{spreadsheets.get} endpoint: \itemize{ \item \url{https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/get} } } googlesheets4/man/gs4_has_token.Rd0000644000176200001440000000100014074074641016616 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/gs4_auth.R \name{gs4_has_token} \alias{gs4_has_token} \title{Is there a token on hand?} \usage{ gs4_has_token() } \value{ Logical. } \description{ Reports whether googlesheets4 has stored a token, ready for use in downstream requests. } \examples{ gs4_has_token() } \seealso{ Other low-level API functions: \code{\link{gs4_token}()}, \code{\link{request_generate}()}, \code{\link{request_make}()} } \concept{low-level API functions} googlesheets4/man/sheet_copy.Rd0000644000176200001440000000744314075054276016254 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/sheet_copy.R \name{sheet_copy} \alias{sheet_copy} \title{Copy a (work)sheet} \usage{ sheet_copy( from_ss, from_sheet = NULL, to_ss = from_ss, to_sheet = NULL, .before = NULL, .after = NULL ) } \arguments{ \item{from_ss}{Something that identifies a Google Sheet: \itemize{ \item its file id as a string or \code{\link[googledrive:as_id]{drive_id}} \item a URL from which we can recover the id \item a one-row \code{\link[googledrive:dribble]{dribble}}, which is how googledrive represents Drive files \item an instance of \code{googlesheets4_spreadsheet}, which is what \code{\link[=gs4_get]{gs4_get()}} returns } Processed through \code{\link[=as_sheets_id]{as_sheets_id()}}.} \item{from_sheet}{Sheet to copy, 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.} \item{to_ss}{The Sheet to copy \emph{to}. Accepts all the same types of input as \code{from_ss}, which is also what this defaults to, if unspecified.} \item{to_sheet}{Optional. Name of the new sheet, as a string. If you don't specify this, Google generates a name, along the lines of "Copy of blah". Note that sheet names must be unique within a Sheet, so if the automatic name would violate this, Google also de-duplicates it for you, meaning you could conceivably end up with "Copy of blah 2". If you have better ideas about sheet names, specify \code{to_sheet}.} \item{.before, .after}{Optional specification of where to put the new sheet. Specify, at most, one of \code{.before} and \code{.after}. Refer to an existing sheet by name (via a string) or by position (via a number). If unspecified, Sheets puts the new sheet at the end.} } \value{ The receiving Sheet, \verb{to_ ss}, as an instance of \code{\link{sheets_id}}. } \description{ Copies a (work)sheet, within its current (spread)Sheet or to another Sheet. } \examples{ if (gs4_has_token()) { ss_aaa <- gs4_create( "sheet-copy-demo-aaa", sheets = list(mtcars = head(mtcars), chickwts = head(chickwts)) ) # copy 'mtcars' sheet within existing Sheet, accept autogenerated name ss_aaa \%>\% sheet_copy() # copy 'mtcars' sheet within existing Sheet # specify new sheet's name and location ss_aaa \%>\% sheet_copy(to_sheet = "mtcars-the-sequel", .after = 1) # make a second Sheet ss_bbb <- gs4_create("sheet-copy-demo-bbb") # copy 'chickwts' sheet from first Sheet to second # accept auto-generated name and default location ss_aaa \%>\% sheet_copy("chickwts", to_ss = ss_bbb) # copy 'chickwts' sheet from first Sheet to second, # WITH a specific name and into a specific location ss_aaa \%>\% sheet_copy( "chickwts", to_ss = ss_bbb, to_sheet = "chicks-two", .before = 1 ) # clean up gs4_find("sheet-copy-demo") \%>\% googledrive::drive_trash() } } \seealso{ If the copy happens within one Sheet, makes a \code{DuplicateSheetRequest}: \itemize{ \item \url{https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#duplicatesheetrequest} } If the copy is from one Sheet to another, wraps the \code{spreadsheets.sheets/copyTo} endpoint: \itemize{ \item \url{https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.sheets/copyTo} } and possibly makes a subsequent \code{UpdateSheetPropertiesRequest}: \itemize{ \item \url{https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#UpdateSheetPropertiesRequest} } Other worksheet functions: \code{\link{sheet_add}()}, \code{\link{sheet_append}()}, \code{\link{sheet_delete}()}, \code{\link{sheet_properties}()}, \code{\link{sheet_relocate}()}, \code{\link{sheet_rename}()}, \code{\link{sheet_resize}()}, \code{\link{sheet_write}()} } \concept{worksheet functions} googlesheets4/man/range_read.Rd0000644000176200001440000001531114075054276016172 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/range_read.R \name{range_read} \alias{range_read} \alias{read_sheet} \title{Read a Sheet into a data frame} \usage{ range_read( ss, sheet = NULL, range = NULL, col_names = TRUE, col_types = NULL, na = "", trim_ws = TRUE, skip = 0, n_max = Inf, guess_max = min(1000, n_max), .name_repair = "unique" ) read_sheet( ss, sheet = NULL, range = NULL, col_names = TRUE, col_types = NULL, na = "", trim_ws = TRUE, skip = 0, n_max = Inf, guess_max = min(1000, n_max), .name_repair = "unique" ) } \arguments{ \item{ss}{Something that identifies a Google Sheet: \itemize{ \item its file id as a string or \code{\link[googledrive:as_id]{drive_id}} \item a URL from which we can recover the id \item a one-row \code{\link[googledrive:dribble]{dribble}}, which is how googledrive represents Drive files \item an instance of \code{googlesheets4_spreadsheet}, which is what \code{\link[=gs4_get]{gs4_get()}} returns } Processed through \code{\link[=as_sheets_id]{as_sheets_id()}}.} \item{sheet}{Sheet to read, in the sense of "worksheet" or "tab". You can identify a sheet by name, with a string, or by position, with a number. Ignored if the sheet is specified via \code{range}. If neither argument specifies the sheet, defaults to the first visible sheet.} \item{range}{A cell range to read from. If \code{NULL}, all non-empty cells are read. Otherwise specify \code{range} as described in \href{https://developers.google.com/sheets/api/guides/concepts#a1_notation}{Sheets A1 notation} or using the helpers documented in \link{cell-specification}. Sheets uses fairly standard spreadsheet range notation, although a bit different from Excel. Examples of valid ranges: \code{"Sheet1!A1:B2"}, \code{"Sheet1!A:A"}, \code{"Sheet1!1:2"}, \code{"Sheet1!A5:A"}, \code{"A1:B2"}, \code{"Sheet1"}. Interpreted strictly, even if the range forces the inclusion of leading, trailing, or embedded empty rows or columns. Takes precedence over \code{skip}, \code{n_max} and \code{sheet}. Note \code{range} can be a named range, like \code{"sales_data"}, without any cell reference.} \item{col_names}{\code{TRUE} to use the first row as column names, \code{FALSE} to get default names, or a character vector to provide column names directly. If user provides \code{col_types}, \code{col_names} can have one entry per column or one entry per unskipped column.} \item{col_types}{Column types. Either \code{NULL} to guess all from the spreadsheet or a string of readr-style shortcodes, with one character or code per column. If exactly one \code{col_type} is specified, it is recycled. See Column Specification for more.} \item{na}{Character vector of strings to interpret as missing values. By default, blank cells are treated as missing data.} \item{trim_ws}{Logical. Should leading and trailing whitespace be trimmed from cell contents?} \item{skip}{Minimum number of rows to skip before reading anything, be it column names or data. Leading empty rows are automatically skipped, so this is a lower bound. Ignored if \code{range} is given.} \item{n_max}{Maximum number of data rows to parse into the returned tibble. Trailing empty rows are automatically skipped, so this is an upper bound on the number of rows in the result. Ignored if \code{range} is given. \code{n_max} is imposed locally, after reading all non-empty cells, so, if speed is an issue, it is better to use \code{range}.} \item{guess_max}{Maximum number of data rows to use for guessing column types.} \item{.name_repair}{Handling of column names. By default, googlesheets4 ensures column names are not empty and are unique. There is full support for \code{.name_repair} as documented in \code{\link[tibble:tibble]{tibble::tibble()}}.} } \value{ A \link[tibble:tibble-package]{tibble} } \description{ This is the main "read" function of the googlesheets4 package. It goes by two names, because we want it to make sense in two contexts: \itemize{ \item \code{read_sheet()} evokes other table-reading functions, like \code{readr::read_csv()} and \code{readxl::read_excel()}. The \code{sheet} in this case refers to a Google (spread)Sheet. \item \code{range_read()} is the right name according to the naming convention used throughout the googlesheets4 package. } \code{read_sheet()} and \code{range_read()} are synonyms and you can use either one. } \section{Column Specification}{ Column types must be specified in a single string of readr-style short codes, e.g. "cci?l" means "character, character, integer, guess, logical". This is not where googlesheets4's col spec will end up, but it gets the ball rolling in a way that is consistent with readr and doesn't reinvent any wheels. Shortcodes for column types: \itemize{ \item \verb{_} or \code{-}: Skip. Data in a skipped column is still requested from the API (the high-level functions in this package are rectangle-oriented), but is not parsed into the data frame output. \item \verb{?}: Guess. A type is guessed for each cell and then a consensus type is selected for the column. If no atomic type is suitable for all cells, a list-column is created, in which each cell is converted to an R object of "best" type. If no column types are specified, i.e. \code{col_types = NULL}, all types are guessed. \item \code{l}: Logical. \item \code{i}: Integer. This type is never guessed from the data, because Sheets have no formal cell type for integers. \item \code{d} or \code{n}: Numeric, in the sense of "double". \item \code{D}: Date. This type is never guessed from the data, because date cells are just serial datetimes that bear a "date" format. \item \code{t}: Time of day. This type is never guessed from the data, because time cells are just serial datetimes that bear a "time" format. \emph{Not implemented yet; returns POSIXct.} \item \code{T}: Datetime, specifically POSIXct. \item \code{c}: Character. \item \code{C}: Cell. This type is unique to googlesheets4. This returns raw cell data, as an R list, which consists of everything sent by the Sheets API for that cell. Has S3 type of \code{"CELL_SOMETHING"} and \code{"SHEETS_CELL"}. Mostly useful internally, but exposed for those who want direct access to, e.g., formulas and formats. \item \code{L}: List, as in "list-column". Each cell is a length-1 atomic vector of its discovered type. \item \emph{Still to come}: duration (code will be \code{:}) and factor (code will be \code{f}). } } \examples{ if (gs4_has_token()) { ss <- gs4_example("deaths") read_sheet(ss, range = "A5:F15") read_sheet(ss, range = "other!A5:F15", col_types = "ccilDD") read_sheet(ss, range = "arts_data", col_types = "ccilDD") read_sheet(gs4_example("mini-gap")) read_sheet( gs4_example("mini-gap"), sheet = "Europe", range = "A:D", col_types = "ccid" ) } } googlesheets4/man/spread_sheet.Rd0000644000176200001440000000475714074074641016562 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/range_read.R \name{spread_sheet} \alias{spread_sheet} \title{Spread a data frame of cells into spreadsheet shape} \usage{ spread_sheet( df, col_names = TRUE, col_types = NULL, na = "", trim_ws = TRUE, guess_max = min(1000, max(df$row)), .name_repair = "unique" ) } \arguments{ \item{df}{A data frame with one row per (nonempty) cell, integer variables \code{row} and \code{column} (probably referring to location within the spreadsheet), and a list-column \code{cell} of \code{SHEET_CELL} objects.} \item{col_names}{\code{TRUE} to use the first row as column names, \code{FALSE} to get default names, or a character vector to provide column names directly. If user provides \code{col_types}, \code{col_names} can have one entry per column or one entry per unskipped column.} \item{col_types}{Column types. Either \code{NULL} to guess all from the spreadsheet or a string of readr-style shortcodes, with one character or code per column. If exactly one \code{col_type} is specified, it is recycled. See Column Specification for more.} \item{na}{Character vector of strings to interpret as missing values. By default, blank cells are treated as missing data.} \item{trim_ws}{Logical. Should leading and trailing whitespace be trimmed from cell contents?} \item{guess_max}{Maximum number of data rows to use for guessing column types.} \item{.name_repair}{Handling of column names. By default, googlesheets4 ensures column names are not empty and are unique. There is full support for \code{.name_repair} as documented in \code{\link[tibble:tibble]{tibble::tibble()}}.} } \value{ A tibble in the shape of the original spreadsheet, but enforcing user's wishes regarding column names, column types, \code{NA} strings, and whitespace trimming. } \description{ Reshapes a data frame of cells (presumably the output of \code{\link[=range_read_cells]{range_read_cells()}}) into another data frame, i.e., puts it back into the shape of the source spreadsheet. This function exists primarily for internal use and for testing. The flagship function \code{\link[=range_read]{range_read()}}, a.k.a. \code{\link[=read_sheet]{read_sheet()}}, is what most users are looking for. It is basically \code{\link[=range_read_cells]{range_read_cells()}} + \code{spread_sheet()}. } \examples{ if (gs4_has_token()) { df <- gs4_example("mini-gap") \%>\% range_read_cells() spread_sheet(df) # ^^ gets same result as ... read_sheet(gs4_example("mini-gap")) } } googlesheets4/man/request_make.Rd0000644000176200001440000000575214075375065016602 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/request_make.R \name{request_make} \alias{request_make} \title{Make a Google Sheets API request} \usage{ request_make(x, ..., encode = "json") } \arguments{ \item{x}{List. Holds the components for an HTTP request, presumably created with \code{\link[=request_generate]{request_generate()}} or \code{\link[gargle:request_develop]{gargle::request_build()}}. Must contain a \code{method} and \code{url}. If present, \code{body} and \code{token} are used.} \item{...}{Optional arguments passed through to the HTTP method.} \item{encode}{If the body is a named list, how should it be encoded? This has the same meaning as \code{encode} in all the \code{\link[httr:VERB]{httr::VERB()}}s, such as \code{\link[httr:POST]{httr::POST()}}. Note, however, that we default to \code{encode = "json"}, which is what you want most of the time when calling the Sheets API. The httr default is \code{"multipart"}. Other acceptable values are \code{"form"} and \code{"raw"}.} } \value{ Object of class \code{response} from \link{httr}. } \description{ Low-level function to execute a Sheets API request. Most users should, instead, use higher-level wrappers that facilitate common tasks, such as reading or writing worksheets or cell ranges. The functions here are intended for internal use and for programming around the Sheets API. } \details{ \code{make_request()} is a very thin wrapper around \code{\link[gargle:request_retry]{gargle::request_retry()}}, only adding the googlesheets4 user agent. Typically the input has been created with \code{\link[=request_generate]{request_generate()}} or \code{\link[gargle:request_develop]{gargle::request_build()}} and the output is processed with \code{process_response()}. \code{\link[gargle:request_retry]{gargle::request_retry()}} retries requests that error with \verb{429 RESOURCE_EXHAUSTED}. Its basic scheme is exponential backoff, with one tweak that is very specific to the Sheets API, which has documented \href{https://developers.google.com/sheets/api/reference/limits}{usage limits}: "a limit of 500 requests per 100 seconds per project and 100 requests per 100 seconds per user" Note that the "project" here means everyone using googlesheets4 who hasn't configured their own OAuth app. This is potentially a lot of users, all acting independently. If you hit the "100 requests per 100 seconds per \strong{user}" limit (which really does mean YOU), the first wait time is a bit more than 100 seconds, then we revert to exponential backoff. If you experience lots of retries, especially with 100 second delays, it means your use of googlesheets4 is more than casual and \strong{it's time for you to get your own OAuth app or use a service account token}. This is explained in the gargle vignette \code{vignette("get-api-credentials", package = "gargle")}. } \seealso{ Other low-level API functions: \code{\link{gs4_has_token}()}, \code{\link{gs4_token}()}, \code{\link{request_generate}()} } \concept{low-level API functions} googlesheets4/man/sheet_relocate.Rd0000644000176200001440000000632414075054276017075 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/sheet_relocate.R \name{sheet_relocate} \alias{sheet_relocate} \title{Relocate one or more (work)sheets} \usage{ sheet_relocate(ss, sheet, .before = if (is.null(.after)) 1, .after = NULL) } \arguments{ \item{ss}{Something that identifies a Google Sheet: \itemize{ \item its file id as a string or \code{\link[googledrive:as_id]{drive_id}} \item a URL from which we can recover the id \item a one-row \code{\link[googledrive:dribble]{dribble}}, which is how googledrive represents Drive files \item an instance of \code{googlesheets4_spreadsheet}, which is what \code{\link[=gs4_get]{gs4_get()}} returns } Processed through \code{\link[=as_sheets_id]{as_sheets_id()}}.} \item{sheet}{Sheet to relocate, 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 move multiple sheets at once or even a list, if you need to mix names and positions.} \item{.before, .after}{Specification of where to locate the sheets(s) identified by \code{sheet}. Exactly one of \code{.before} and \code{.after} must be specified. Refer to an existing sheet by name (via a string) or by position (via a number).} } \value{ The input \code{ss}, as an instance of \code{\link{sheets_id}} } \description{ Move (work)sheets around within a (spread)Sheet. The outcome is most predictable for these common and simple use cases: \itemize{ \item Reorder and move one or more sheets to the front. \item Move a single sheet to a specific (but arbitrary) location. \item Move multiple sheets to the back with \code{.after = 100} (\code{.after} can be any number greater than or equal to the number of sheets). } If your relocation task is more complicated and you are puzzled by the result, break it into a sequence of simpler calls to \code{sheet_relocate()}. } \examples{ if (gs4_has_token()) { sheet_names <- c("alfa", "bravo", "charlie", "delta", "echo", "foxtrot") ss <- gs4_create("sheet-relocate-demo", sheets = sheet_names) sheet_names(ss) # move one sheet, forwards then backwards ss \%>\% sheet_relocate("echo", .before = "bravo") \%>\% sheet_names() ss \%>\% sheet_relocate("echo", .after = "delta") \%>\% sheet_names() # reorder and move multiple sheets to the front ss \%>\% sheet_relocate(list("foxtrot", 4)) \%>\% sheet_names() # put the sheets back in the original order ss \%>\% sheet_relocate(sheet_names) \%>\% sheet_names() # reorder and move multiple sheets to the back ss \%>\% sheet_relocate(c("bravo", "alfa", "echo"), .after = 10) \%>\% sheet_names() # clean up gs4_find("sheet-relocate-demo") \%>\% googledrive::drive_trash() } } \seealso{ Constructs a batch of \code{UpdateSheetPropertiesRequest}s (one per sheet): \itemize{ \item \url{https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#UpdateSheetPropertiesRequest} } Other worksheet functions: \code{\link{sheet_add}()}, \code{\link{sheet_append}()}, \code{\link{sheet_copy}()}, \code{\link{sheet_delete}()}, \code{\link{sheet_properties}()}, \code{\link{sheet_rename}()}, \code{\link{sheet_resize}()}, \code{\link{sheet_write}()} } \concept{worksheet functions} googlesheets4/man/request_generate.Rd0000644000176200001440000000601614076034731017442 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/request_generate.R \name{request_generate} \alias{request_generate} \title{Generate a Google Sheets API request} \usage{ request_generate( endpoint = character(), params = list(), key = NULL, token = gs4_token() ) } \arguments{ \item{endpoint}{Character. Nickname for one of the selected Sheets API v4 endpoints built into googlesheets4. Learn more in \code{\link[=gs4_endpoints]{gs4_endpoints()}}.} \item{params}{Named list. Parameters destined for endpoint URL substitution, the query, or the body.} \item{key}{API key. Needed for requests that don't contain a token. The need for an API key in the absence of a token is explained in Google's document \href{https://support.google.com/googleapi/answer/6158857?hl=en&ref_topic=7013279}{Credentials, access, security, and identity}. In order of precedence, these sources are consulted: the formal \code{key} argument, a \code{key} parameter in \code{params}, a user-configured API key set up with \code{\link[=gs4_auth_configure]{gs4_auth_configure()}} and retrieved with \code{\link[=gs4_api_key]{gs4_api_key()}}.} \item{token}{Set this to \code{NULL} to suppress the inclusion of a token. Note that, if auth has been de-activated via \code{\link[=gs4_deauth]{gs4_deauth()}}, \code{gs4_token()} will actually return \code{NULL}.} } \value{ \code{list()}\cr Components are \code{method}, \code{url}, \code{body}, and \code{token}, suitable as input for \code{\link[=request_make]{request_make()}}. } \description{ Generate a request, using knowledge of the \href{https://developers.google.com/sheets/api/}{Sheets API} from its Discovery Document (\verb{https://www.googleapis.com/discovery/v1/apis/sheets/v4/rest}). Use \code{\link[=request_make]{request_make()}} to execute the request. Most users should, instead, use higher-level wrappers that facilitate common tasks, such as reading or writing worksheets or cell ranges. The functions here are intended for internal use and for programming around the Sheets API. \code{request_generate()} lets you provide the bare minimum of input. It takes a nickname for an endpoint and: \itemize{ \item Uses the API spec to look up the \code{method}, \code{path}, and \code{base_url}. \item Checks \code{params} for validity and completeness with respect to the endpoint. Uses \code{params} for URL endpoint substitution and separates remaining parameters into those destined for the body versus the query. \item Adds an API key to the query if and only if \code{token = NULL}. } } \examples{ req <- request_generate( "sheets.spreadsheets.get", list(spreadsheetId = gs4_example("deaths")), key = "PRETEND_I_AM_AN_API_KEY", token = NULL ) req } \seealso{ \code{\link[gargle:request_develop]{gargle::request_develop()}}, \code{\link[gargle:request_develop]{gargle::request_build()}}, \code{\link[gargle:request_make]{gargle::request_make()}} Other low-level API functions: \code{\link{gs4_has_token}()}, \code{\link{gs4_token}()}, \code{\link{request_make}()} } \concept{low-level API functions} googlesheets4/man/sheet_resize.Rd0000644000176200001440000000473614075054276016605 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/sheet_resize.R \name{sheet_resize} \alias{sheet_resize} \title{Change the size of a (work)sheet} \usage{ sheet_resize(ss, sheet = NULL, nrow = NULL, ncol = NULL, exact = FALSE) } \arguments{ \item{ss}{Something that identifies a Google Sheet: \itemize{ \item its file id as a string or \code{\link[googledrive:as_id]{drive_id}} \item a URL from which we can recover the id \item a one-row \code{\link[googledrive:dribble]{dribble}}, which is how googledrive represents Drive files \item an instance of \code{googlesheets4_spreadsheet}, which is what \code{\link[=gs4_get]{gs4_get()}} returns } Processed through \code{\link[=as_sheets_id]{as_sheets_id()}}.} \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.} \item{nrow, ncol}{Desired number of rows or columns, respectively. The default of \code{NULL} means to leave unchanged.} \item{exact}{Logical, indicating whether to impose \code{nrow} and \code{ncol} exactly or to treat them as lower bounds. If \code{exact = FALSE}, \code{sheet_resize()} can only add cells. If \code{exact = TRUE}, cells can be deleted and their contents are lost.} } \value{ The input \code{ss}, as an instance of \code{\link{sheets_id}} } \description{ Changes the number of rows and/or columns in a (work)sheet. } \examples{ if (gs4_has_token()) { # create a Sheet with the default initial worksheet (ss <- gs4_create("sheet-resize-demo")) # see (work)sheet dims sheet_properties(ss) # no resize occurs sheet_resize(ss, nrow = 2, ncol = 6) # reduce sheet size sheet_resize(ss, nrow = 5, ncol = 7, exact = TRUE) # add rows sheet_resize(ss, nrow = 7) # add columns sheet_resize(ss, ncol = 10) # add rows and columns sheet_resize(ss, nrow = 9, ncol = 12) # re-inspect (work)sheet dims sheet_properties(ss) # clean up gs4_find("sheet-resize-demo") \%>\% googledrive::drive_trash() } } \seealso{ Makes an \code{UpdateSheetPropertiesRequest}: \itemize{ \item <# https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#UpdateSheetPropertiesRequest> } Other worksheet functions: \code{\link{sheet_add}()}, \code{\link{sheet_append}()}, \code{\link{sheet_copy}()}, \code{\link{sheet_delete}()}, \code{\link{sheet_properties}()}, \code{\link{sheet_relocate}()}, \code{\link{sheet_rename}()}, \code{\link{sheet_write}()} } \concept{worksheet functions} googlesheets4/man/cell-specification.Rd0000644000176200001440000000335014074074641017635 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/cell-specification.R \name{cell-specification} \alias{cell-specification} \alias{cell_limits} \alias{cell_rows} \alias{cell_cols} \alias{anchored} \title{Specify cells} \description{ Many functions in googlesheets4 use a \code{range} argument to target specific cells. The Sheets v4 API expects user-specified ranges to be expressed via \href{https://developers.google.com/sheets/api/guides/concepts#a1_notation}{its A1 notation}, but googlesheets4 accepts and converts a few alternative specifications provided by the functions in the \link{cellranger} package. Of course, you can always provide A1-style ranges directly to functions like \code{\link[=read_sheet]{read_sheet()}} or \code{\link[=range_read_cells]{range_read_cells()}}. Why would you use the \link{cellranger} helpers? Some ranges are practically impossible to express in A1 notation, specifically when you want to describe rectangles with some bounds that are specified and others determined by the data. } \examples{ if (gs4_has_token() && interactive()) { ss <- gs4_example("mini-gap") # Specify only the rows or only the columns read_sheet(ss, range = cell_rows(1:3)) read_sheet(ss, range = cell_cols("C:D")) read_sheet(ss, range = cell_cols(1)) # Specify upper or lower bound on row or column read_sheet(ss, range = cell_rows(c(NA, 4))) read_sheet(ss, range = cell_cols(c(NA, "D"))) read_sheet(ss, range = cell_rows(c(3, NA))) read_sheet(ss, range = cell_cols(c(2, NA))) read_sheet(ss, range = cell_cols(c("C", NA))) # Specify a partially open rectangle read_sheet(ss, range = cell_limits(c(2, 3), c(NA, NA)), col_names = FALSE) read_sheet(ss, range = cell_limits(c(1, 2), c(NA, 4))) } } googlesheets4/man/gs4_get.Rd0000644000176200001440000000234714075054276015444 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/gs4_get.R \name{gs4_get} \alias{gs4_get} \title{Get Sheet metadata} \usage{ gs4_get(ss) } \arguments{ \item{ss}{Something that identifies a Google Sheet: \itemize{ \item its file id as a string or \code{\link[googledrive:as_id]{drive_id}} \item a URL from which we can recover the id \item a one-row \code{\link[googledrive:dribble]{dribble}}, which is how googledrive represents Drive files \item an instance of \code{googlesheets4_spreadsheet}, which is what \code{\link[=gs4_get]{gs4_get()}} returns } Processed through \code{\link[=as_sheets_id]{as_sheets_id()}}.} } \value{ A list with S3 class \code{googlesheets4_spreadsheet}, for printing purposes. } \description{ Retrieve spreadsheet-specific metadata, such as details on the individual (work)sheets or named ranges. \itemize{ \item \code{gs4_get()} complements \code{\link[googledrive:drive_get]{googledrive::drive_get()}}, which returns metadata that exists for any file on Drive. } } \examples{ if (gs4_has_token()) { gs4_get(gs4_example("mini-gap")) } } \seealso{ Wraps the \code{spreadsheets.get} endpoint: \itemize{ \item \url{https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/get} } } googlesheets4/DESCRIPTION0000644000176200001440000000342414076066132014544 0ustar liggesusersPackage: googlesheets4 Title: Access Google Sheets using the Sheets API V4 Version: 1.0.0 Authors@R: c(person(given = "Jennifer", family = "Bryan", role = c("cre", "aut"), email = "jenny@rstudio.com", comment = c(ORCID = "0000-0002-6983-2759")), person(given = "RStudio", role = c("cph", "fnd"))) Description: Interact with Google Sheets through the Sheets API v4 . "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. License: MIT + file LICENSE URL: https://googlesheets4.tidyverse.org, https://github.com/tidyverse/googlesheets4 BugReports: https://github.com/tidyverse/googlesheets4/issues Depends: R (>= 3.3) Imports: cellranger, cli (>= 3.0.0), curl, gargle (>= 1.2.0), glue (>= 1.3.0), googledrive (>= 2.0.0), httr, ids, magrittr, methods, purrr, rematch2, rlang (>= 0.4.11), tibble (>= 2.1.1), utils, vctrs (>= 0.2.3) Suggests: covr, readr, rmarkdown, sodium, spelling, testthat (>= 3.0.0), withr ByteCompile: true Config/Needs/website: pkgdown, tidyverse, r-lib/downlit, tidyverse/tidytemplate Config/testthat/edition: 3 Encoding: UTF-8 Language: en-US RoxygenNote: 7.1.1.9001 NeedsCompilation: no Packaged: 2021-07-21 18:20:53 UTC; jenny Author: Jennifer Bryan [cre, aut] (), RStudio [cph, fnd] Maintainer: Jennifer Bryan Repository: CRAN Date/Publication: 2021-07-21 18:50:01 UTC googlesheets4/tests/0000755000176200001440000000000013627542425014202 5ustar liggesusersgooglesheets4/tests/spelling.R0000644000176200001440000000024113564636604016142 0ustar liggesusersif(requireNamespace('spelling', quietly = TRUE)) spelling::spell_check_test(vignettes = TRUE, error = FALSE, skip_on_cran = TRUE) googlesheets4/tests/testthat/0000755000176200001440000000000014076066131016034 5ustar liggesusersgooglesheets4/tests/testthat/test-make_column.R0000644000176200001440000000115713564636604021443 0ustar liggesuserstest_that("resolve_col_type() passes `ctype` other than 'COL_GUESS' through", { expect_identical(resolve_col_type("a cell", "COL_ANYTHING"), "COL_ANYTHING") }) test_that("resolve_col_type() implements coercion DAG for 'COL_GUESS'", { input <- c("l", "D") expect_identical(resolve_col_type(input, "COL_GUESS"), "COL_LIST") }) test_that("blank cell doesn't trick resolve_col_type() into guessing COL_LIST", { input <- list( structure(1, class = c("CELL_BLANK", "SHEETS_CELL")), structure(1, class = c("CELL_TEXT", "SHEETS_CELL")) ) expect_identical(resolve_col_type(input, "COL_GUESS"), "CELL_TEXT") }) googlesheets4/tests/testthat/test-schema_GridRange.R0000644000176200001440000000402114074074641022316 0ustar liggesuserstest_that("we can make a GridRange from a range_spec", { sheets_df <- tibble::tibble(name = "abc", id = 123) # test cases are taken from examples given for GridRange schema # https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/other#GridRange spec <- new_range_spec(sheet_name = "abc", sheets_df = sheets_df) out <- as_GridRange(spec) expect_equal(out$sheetId, 123) spec <- new_range_spec( sheet_name = "abc", cell_range = "A3:B4", sheets_df = sheets_df ) out <- as_GridRange(spec) expect_equal(out$sheetId, 123) expect_equal(out$startRowIndex, 2) expect_equal(out$endRowIndex, 4) expect_equal(out$startColumnIndex, 0) expect_equal(out$endColumnIndex, 2) spec <- new_range_spec( sheet_name = "abc", cell_range = "A5:B", sheets_df = sheets_df ) out <- as_GridRange(spec) expect_equal(out$sheetId, 123) expect_equal(out$startRowIndex, 4) expect_null(out$endRowIndex) expect_equal(out$startColumnIndex, 0) expect_equal(out$endColumnIndex, 2) spec <- new_range_spec( sheet_name = "abc", cell_range = "A:B", sheets_df = sheets_df ) out <- as_GridRange(spec) expect_equal(out$sheetId, 123) expect_null(out$startRowIndex) expect_null(out$endRowIndex) expect_equal(out$startColumnIndex, 0) expect_equal(out$endColumnIndex, 2) spec <- new_range_spec( sheet_name = "abc", cell_range = "A1:A1", sheets_df = sheets_df ) out <- as_GridRange(spec) expect_equal(out$sheetId, 123) expect_equal(out$startRowIndex, 0) expect_equal(out$endRowIndex, 1) expect_equal(out$startColumnIndex, 0) expect_equal(out$endColumnIndex, 1) spec1 <- new_range_spec( sheet_name = "abc", cell_range = "C3:C3", sheets_df = sheets_df ) spec2 <- new_range_spec( sheet_name = "abc", cell_range = "C3", sheets_df = sheets_df ) expect_equal(as_GridRange(spec1), as_GridRange(spec2)) }) test_that("we refuse to make a GridRange from a named_range", { spec <- new_range_spec(named_range = "thingy") expect_error(as_GridRange(spec), "does not accept a named range") }) googlesheets4/tests/testthat/test-aaa.R0000644000176200001440000000027514074074641017665 0ustar liggesuserstest_that("token registered with googlesheets4 and googledrive", { skip_if_offline() skip_if_no_token() expect_true(gs4_has_token()) expect_true(googledrive::drive_has_token()) }) googlesheets4/tests/testthat/test-schema_CellData.R0000644000176200001440000001065014074113143022121 0ustar liggesusers## helpers --------------------------------------------------------------------- expect_empty_cell <- function(object) { expect_equal(object[["userEnteredValue"]], NA) } expect_cell_value <- function(object, nm, val) { expect_equal( object[["userEnteredValue"]], list2(!!nm := val) ) } expect_cell_format <- function(object, fmt) { expect_equal(object[["userEnteredFormat"]], fmt) } ## tests ----------------------------------------------------------------------- test_that("expect_empty_cell() is synced with empty_cell()", { expect_empty_cell(empty_cell()) }) test_that("as_CellData() treats NULL as empty cell", { expect_empty_cell(as_CellData(NULL)[[1]]) }) test_that("as_CellData() works for logical", { out <- as_CellData(c(TRUE, NA, FALSE, NA)) expect_cell_value(out[[1]], "boolValue", TRUE) expect_empty_cell(out[[2]]) expect_cell_value(out[[3]], "boolValue", FALSE) expect_empty_cell(out[[4]]) }) test_that("as_CellData() works for character and factor", { out <- as_CellData(c("a", NA, "c", NA)) expect_cell_value(out[[1]], "stringValue", "a") expect_empty_cell(out[[2]]) expect_cell_value(out[[3]], "stringValue", "c") expect_empty_cell(out[[4]]) out <- as_CellData(factor(c("a", NA, "c", NA))) expect_cell_value(out[[1]], "stringValue", "a") expect_empty_cell(out[[2]]) expect_cell_value(out[[3]], "stringValue", "c") expect_empty_cell(out[[4]]) }) test_that("as_CellData() works for integer or double", { out <- as_CellData(c(1L, NA, 3L, NA)) expect_cell_value(out[[1]], "numberValue", 1L) expect_empty_cell(out[[2]]) expect_cell_value(out[[3]], "numberValue", 3L) expect_empty_cell(out[[4]]) out <- as_CellData(c(1.5, NA, 3.5, NA)) expect_cell_value(out[[1]], "numberValue", 1.5) expect_empty_cell(out[[2]]) expect_cell_value(out[[3]], "numberValue", 3.5) expect_empty_cell(out[[4]]) }) test_that("as_CellData() works for googlesheets4_schema_CellData", { out <- as_CellData("a")[[1]] expect_identical(out, as_CellData(out)) out <- as_CellData(list("a", TRUE, 1.5)) expect_identical(out, as_CellData(out)) }) test_that("as_CellData() works for Date", { input <- as.Date(c("2003-06-06", NA, "1982-12-05")) naked_input <- unclass(input) out <- as_CellData(input) # 25569 = DATEVALUE("1970-01-01), i.e. Unix epoch as a serial date, when the # date origin is December 30th 1899 expect_cell_value(out[[1]], "numberValue", naked_input[[1]] + 25569) expect_empty_cell(out[[2]]) expect_cell_value(out[[3]], "numberValue", naked_input[[3]] + 25569) fmt <- list(numberFormat = list(type = "DATE", pattern = "yyyy-mm-dd")) expect_cell_format(out[[1]], fmt) expect_cell_format(out[[2]], fmt) expect_cell_format(out[[3]], fmt) }) test_that("as_CellData() works for POSIXct", { input <- as.POSIXct(c("1978-05-31 04:24:32", NA, "2006-07-19 23:27:37")) naked_input <- unclass(input) attributes(naked_input) <- NULL out <- as_CellData(input) # 86400 = 60 * 60 * 24 = number of seconds in a day # 25569 = DATEVALUE("1970-01-01), i.e. Unix epoch as a serial date, when the # date origin is December 30th 1899 expect_cell_value( out[[1]], "numberValue", (naked_input[[1]] / 86400) + 25569 ) expect_empty_cell(out[[2]]) expect_cell_value( out[[3]], "numberValue", (naked_input[[3]] / 86400) + 25569 ) fmt <- list(numberFormat = list( type = "DATE_TIME", pattern = "yyyy-mm-dd hh:mm:ss" )) expect_cell_format(out[[1]], fmt) expect_cell_format(out[[2]], fmt) expect_cell_format(out[[3]], fmt) }) test_that("as_CellData() works for list", { input <- list(TRUE, NA, "a", 1.5, factor("a"), 4L) out <- as_CellData(input) expect_cell_value(out[[1]], "boolValue", TRUE) expect_empty_cell(out[[2]]) expect_cell_value(out[[3]], "stringValue", "a") expect_cell_value(out[[4]], "numberValue", 1.5) expect_cell_value(out[[5]], "stringValue", "a") expect_cell_value(out[[6]], "numberValue", 4L) }) test_that("as_CellData() works for formula", { hyperlink <- "=HYPERLINK(\"http://www.google.com/\",\"Google\")" image <- "=IMAGE(\"https://www.google.com/images/srpr/logo3w.png\")" out <- as_CellData(gs4_formula(c(hyperlink, NA, image))) expect_cell_value(out[[1]], "formulaValue", hyperlink) expect_empty_cell(out[[2]]) expect_cell_value(out[[3]], "formulaValue", image) }) test_that("as_CellData() doesn't add extra nesting to list-cols", { expect_identical( as_CellData(c("a", "b")), as_CellData(list("a", "b")) ) }) googlesheets4/tests/testthat/test-range_spec.R0000644000176200001440000001050214074074641021243 0ustar liggesusers# as_range_spec() ---- test_that("as_range_spec() rejects hopeless input", { expect_error(as_range_spec(3), "Can't make a range") }) test_that("as_range_spec() can deal with nothingness", { spec <- as_range_spec(NULL) expect_true(all(map_lgl(spec, ~ is.null(.x) || isFALSE(.x)))) }) test_that("as_range_spec() partitions 'Sheet1!A1:B2'", { sheets_df <- tibble::tibble(name = "Sheet1") spec <- as_range_spec("Sheet1!A1:B2", sheets_df = sheets_df) expect_identical(spec$sheet_name, "Sheet1") expect_identical(spec$cell_range, "A1:B2") expect_true(spec$shim) spec <- as_range_spec("'Sheet1'!A5:A", sheets_df = sheets_df) # make sure we store unescaped name in range_spec expect_identical(spec$sheet_name, "Sheet1") expect_identical(spec$cell_range, "A5:A") expect_true(spec$shim) }) test_that("as_range_spec() seeks a named range, then a sheet name", { nr_df <- tibble::tibble(name = c("a", "thingy", "z")) spec <- as_range_spec("thingy", nr_df = nr_df) expect_null(spec$sheet_name) expect_identical(spec$named_range, "thingy") expect_false(spec$shim) spec <- as_range_spec("thingy", nr_df = nr_df, sheets_df = nr_df) expect_null(spec$sheet_name) expect_identical(spec$named_range, "thingy") expect_false(spec$shim) spec <- as_range_spec( "thingy", nr_df = tibble::tibble(name = letters[1:3]), sheets_df = nr_df ) expect_null(spec$named_range) expect_identical(spec$sheet_name, "thingy") expect_false(spec$shim) }) test_that("A1 range is detected, w/ or w/o sheet", { spec <- as_range_spec("1:2") expect_identical(spec$cell_range, "1:2") expect_true(spec$shim) sheets_df <- tibble::tibble(name = LETTERS[1:3]) spec <- as_range_spec("1:2", sheet = 3, sheets_df = sheets_df) expect_identical(spec$sheet_name, "C") expect_identical(spec$cell_range, "1:2") expect_true(spec$shim) spec <- as_range_spec("1:2", sheet = "B", sheets_df = sheets_df) expect_identical(spec$sheet_name, "B") expect_identical(spec$cell_range, "1:2") expect_true(spec$shim) }) test_that("skip is converted to equivalent cell limits", { spec <- as_range_spec(x = NULL, skip = 1) expect_equal(spec$cell_limits, cell_rows(c(2, NA))) }) test_that("cell_limits input works, w/ or w/o sheet", { spec <- as_range_spec(cell_rows(1:2)) expect_equal(spec$cell_limits, cell_rows(1:2)) expect_true(spec$shim) sheets_df <- tibble::tibble(name = LETTERS[1:3]) spec <- as_range_spec(cell_rows(1:2), sheet = 3, sheets_df = sheets_df) expect_equal(spec$sheet_name, "C") expect_equal(spec$cell_limits, cell_rows(1:2)) expect_true(spec$shim) spec <- as_range_spec(cell_rows(1:2), sheet = "B", sheets_df = sheets_df) expect_equal(spec$sheet_name, "B") expect_equal(spec$cell_limits, cell_rows(1:2)) expect_true(spec$shim) }) test_that("invalid range is rejected", { # no named ranges or sheet names for lookup --> interpret as A1 expect_error( as_range_spec("thingy"), "doesn't appear to be" ) expect_error( as_range_spec("thingy", nr_names = "nope", sheet_names = "nah"), "doesn't appear to be" ) }) test_that("unresolvable sheet raises error", { expect_gs4_error(as_range_spec("A5:A", sheet = 3), "Can't look up") expect_gs4_error(as_range_spec(x = NULL, sheet = 3), "Can't look up") sheets_df <- tibble::tibble(name = LETTERS[1:3]) expect_error( as_range_spec(x = NULL, sheet = "nope", sheets_df = sheets_df), class = "googlesheets4_error_sheet_not_found" ) expect_error( as_range_spec("A5:A", sheet = "nope", sheets_df = sheets_df), class = "googlesheets4_error_sheet_not_found" ) expect_error( as_range_spec("nope!A5:A", sheets_df = sheets_df), class = "googlesheets4_error_sheet_not_found" ) }) # as_A1_range() ---- test_that("as_A1_range() works", { expect_null(as_A1_range(new_range_spec())) expect_equal(as_A1_range(new_range_spec(sheet_name = "Sheet1")), "'Sheet1'") expect_equal(as_A1_range(new_range_spec(named_range = "abc")), "abc") expect_equal(as_A1_range(new_range_spec(cell_range = "B3:D9")), "B3:D9") expect_equal( as_A1_range(new_range_spec(sheet_name = "Sheet1", cell_range = "A1")), "'Sheet1'!A1" ) rs <- new_range_spec(cell_limits = cell_cols(3:5)) expect_equal(as_A1_range(rs), "C:E") rs <- new_range_spec(sheet_name = "Sheet1", cell_limits = cell_rows(2:3)) expect_equal(as_A1_range(rs), "'Sheet1'!2:3") }) googlesheets4/tests/testthat/test-range_delete.R0000644000176200001440000000536714074074641021570 0ustar liggesusers# ---- nm_fun ---- me_ <- nm_fun("TEST-range_delete") # ---- tests ---- test_that("range_delete() works", { skip_if_offline() skip_if_no_token() df <- gs4_fodder(4) ss <- local_ss(me_(), sheets = list(df)) range_delete(ss, range = "2:3") range_delete(ss, range = "B") range_delete(ss, range = "B2", shift = "left") out <- range_read(ss) expect_match(sub("[A-Z](\\d)", "\\1", out[2, ]), "5") expect_equal(names(out), c("A", "C", "D")) expect_equal(out[[1, 2]], "D4") }) # helpers ---- test_that("determine_shift() 'works' for ranges where user input is required", { sheets_df <- tibble::tibble(name = "Sheet1", index = 0) # these are all essentially true rectangles and the user will have to tell # us how to shift cells into the deleted region bounded_bottom_and_right <- list( cell_limits(c(NA, NA), c(3, 5)), cell_limits(c( 1, NA), c(3, 5)), cell_limits(c(NA, 3), c(3, 5)), cell_limits(c( 1, 3), c(3, 5)) ) out <- purrr::map( bounded_bottom_and_right, ~ determine_shift(as_GridRange(as_range_spec(.x, sheets_df = sheets_df))) ) purrr::map(out, expect_null) }) test_that("determine_shift() detects ranges where we shift ROWS up", { sheets_df <- tibble::tibble(name = "Sheet1", index = 0) # these are bounded on the bottom, but not the on the right bounded_bottom <- list( cell_limits(c(NA, NA), c(3, NA)), cell_limits(c( 1, NA), c(3, NA)), cell_limits(c(NA, 3), c(3, NA)), cell_limits(c( 1, 3), c(3, NA)) ) out <- purrr::map_chr( bounded_bottom, ~ determine_shift(as_GridRange(as_range_spec(.x, sheets_df = sheets_df))) ) expect_match(out, "ROWS") }) test_that("determine_shift() detects ranges where we shift COLUMNS left", { sheets_df <- tibble::tibble(name = "Sheet1", index = 0) # these are bounded on the bottom, but not the on the right bounded_right <- list( cell_limits(c(NA, NA), c(NA, 5)), cell_limits(c( 1, NA), c(NA, 5)), cell_limits(c(NA, 3), c(NA, 5)), cell_limits(c( 1, 3), c(NA, 5)) ) out <- purrr::map_chr( bounded_right, ~ determine_shift(as_GridRange(as_range_spec(.x, sheets_df = sheets_df))) ) expect_match(out, "COLUMNS") }) test_that("determine_shift() detects ranges where we must error", { sheets_df <- tibble::tibble(name = "Sheet1", index = 0) # these are not bounded at on either the bottom or the right not_bounded <- list( cell_limits(c(NA, NA), c(NA, NA)), cell_limits(c( 1, NA), c(NA, NA)), cell_limits(c(NA, 3), c(NA, NA)), cell_limits(c( 1, 3), c(NA, NA)) ) expect_bad_range <- function(x) { grid_range <- as_GridRange(as_range_spec(x, sheets_df = sheets_df)) expect_error(determine_shift(grid_range), "must be bounded") } purrr::walk(not_bounded, expect_bad_range) }) googlesheets4/tests/testthat/test-utils-sheet-geometry.R0000644000176200001440000000371713640673130023242 0ustar liggesusers# x = empty cell = not sent back by API # O = occupied cell = present in API payload # A B C D E # 1 x x x x x # 2 x O O O x # 3 x O O x x <-- yes, it is intentional that D3 is empty # 4 x x x x x cell_df <- tibble::tribble( ~ row, ~ col, ~ cell, 2, 2, "B2", 2, 3, "C2", 2, 4, "D2", 3, 2, "B3", 3, 3, "C3" ) # row and col should really be integer cell_df$row <- as.integer(cell_df$row) cell_df$col <- as.integer(cell_df$col) # cell has to be a list-column for the tibble::add_row() in insert_shims() # to work, with increased type-strictness coming to tibble v3.0 # TODO: maybe use an even more realistic cell-type of object here, when the # helpers are better cell_df$cell <- as.list(cell_df$cell) limitize <- function(df) { cell_limits(c(min(df$row), min(df$col)), c(max(df$row), max(df$col))) } expect_shim <- function(rg) { expect_identical( limitize(insert_shims(cell_df, as_cell_limits(rg))), as_cell_limits(rg) ) } test_that("observed data occupies range rectangle --> no shim needed", { expect_identical( insert_shims(cell_df, cell_limits = as_cell_limits("B2:D3")), cell_df ) }) test_that("can shim a single side", { ## up expect_shim("B1:D3") ## down expect_shim("B2:D4") ## left expect_shim("A2:D3") ## right expect_shim("B2:E3") }) test_that("can shim two opposing sides", { ## row direction expect_shim("B1:D4") ## col direction expect_shim("A2:E3") }) test_that("can shim on two perpendicular sides", { ## up and left expect_shim("A1:D3") ## up and right expect_shim("B1:E3") # down and left expect_shim("A2:D4") # down and right expect_shim("B2:E4") }) test_that("can shim three sides", { ## all but bottom expect_shim("A1:E3") ## all but left expect_shim("B1:E4") ## all but top expect_shim("A2:E4") ## all but right expect_shim("A1:D4") }) test_that("can shim four sides", { expect_shim("A1:E4") }) googlesheets4/tests/testthat/test-gs4_fodder.R0000644000176200001440000000035314074074641021160 0ustar liggesuserstest_that("gs4_fodder() works", { dat <- gs4_fodder(3, 5) expect_named(dat, LETTERS[1:5]) ltrs <- rep(LETTERS[1:5], each = 3) nbrs <- rep(1:3, 5) + 1 expect_equal( as.vector(as.matrix(dat)), paste0(ltrs, nbrs) ) }) googlesheets4/tests/testthat/test-range_autofit.R0000644000176200001440000000463714074126177022002 0ustar liggesusers# ---- nm_fun ---- me_ <- nm_fun("TEST-range_autofit") # ---- tests ---- test_that("range_autofit() works", { skip_if_offline() skip_if_no_token() dat <- tibble::tribble( ~x, ~y, ~z, ~a, ~b, ~c, "abcd", "efgh", "ijkl", "mnop", "qrst", "uvwx" ) ss <- local_ss(me_(), sheets = list(dat = dat)) ssid <- as_sheets_id(ss) range_autofit(ss) before <- gs4_get_impl_( ssid, fields = "sheets.data.columnMetadata.pixelSize" ) dat2 <- purrr::modify(dat, ~ paste0(.x, "_", .x)) dat4 <- purrr::modify(dat2, ~ paste0(.x, "_", .x)) sheet_append(ss, dat4) range_autofit(ss) after <- gs4_get_impl_( ssid, fields = "sheets.data.columnMetadata.pixelSize" ) before <- pluck(before, "sheets", 1, "data", 1, "columnMetadata") after <- pluck(after, "sheets", 1, "data", 1, "columnMetadata") expect_true(all(unlist(before) < unlist(after))) }) # ---- helpers ---- test_that("A1-style ranges can be turned into a request", { req <- prepare_auto_resize_request(123, as_range_spec("D:H")) req <- pluck(req, 1, "autoResizeDimensions", "dimensions") expect_equal(req$dimension, "COLUMNS") expect_equal(req$startIndex, cellranger::letter_to_num("D") - 1) expect_equal(req$endIndex, cellranger::letter_to_num("H")) req <- prepare_auto_resize_request(123, as_range_spec("3:7")) req <- pluck(req, 1, "autoResizeDimensions", "dimensions") expect_equal(req$dimension, "ROWS") expect_equal(req$startIndex, 3 - 1) expect_equal(req$endIndex, 7) }) test_that("cell_limits can be turned into a request", { req <- prepare_auto_resize_request( 123, as_range_spec(cell_limits()) ) req <- pluck(req, 1, "autoResizeDimensions", "dimensions") expect_equal(req$dimension, "COLUMNS") expect_null(req$startIndex) expect_null(req$endIndex) req <- prepare_auto_resize_request( 123, as_range_spec(cell_cols(c(3, NA))) ) req <- pluck(req, 1, "autoResizeDimensions", "dimensions") expect_equal(req$dimension, "COLUMNS") expect_equal(req$startIndex, 3 - 1) expect_null(req$endIndex) req <- prepare_auto_resize_request( 123, as_range_spec(cell_cols(c(NA, 5))) ) req <- pluck(req, 1, "autoResizeDimensions", "dimensions") expect_equal(req$dimension, "COLUMNS") expect_equal(req$endIndex, 5) }) test_that("an invalid range is rejected", { expect_error( prepare_auto_resize_request(123, as_range_spec("D3:H")), "only columns or only rows" ) }) googlesheets4/tests/testthat/test-utils-sheet.R0000644000176200001440000000151513640673130021403 0ustar liggesuserstest_that("enlist_sheets() works", { df1 <- data.frame(x = 1L) df2 <- data.frame(x = 2L) df_list <- list(df1 = df1, df2 = df2) f <- function(sheets = NULL) enlist_sheets(enquo(sheets)) expect_null(f()) expect_identical( f(c("string_1", "string_2")), list(name = c("string_1", "string_2"), value = list(NULL, NULL)) ) expect_identical( f(df1), list(name = "df1", value = list(data.frame(x = 1L))) ) expect_identical( f(list(df1, df2)), list(name = list(NULL, NULL), value = list(df1, df2)) ) expect_identical( f(list(df1 = df1, df2 = df2)), list(name = c("df1", "df2"), value = list(df1, df2)) ) expect_identical( f(df_list), f(list(df1 = df1, df2 = df2)) ) expect_identical( f(data.frame(x = 1L)), list(name = list(NULL), value = list(data.frame(x = 1L))) ) }) googlesheets4/tests/testthat/test-sheet_delete.R0000644000176200001440000000117414074074641021574 0ustar liggesusers# ---- nm_fun ---- me_ <- nm_fun("TEST-sheet_delete") # ---- tests ---- test_that("sheet_delete() rejects invalid `sheet`", { expect_error( sheet_delete(as_sheets_id("123"), sheet = TRUE), "must be either" ) }) test_that("sheet_delete() works", { skip_if_offline() skip_if_no_token() ss <- local_ss(me_()) sheet_add(ss, c("alpha", "beta", "gamma", "delta")) expect_error_free( sheet_delete(ss, 1) ) expect_error_free( sheet_delete(ss, "gamma") ) expect_error_free( sheet_delete(ss, list("alpha", 2)) ) sheets_df <- sheet_properties(ss) expect_identical(sheets_df$name, "delta") }) googlesheets4/tests/testthat/test-sheet_rename.R0000644000176200001440000000064114074074641021577 0ustar liggesusers# ---- nm_fun ---- me_ <- nm_fun("TEST-sheet_rename") # ---- tests ---- test_that("internal copy works", { skip_if_offline() skip_if_no_token() ss <- local_ss( me_(), sheets = list(iris = head(iris), chickwts = head(chickwts)) ) ss %>% sheet_rename(2, new_name = "poultry") %>% sheet_rename(1, new_name = "flowers") out <- sheet_names(ss) expect_equal(out, c("flowers", "poultry")) }) googlesheets4/tests/testthat/test-rectangle.R0000644000176200001440000000174713635427346021122 0ustar liggesuserstest_that("glean_lgl() works", { expect_identical(glean_lgl(list(a = TRUE), "a"), TRUE) expect_identical(glean_lgl(list(b = TRUE), "a"), NA) expect_identical(glean_lgl(list(), "a"), NA) expect_identical(glean_lgl(list(b = TRUE), "a", .default = FALSE), FALSE) expect_error(glean_lgl(list(a = "a"), "a"), "Can't coerce") }) test_that("glean_chr() works", { expect_identical(glean_chr(list(a = "hi"), "a"), "hi") expect_identical(glean_chr(list(b = "bye"), "a"), NA_character_) expect_identical(glean_chr(list(), "a"), NA_character_) expect_identical(glean_chr(list(b = "bye"), "a", .default = "huh"), "huh") }) test_that("glean_int() works", { expect_identical(glean_int(list(a = 1L), "a"), 1L) expect_identical(glean_int(list(b = 1L), "a"), NA_integer_) expect_identical(glean_int(list(), "a"), NA_integer_) expect_identical(glean_int(list(b = 1L), "a", .default = 2L), 2L) expect_error(glean_int(list(a = "a"), "a"), "Can't coerce") }) googlesheets4/tests/testthat/test-sheets_id-class.R0000644000176200001440000000743614075052756022227 0ustar liggesuserstest_that("can coerce simple strings and drive_id's to sheets_id", { expect_s3_class(as_sheets_id("123"), "sheets_id") expect_identical(as_sheets_id(as_sheets_id("123")), as_sheets_id("123")) expect_identical(as_sheets_id(as_id("123")), as_sheets_id("123")) }) test_that("string with invalid character is rejected", { expect_snapshot(as_sheets_id("abc&123"), error = TRUE) }) test_that("invalid inputs are caught", { expect_error(as_sheets_id(NULL)) expect_error(as_sheets_id(1)) expect_snapshot(as_sheets_id(letters[1:2]), error = TRUE) }) test_that("id can be dug out of a URL", { expect_identical( as_sheets_id("https://docs.google.com/spreadsheets/d/abc123/"), as_sheets_id("abc123") ) expect_identical( as_sheets_id("https://docs.google.com/spreadsheets/d/abc123/edit#gid=123"), as_sheets_id("abc123") ) }) test_that("invalid URL is interpreted as filepath, results in NA", { out <- as_sheets_id("https://www.r-project.org") expect_equal(vec_data(NA), NA) }) # how I created the reference dribble, which represents two files: # * one Google Sheet # * one non-Google Sheet # dat <- googledrive::drive_examples_remote() # dat <- dat[dat$name %in% c("chicken.txt", "chicken_sheet"), ] # saveRDS(dat, file = test_path("ref/dribble.rds"), version = 2) test_that("multi-row dribble is rejected", { d <- readRDS(test_path("ref/dribble.rds")) expect_snapshot(as_sheets_id(d), error = TRUE) }) test_that("dribble with non-Sheet file is rejected", { d <- readRDS(test_path("ref/dribble.rds")) d <- googledrive::drive_reveal(d, what = "mime_type") d <- d[d$mime_type == "text/plain", ] expect_snapshot(as_sheets_id(d), error = TRUE) }) test_that("dribble with one Sheet can be coerced", { d <- readRDS(test_path("ref/dribble.rds")) d <- googledrive::drive_reveal(d, what = "mime_type") d <- d[d$mime_type == "application/vnd.google-apps.spreadsheet", ] expect_s3_class(as_sheets_id(d), "sheets_id") }) test_that("a googlesheets4_spreadsheet can be coerced", { x <- new("Spreadsheet", spreadsheetId = "123") out <- as_sheets_id(new_googlesheets4_spreadsheet(x)) expect_s3_class(out, "sheets_id") expect_identical(out, as_sheets_id("123")) }) test_that("as_id.googlesheets4_spreadsheet works", { x <- new_googlesheets4_spreadsheet(list(spreadsheetId = "123")) expect_identical(as_id(x), as_id("123")) }) ## sheets_id print method ---- test_that("sheets_id print method reveals metadata", { skip_if_offline() skip_if_no_token() expect_snapshot(print(gs4_example("gapminder"))) }) test_that("sheets_id print method doesn't error for nonexistent ID", { skip_if_offline() skip_if_no_token() expect_error_free(format(as_sheets_id("12345"))) expect_snapshot(as_sheets_id("12345")) }) test_that("can print public sheets_id if deauth'd", { skip_if_offline() skip_on_cran() local_deauth() expect_snapshot(print(gs4_example("mini-gap"))) }) test_that("sheets_id print does not error for lack of cred", { skip_if_offline() skip_on_cran() local_deauth() local_interactive(FALSE) withr::local_options(list(gargle_oauth_cache = FALSE)) # typical initial state: auth_active, but no token yet .auth$clear_cred() .auth$set_auth_active(TRUE) expect_error_free(format(gs4_example("mini-gap"))) expect_snapshot(print(gs4_example("mini-gap"))) }) ## low-level helpers ---- test_that("new_sheets_id() handles 0-length input and NA", { expect_error_free( out <- new_sheets_id(character()) ) expect_length(out, 0) expect_s3_class(out, "sheets_id") expect_error_free( out <- new_sheets_id(NA_character_) ) expect_true(is.na(out)) expect_s3_class(out, "sheets_id") }) test_that("combining 2 sheets_id yields drive_id", { id1 <- as_sheets_id("abc") id2 <- as_sheets_id("def") expect_s3_class(c(id1, id2), class(new_drive_id())) }) googlesheets4/tests/testthat/test-range_read.R0000644000176200001440000000700114074074641021224 0ustar liggesuserstest_that("read_sheet() works and discovers reasonable types", { skip_if_offline() skip_if_no_token() dat <- range_read( test_sheet("googlesheets4-col-types"), sheet = "lots-of-types" ) expect_type( dat$logical, "logical") expect_type( dat$character, "character") expect_type( dat$factor, "character") expect_type( dat$integer, "double") expect_type( dat$double, "double") expect_s3_class(dat$date, "POSIXct") expect_s3_class(dat$datetime, "POSIXct") }) test_that("read_sheet() enacts user-specified coltypes", { skip_if_offline() skip_if_no_token() dat <- range_read( test_sheet("googlesheets4-col-types"), sheet = "lots-of-types", col_types = "lccinDT" ) expect_type( dat$logical, "logical") expect_type( dat$character, "character") expect_type( dat$factor, "character") # TODO: revisit when 'f' means factor expect_type( dat$integer, "integer") expect_type( dat$double, "double") expect_s3_class(dat$date, "Date") expect_s3_class(dat$datetime, "POSIXct") }) test_that("read_sheet() can skip columns", { skip_if_offline() skip_if_no_token() dat <- range_read( test_sheet("googlesheets4-col-types"), sheet = "lots-of-types", col_types = "?-_-_-?" ) expect_equal(ncol(dat), 2) expect_type( dat$logical, "logical") expect_s3_class(dat$datetime, "POSIXct") }) # https://github.com/tidyverse/googlesheets4/issues/73 # https://github.com/tidyverse/googlesheets4/issues/174 test_that("read_sheet() honors `na`", { skip_if_offline() skip_if_no_token() # default behaviour dat <- read_sheet( test_sheet("googlesheets4-col-types"), sheet = "NAs" ) expect_true(all(map_lgl(dat, is.character))) expect_false(is.na(dat$...NA[2])) expect_true(is.na(dat$space[2])) expect_true(is.na(dat$empty_string[2])) expect_true(is.na(dat$truly_empty[2])) # can explicit whitespace survive? dat <- read_sheet( test_sheet("googlesheets4-col-types"), sheet = "NAs", trim_ws = FALSE ) expect_equal(dat$space[2], " ") # can we request empty string instead of NA? dat <- read_sheet( test_sheet("googlesheets4-col-types"), sheet = "NAs", na = character() ) expect_equal(dat$space[2], "") expect_equal(dat$empty_string[2], "") expect_equal(dat$truly_empty[2], "") # explicit whitespace and empty-string-for-NA dat <- read_sheet( test_sheet("googlesheets4-col-types"), sheet = "NAs", na = character(), trim_ws = FALSE ) expect_equal(dat$space[2], " ") # more NA strings dat <- read_sheet( test_sheet("googlesheets4-col-types"), sheet = "NAs", na = c("", "NA", "Missing") ) expect_true(is.na(dat$...Missing[2])) expect_true(is.na(dat$...NA[2])) expect_true(is.na(dat$space[2])) expect_true(is.na(dat$empty_string[2])) expect_true(is.na(dat$truly_empty[2])) # column name that is NA string dat <- read_sheet( test_sheet("googlesheets4-col-types"), sheet = "NAs", na = "complete", .name_repair = ~ vec_as_names(.x, repair = "unique", quiet = TRUE) ) expect_match(rev(names(dat))[1], "^...") # how NA strings interact with column typing dat <- read_sheet( test_sheet("googlesheets4-col-types"), sheet = "NAs", na = c("one", "three") ) expect_true(is.character(dat$...Missing)) expect_true(is.character(dat$...NA)) expect_true(is.character(dat$space)) expect_true(is.character(dat$complete)) expect_true(is.logical(dat$empty_string)) expect_true(is.logical(dat$truly_empty)) }) googlesheets4/tests/testthat/test-range_read_cells.R0000644000176200001440000000476014074074641022417 0ustar liggesuserstest_that("cells() returns `row` and `col` as integer", { skip_if_offline() skip_if_no_token() out <- range_read_cells( test_sheet("googlesheets4-cell-tests"), range = "'range-experimentation'!A1:B2" ) expect_true(is.integer(out$row)) expect_true(is.integer(out$col)) }) test_that("slightly tricky `range`s work", { skip_if_offline() skip_if_no_token() out <- range_read_cells( test_sheet("googlesheets4-cell-tests"), range = "'range-experimentation'!B:D" ) expect_true(all(grepl("^[BCD]", out$loc))) out <- range_read_cells( test_sheet("googlesheets4-cell-tests"), range = "'range-experimentation'!2:3" ) expect_true(all(grepl("[23]$", out$loc))) out <- range_read_cells( test_sheet("googlesheets4-cell-tests"), range = "'range-experimentation'!B3:C" ) expect_true(all(grepl("^[BC]", out$loc))) expect_true(all(grepl("[3-9]$", out$loc))) out <- range_read_cells( test_sheet("googlesheets4-cell-tests"), range = "'range-experimentation'!B3:5" ) expect_true(all(grepl("^[BCDE]", out$loc))) expect_true(all(grepl("[345]$", out$loc))) }) # https://github.com/tidyverse/googlesheets4/issues/4 test_that("full cell data and empties are within reach", { skip_if_offline() skip_if_no_token() out <- range_read_cells( test_sheet("googlesheets4-cell-tests"), sheet = "empties-and-formats", cell_data = "full", discard_empty = FALSE ) # B2 is empty; make sure it's here expect_true("B2" %in% out$loc) # C2 is empty and orange; make sure it's here and format is available expect_error_free( cell <- out$cell[[which(out$loc == "C2")]] ) expect_true(!is.null(cell$effectiveFormat)) # C1 bears a note expect_error_free( cell <- out$cell[[which(out$loc == "C1")]] ) note <- cell$note expect_true(!is.null(note)) expect_match(note, "Note") }) # https://github.com/tidyverse/googlesheets4/issues/78 test_that("formula cells are parsed based on effectiveValue", { skip_if_offline() skip_if_no_token() out <- range_read_cells( test_sheet("googlesheets4-cell-tests"), sheet = "formulas", range = "B:B", cell_data = "full", discard_empty = FALSE ) expect_s3_class(out$cell[[which(out$loc == "B2")]], "CELL_TEXT") expect_s3_class(out$cell[[which(out$loc == "B3")]], "CELL_NUMERIC") expect_s3_class(out$cell[[which(out$loc == "B4")]], "CELL_BLANK") expect_s3_class(out$cell[[which(out$loc == "B5")]], "CELL_TEXT") expect_s3_class(out$cell[[which(out$loc == "B6")]], "CELL_BLANK") }) googlesheets4/tests/testthat/test-range_speedread.R0000644000176200001440000000172514074074641022254 0ustar liggesuserstest_that("range_spreadread() works", { skip_if_offline() skip_if_no_token() skip_if_not_installed("readr") # specify a sheet-qualified cell range read <- range_read( test_sheet("googlesheets4-cell-tests"), range = "'range-experimentation'!B:D" ) speedread <- range_speedread( test_sheet("googlesheets4-cell-tests"), range = "'range-experimentation'!B:D", col_types = readr::cols() # suppress col spec printing ) expect_equal(read, speedread, ignore_attr = TRUE) # specify col_types read <- range_read( gs4_example("deaths"), sheet = "other", range = "A5:F15", col_types = "??i?DD" ) speedread <- range_speedread( gs4_example("deaths"), sheet = "other", range = "A5:F15", col_types = readr::cols( Age = readr::col_integer(), `Date of birth` = readr::col_date("%m/%d/%Y"), `Date of death` = readr::col_date("%m/%d/%Y") ) ) expect_equal(read, speedread, ignore_attr = TRUE) }) googlesheets4/tests/testthat/test-argument-checkers.R0000644000176200001440000000450114074074641022546 0ustar liggesuserstest_that("col_names must be logical or character and have length", { expect_snapshot(check_col_names(1:3), error = TRUE) expect_snapshot(check_col_names(factor("a")), error = TRUE) expect_snapshot(check_col_names(character()), error = TRUE) }) test_that("logical col_names must be TRUE or FALSE", { expect_snapshot(check_col_names(NA), error = TRUE) expect_snapshot(check_col_names(c(TRUE, FALSE)), error = TRUE) expect_identical(check_col_names(TRUE), TRUE) expect_identical(check_col_names(FALSE), FALSE) }) test_that("standardise_ctypes() turns NULL col_types into 'COL_GUESS'", { expect_equal(standardise_ctypes(NULL), c("?" = "COL_GUESS")) }) test_that("standardise_ctypes() errors for only 'COL_SKIP'", { errmsg <- "can't request that all columns be skipped" expect_error(standardise_ctypes("-"), errmsg) expect_error(standardise_ctypes("-_"), errmsg) }) test_that("standardise_ctypes() understands and requires readr shortcodes", { good <- "_-lidnDtTcCL?" expect_equal( standardise_ctypes(good), c(`_` = "COL_SKIP", `-` = "COL_SKIP", l = "CELL_LOGICAL", i = "CELL_INTEGER", d = "CELL_NUMERIC", n = "CELL_NUMERIC", D = "CELL_DATE", t = "CELL_TIME", T = "CELL_DATETIME", c = "CELL_TEXT", C = "COL_CELL", L = "COL_LIST", `?` = "COL_GUESS") ) expect_error(standardise_ctypes("abe"), "Unrecognized") expect_error(standardise_ctypes("f:"), "Unrecognized") expect_error(standardise_ctypes(""), "at least one") }) test_that("col_types of right length are tolerated", { expect_identical(rep_ctypes(1, ctypes = "a"), "a") expect_identical(rep_ctypes(2, ctypes = c("a", "b")), c("a", "b")) expect_identical( rep_ctypes(2, ctypes = c("a", "b", "COL_SKIP")), c("a", "b", "COL_SKIP") ) }) test_that("a single col_types is repeated to requested length", { expect_identical(rep_ctypes(2, ctypes = "a"), c("a", "a")) }) test_that("col_types with length > 1 and != n throw error", { expect_error(rep_ctypes(1, ctypes = rep("a", 2)), "not compatible") expect_error(rep_ctypes(3, ctypes = rep("a", 2)), "not compatible") }) test_that("filter_col_names() removes entries for skipped columns", { expect_identical(filter_col_names(letters[1:2], letters[3:4]), letters[1:2]) expect_identical( filter_col_names(letters[1:3], ctypes = c("a", "COL_SKIP", "c")), letters[c(1, 3)] ) }) googlesheets4/tests/testthat/test-range_write.R0000644000176200001440000001430714074112242021440 0ustar liggesusers# ---- nm_fun ---- me_ <- nm_fun("TEST-range_write") # ---- tests ---- test_that("range_write() works", { skip_if_offline() skip_if_no_token() n <- 3 m <- 5 data <- suppressMessages( # silence messages about name repair tibble::as_tibble( matrix(head(letters, n * m), nrow = n , ncol = m), .name_repair = "unique" ) ) ss <- local_ss(me_(), sheets = list(foo = data)) # this is intentional below: refer to sheet in various ways # write into existing cells --> no size change range_write(ss, data[3:2, ]) props <- sheet_properties(ss) expect_equal(props$grid_rows, n + 1) expect_equal(props$grid_columns, m) df <- read_sheet(ss) expect_identical(df[1, ], df[3, ]) # write into non-existing cells --> sheet must grow range_write(ss, data, range = "foo!F5") props <- sheet_properties(ss) expect_equal(props$grid_rows, (5 - 1) + n + 1) expect_equal(props$grid_columns, (which(LETTERS == "F") - 1) + m) df <- read_sheet(ss, range = cell_cols(c("F", NA))) expect_equal(df, data) # write into existing and non-existing cells --> need new columns range_write(ss, data[1:3], sheet = "foo", range = "I2:K5") props <- sheet_properties(ss) expect_equal(props$grid_columns, (which(LETTERS == "K"))) df <- read_sheet(ss, range = "I2:K5") expect_equal(df, data[1:3]) }) # https://github.com/tidyverse/googlesheets4/issues/203 test_that("we can write a hole-y tibble containing NULLs", { skip_if_offline() skip_if_no_token() dat_write <- tibble::tibble(A = list(NULL, "HI"), B = month.abb[1:2]) ss <- local_ss(me_("write-NULL"), sheets = dat_write) write_sheet(dat_write, ss, sheet = 1) dat_read <- read_sheet(ss) expect_equal(dat_read$A, c(NA, "HI")) expect_equal(dat_read$B, dat_write$B) dat_read <- read_sheet(ss, col_types = "Lc") expect_equal(dat_read$A, dat_write$A) expect_equal(dat_read$B, dat_write$B) }) # ---- helpers ---- test_that("prepare_loc() makes the right call re: `start` vs. `range`", { expect_loc <- function(x, loc) { sheets_df <- tibble::tibble(name = "Sheet1", index = 0, id = 123) out <- prepare_loc(as_range_spec(x, sheets_df = sheets_df)) expect_named(out, loc) } expect_loc(NULL, "start") expect_loc("Sheet1", "start") expect_loc("D4", "start") expect_loc("B5:B5", "start") expect_loc(cell_limits(c(5, 2), c(5, 2)), "start") expect_loc("B4:G9", "range") expect_loc("A2:F", "range") expect_loc("A2:5", "range") expect_loc("C:E", "range") expect_loc("5:7", "range") expect_loc(cell_limits(c(2, 4), c(NA, NA)), "range") }) test_that("prepare_dims() works when write_loc is a `start` (a GridCoordinate)", { n <- 3 m <- 5 data <- suppressMessages( # silence messages about name repair tibble::as_tibble( matrix(head(letters, n * m), nrow = n , ncol = m), .name_repair = "unique" ) ) expect_dims <- function(loc, col_names, dims) { expect_equal(prepare_dims(loc, data, col_names = col_names), dims) } # no row or column info --> default offset is 0 (remember these are 0-indexed) loc <- list(start = new("GridCoordinate", sheetId = 123)) expect_dims(loc, col_names = TRUE, list(nrow = n + 1, ncol = m)) expect_dims(loc, col_names = FALSE, list(nrow = n, ncol = m)) # row offset loc <- list(start = new("GridCoordinate", sheetId = 123, rowIndex = 2)) expect_dims(loc, col_names = TRUE, list(nrow = 2 + n + 1, ncol = m)) expect_dims(loc, col_names = FALSE, list(nrow = 2 + n, ncol = m)) # column offset loc <- list(start = new("GridCoordinate", sheetId = 123, columnIndex = 3)) expect_dims(loc, col_names = TRUE, list(nrow = n + 1, ncol = 3 + m)) expect_dims(loc, col_names = FALSE, list(nrow = n, ncol = 3 + m)) # row and column offset loc <- list( start = new("GridCoordinate", sheetId = 123, rowIndex = 2, columnIndex = 3) ) expect_dims(loc, col_names = TRUE, list(nrow = 2 + n + 1, ncol = 3 + m)) expect_dims(loc, col_names = FALSE, list(nrow = 2 + n, ncol = 3 + m)) }) test_that("prepare_dims() works when write_loc is a `range` (a GridRange)", { n <- 3 m <- 5 data <- suppressMessages( # silence messages about name repair tibble::as_tibble( matrix(head(letters, n * m), nrow = n , ncol = m), .name_repair = "unique" ) ) expect_dims <- function(x, col_names, dims) { sheets_df <- tibble::tibble(name = "Sheet1", index = 0) loc <- prepare_loc(as_range_spec(x, sheets_df = sheets_df)) expect_equal(prepare_dims(loc, data, col_names = col_names), dims) } # fully specified range; lower right cell is all that matters expect_dims("B4:G9", col_names = TRUE, list(nrow = 9, ncol = which(LETTERS == "G"))) expect_dims("B4:G9", col_names = FALSE, list(nrow = 9, ncol = which(LETTERS == "G"))) # range is open on the bottom # get row extent from upper left of range + data, column extent from range expect_dims("B3:D", col_names = TRUE, list(nrow = 2 + n + 1, ncol = which(LETTERS == "D"))) expect_dims("B3:D", col_names = FALSE, list(nrow = 2 + n , ncol = which(LETTERS == "D"))) # range is open on the right # get row extent from range, column extent from range + data expect_dims("C3:5", col_names = TRUE, list(nrow = 5, ncol = which(LETTERS == "C") + m - 1)) expect_dims("C3:5", col_names = FALSE, list(nrow = 5, ncol = which(LETTERS == "C") + m - 1)) # range is open on left (trivially) and on the right # get row extent from range, column extent from the data expect_dims("5:7", col_names = TRUE, list(nrow = 7, ncol = m)) expect_dims("5:7", col_names = FALSE, list(nrow = 7, ncol = m)) # range is open on the top (trivially) and bottom # get row extent from data, column extent from range expect_dims("B:H", col_names = TRUE, list(nrow = n + 1, ncol = which(LETTERS == "H"))) expect_dims("B:H", col_names = FALSE, list(nrow = n, ncol = which(LETTERS == "H"))) # range is open on the bottom and right # get row extent from range + data, column extent from range + data expect_dims( cell_limits(c(2, 4), c(NA, NA)), col_names = TRUE, list(nrow = 2 + n + 1 - 1, ncol = 4 + m - 1) ) expect_dims( cell_limits(c(2, 4), c(NA, NA)), col_names = FALSE, list(nrow = 2 + n - 1, ncol = 4 + m - 1) ) }) googlesheets4/tests/testthat/test-sheet_append.R0000644000176200001440000000077114074077150021601 0ustar liggesusers# ---- nm_fun ---- me_ <- nm_fun("TEST-sheet_append") # ---- tests ---- test_that("sheet_append() works", { skip_if_offline() skip_if_no_token() dat <- tibble::tibble(x = as.numeric(1:10), y = LETTERS[1:10]) ss <- local_ss(me_(), sheets = list(test = dat[0, ])) sheet_append(ss, dat[1, ], sheet = "test") out <- range_read(ss, sheet = "test") expect_equal(out, dat[1, ]) sheet_append(ss, dat[2:10, ], sheet = "test") out <- range_read(ss, sheet = "test") expect_equal(out, dat) }) googlesheets4/tests/testthat/test-request_generate.R0000644000176200001440000000050713564636604022511 0ustar liggesuserstest_that("can generate a basic request", { req <- request_generate( "sheets.spreadsheets.get", list(spreadsheetId = "abc123"), token = NULL ) expect_identical(req$method, "GET") expect_match( req$url, "^https://sheets.googleapis.com/v4/spreadsheets/abc123\\?key=.+" ) expect_null(req$token) }) googlesheets4/tests/testthat/test-utils-cell-ranges.R0000644000176200001440000001717014074074641022477 0ustar liggesusers# sq_escape() and sq_unescape() ---- test_that("sq_escape() and sq_unescape() pass NULL through", { expect_null(sq_escape(NULL)) expect_null(sq_unescape(NULL)) }) test_that("sq_escape() does nothing if string already single-quoted", { x <- c("'abc'", "'ab'c'", "''") expect_identical(sq_escape(x), x) }) test_that("sq_escape() duplicates single quotes and adds to start, end", { expect_identical( sq_escape(c( "abc", "'abc", "abc'", "'a'bc", "'")), c("'abc'", "'''abc'", "'abc'''", "'''a''bc'", "''''") ) }) test_that("sq_unescape() does nothing if string is not single-quoted", { x <- c("abc", "'abc", "abc'", "a'bc", "'a'bc") expect_identical(sq_unescape(x), x) }) test_that("sq_unescape() strips outer single quotes, de-duplicates inner", { expect_identical( sq_unescape(c("'abc'", "'''abc'", "'abc'''", "'''a''bc'", "''''")), c( "abc", "'abc", "abc'", "'a'bc", "'") ) }) # qualified_A1() ---- test_that("qualified_A1 works", { expect_null(qualified_A1()) expect_identical(qualified_A1("foo"), "'foo'") expect_identical(qualified_A1("foo", "A1"), "'foo'!A1") expect_identical(qualified_A1("'foo'"), "'foo'") expect_identical(qualified_A1(cell_range = "A1"), "A1") }) # lookup_sheet_name() ---- test_that("lookup_sheet_name() requires sheet to be length-1 character or numeric", { expect_error(lookup_sheet_name(c("a", "b")), "length 1") expect_error(lookup_sheet_name(1:2), "length 1") expect_error(lookup_sheet_name(TRUE), "must be either") }) test_that("lookup_sheet_name() errors if number is incompatible with sheet names", { sheets_df <- tibble::tibble(name = c("a", "foo", "z")) expect_error(lookup_sheet_name(4, sheets_df), "out-of-bounds") expect_error(lookup_sheet_name(0, sheets_df), "out-of-bounds") }) test_that("lookup_sheet_name() consults sheet names, if given", { sheets_df <- tibble::tibble(name = c("a", "foo", "z")) expect_identical(lookup_sheet_name("foo", sheets_df), "foo") expect_error( lookup_sheet_name("nope", sheets_df), class = "googlesheets4_error_sheet_not_found" ) }) test_that("lookup_sheet_name() works with a number", { sheets_df <- tibble::tibble(name = c("a", "foo", "z")) expect_identical(lookup_sheet_name(2, sheets_df), "foo") }) # resolve_limits() ---- test_that("resolve_limits() leaves these cases unchanged", { expect_no_change <- function(cl) expect_identical(resolve_limits(cl), cl) expect_no_change(cell_limits(c(2, 2), c(3, 3))) expect_no_change(cell_limits(c(NA, NA), c(NA, NA))) expect_no_change(cell_limits(c(2, NA), c(3, NA))) expect_no_change(cell_limits(c(NA, 2), c(NA, 3))) expect_no_change(cell_limits(c(2, 2), c(3, NA))) expect_no_change(cell_limits(c(2, 2), c(NA, 3))) }) test_that("resolve_limits() completes a row- or column-only range", { expect_identical( resolve_limits(cell_limits(c(2, NA), c( NA, NA))), cell_limits(c(2, NA), c(5000000, NA)) ) expect_identical( # I now think it's a bug that cell_limits() fills in this start row resolve_limits(cell_limits(c(NA, NA), c(3, NA))), cell_limits(c(1, NA), c(3, NA)) ) expect_identical( resolve_limits(cell_limits(c(NA, 2), c(NA, NA))), cell_limits(c(NA, 2), c(NA, 18278)) ) expect_identical( # I now think it's a bug that cell_limits() fills in this start column resolve_limits(cell_limits(c(NA, NA), c(NA, 3))), cell_limits(c(NA, 1), c(NA, 3)) ) }) test_that("resolve_limits() completes upper left cell", { expect_identical( resolve_limits(cell_limits(c(2, NA), c(NA, 3))), cell_limits(c(2, 1), c(NA, 3)) ) expect_identical( resolve_limits(cell_limits(c(NA, 2), c(3, NA))), cell_limits(c( 1, 2), c(3, NA)) ) expect_identical( resolve_limits(cell_limits(c(NA, NA), c(3, 3))), cell_limits(c( 1, 1), c(3, 3)) ) expect_identical( resolve_limits(cell_limits(c(2, NA), c(3, 3))), cell_limits(c(2, 1), c(3, 3)) ) expect_identical( resolve_limits(cell_limits(c(NA, 2), c(3, 3))), cell_limits(c( 1, 2), c(3, 3)) ) }) test_that("resolve_limits() populates column of lower right cell", { expect_identical( resolve_limits(cell_limits(c(2, 2), c(NA, NA))), cell_limits(c(2, 2), c(NA, 18278)) ) }) # as_sheets_range ---- ## "case numbers" refer to output produced by: # tidyr::crossing( # start_row = c(NA, "start_row"), start_col = c(NA, "start_col"), # end_row = c(NA, "end_row"), end_col = c(NA, "end_col") # ) test_that("as_sheets_range() works when all limits are given", { # 1 start_row start_col end_row end_col expect_identical( as_sheets_range(cell_limits(c(2, 2), c(3, 3))), "B2:C3" ) }) test_that("as_sheets_range() returns NULL when all limits are NA", { # 16 NA NA NA NA expect_null(as_sheets_range(cell_limits())) }) test_that("as_sheets_range() deals with row-only range", { # 6 start_row NA end_row NA expect_identical( as_sheets_range(cell_limits(c(2, NA), c(3, NA))), "2:3" ) }) test_that("as_sheets_range() deals with column-only range", { # 11 NA start_col NA end_col expect_identical( as_sheets_range(cell_limits(c(NA, 2), c(NA, 3))), "B:C" ) }) test_that("as_sheets_range() deals when one of lr limits is missing", { # 2 start_row start_col end_row NA # 3 start_row start_col NA end_col expect_identical( as_sheets_range(cell_limits(c(2, 2), c(3, NA))), "B2:3" ) expect_identical( as_sheets_range(cell_limits(c(2, 2), c(NA, 3))), "B2:C" ) }) # TODO: I disabled this when switching to testthat 3e # removing the mock of cellranger::cell_limits() didn't seem to hurt anything, # which seems odd # # I have to think about cellranger soon, so revisit this when that happens # # commenting out, not skipping, because this is the only with_mock() #test_that("as_sheets_range() errors for limits that should be fixed by resolve_limits()", { # I think cellranger::cell_limits() should do much less. # Already planning here for such a change there. # Here's a very crude version of what I have in mind. # cl <- function(ul, lr) { # structure( # list(ul = as.integer(ul), lr = as.integer(lr), sheet = NA_character_), # class = c("cell_limits", "list") # ) # } # with_mock( # resolve_limits = function(x) x, # `cellranger:::cell_limits` = function(ul, lr, sheet) cl(ul, lr), { # # 5 start_row NA end_row end_col # expect_error(as_sheets_range(cell_limits(c(2, NA), c(3, 3)))) # # 9 NA start_col end_row end_col # expect_error(as_sheets_range(cell_limits(c(NA, 2), c(3, 3)))) # # 13 NA NA end_row end_col # expect_error(as_sheets_range(cell_limits(c(NA, NA), c(3, 3)))) # # 8 start_row NA NA NA # expect_error(as_sheets_range(cell_limits(c(2, NA), c(NA, NA)))) # # 14 NA NA end_row NA # expect_error(as_sheets_range(cell_limits(c(NA, NA), c(2, NA)))) # # 12 NA start_col NA NA # expect_error(as_sheets_range(cell_limits(c(NA, 2), c(NA, NA)))) # # 15 NA NA NA end_col # expect_error(as_sheets_range(cell_limits(c(NA, NA), c(NA, 3)))) # # 10 NA start_col end_row NA # expect_error(as_sheets_range(cell_limits(c(NA, 2), c(3, NA)))) # # 7 start_row NA NA end_col # expect_error(as_sheets_range(cell_limits(c(2, NA), c(NA, 3)))) # # 4 start_row start_col NA NA # expect_error(as_sheets_range(cell_limits(c(2, 2), c(NA, NA)))) # } # ) # }) googlesheets4/tests/testthat/helper.R0000644000176200001440000000213014074121022017417 0ustar liggesusersauth_success <- tryCatch( gs4_auth_testing(), googlesheets4_auth_internal_error = function(e) NULL ) if (!isTRUE(auth_success)) { gs4_bullets(c( "!" = "Internal auth failed; calling {.fun gs4_deauth}." )) gs4_deauth() } skip_if_no_token <- function() { if (gs4_has_token()) { # hack to slow things down in CI Sys.sleep(3) } else { skip("No token") } } expect_error_free <- function(...) { expect_error(..., regexp = NA) } expect_gs4_error <- function(...) { expect_error(..., class = "googlesheets4_error") } local_ss <- function(name, ..., env = parent.frame()) { existing <- gs4_find(name) if (nrow(existing) > 0) { gs4_abort("A spreadsheet named {.s_sheet name} already exists.") } withr::defer({ trash_me <- gs4_find(name) if (nrow(trash_me) < 1) { cli::cli_warn(" The spreadsheet named {.s_sheet name} already seems to be deleted.") } else { quiet <- gs4_quiet() %|% is_testing() if (quiet) googledrive::local_drive_quiet() googledrive::drive_trash(trash_me) } }, envir = env) gs4_create(name, ...) } googlesheets4/tests/testthat/_snaps/0000755000176200001440000000000014074371740017322 5ustar liggesusersgooglesheets4/tests/testthat/_snaps/sheet_add.md0000644000176200001440000000034214075373703021565 0ustar liggesusers# sheet_add() rejects non-character `sheet` Code sheet_add(test_sheet("googlesheets4-cell-tests"), sheet = 3) Error `sheet` must be : x `sheet` has class . googlesheets4/tests/testthat/_snaps/argument-checkers.md0000644000176200001440000000144414075373535023263 0ustar liggesusers# col_names must be logical or character and have length Code check_col_names(1:3) Error `col_names` must be : x `col_names` has class . --- Code check_col_names(factor("a")) Error `col_names` must be : x `col_names` has class . --- Code check_col_names(character()) Error `col_names` must have length greater than zero. # logical col_names must be TRUE or FALSE Code check_col_names(NA) Error `col_names` must be either `TRUE` or `FALSE`. --- Code check_col_names(c(TRUE, FALSE)) Error `col_names` must be either `TRUE` or `FALSE`. googlesheets4/tests/testthat/_snaps/sheets_id-class.md0000644000176200001440000000647014075374030022721 0ustar liggesusers# string with invalid character is rejected Code as_sheets_id("abc&123") Error A must match this regular expression: `^[a-zA-Z0-9_-]+$` Invalid input: x 'abc&123' # invalid inputs are caught Code as_sheets_id(letters[1:2]) Error A object can't have length greater than 1. x Actual input has length 2. # multi-row dribble is rejected Code as_sheets_id(d) Error input must have exactly 1 row. x Actual input has 2 rows. # dribble with non-Sheet file is rejected Code as_sheets_id(d) Error input must refer to a Google Sheet, i.e. a file with MIME type 'application/vnd.google-apps.spreadsheet'. i File name: "chicken.txt" i File id: '1wOLeWVRkTb6lDmLRiOhg9iKM7DlN762Y' x MIME TYPE: 'text/plain' # sheets_id print method reveals metadata Code print(gs4_example("gapminder")) Output Spreadsheet name: gapminder ID: 1U6Cf_qEOhiR9AZqTqS3mbMF3zt2db48ZP5v3rkrAEJY Locale: en_US Time zone: America/Los_Angeles # of sheets: 5 # of named ranges: 1 (Sheet name): (Nominal extent in rows x columns) Africa: 625 x 6 Americas: 301 x 6 Asia: 397 x 6 Europe: 361 x 6 Oceania: 25 x 6 (Named range): (A1 range) canada: 'Americas'!A38:F49 # sheets_id print method doesn't error for nonexistent ID Code as_sheets_id("12345") Output Spreadsheet name: ID: 12345 Locale: Time zone: # of sheets: Unable to get metadata for this Sheet. Error details: Client error: (404) NOT_FOUND * A specified resource is not found, or the request is rejected by undisclosed reasons, such as whitelisting. * Requested entity was not found. # can print public sheets_id if deauth'd Code print(gs4_example("mini-gap")) Output Spreadsheet name: mini-gap ID: 1k94ZVVl6sdj0AXfK9MQOuQ4rOhd1PULqpAu2_kr9MAU Locale: en_US Time zone: America/Los_Angeles # of sheets: 5 (Sheet name): (Nominal extent in rows x columns) Africa: 6 x 6 Americas: 6 x 6 Asia: 6 x 6 Europe: 6 x 6 Oceania: 6 x 6 # sheets_id print does not error for lack of cred Code print(gs4_example("mini-gap")) Output Spreadsheet name: ID: 1k94ZVVl6sdj0AXfK9MQOuQ4rOhd1PULqpAu2_kr9MAU Locale: Time zone: # of sheets: Unable to get metadata for this Sheet. Error details: Can't get Google credentials. i Are you running googlesheets4 in a non-interactive session? Consider: * Call `gs4_deauth()` to prevent the attempt to get credentials. * Call `gs4_auth()` directly with all necessary specifics. i See gargle's "Non-interactive auth" vignette for more details: i googlesheets4/tests/testthat/_snaps/utils-ui.md0000644000176200001440000000020714075374031021413 0ustar liggesusers# abort_unsupported_conversion() works Don't know how to make an instance of from something of class . googlesheets4/tests/testthat/_snaps/schemas.md0000644000176200001440000000123614075373672021300 0ustar liggesusers# new() rejects data not expected for schema Code new("Spreadsheet", foofy = "blah") Error Properties not recognized for the 'Spreadsheet' schema: x 'foofy' --- Code new("Spreadsheet", foofy = "blah", foo = "bar") Error Properties not recognized for the 'Spreadsheet' schema: x 'foofy' x 'foo' # check_against_schema() errors when no schema can be found Code check_against_schema(x) Error Trying to check an object of class , but can't get a schema. googlesheets4/tests/testthat/test-sheet_write.R0000644000176200001440000000405414074074641021464 0ustar liggesusers# ---- nm_fun ---- me_ <- nm_fun("TEST-sheet_write") # ---- tests ---- test_that("sheet_write() writes what it should", { skip_if_offline() skip_if_no_token() dat <- range_read( test_sheet("googlesheets4-col-types"), sheet = "lots-of-types", col_types = "lccinDT" # TODO: revisit when 'f' means factor ) dat$factor <- factor(dat$factor) ss <- local_ss(me_("datetimes")) sheet_write(dat, ss) x <- range_read(ss, sheet = "dat", col_types = "C") # the main interesting bit to test is whether we successfully sent # correct value for the date and datetime, with a sane (= ISO 8601) format expect_equal( purrr::pluck(x, "date", 1, "formattedValue"), format(dat$date[1]) ) expect_equal( purrr::pluck(x, "date", 1, "effectiveFormat", "numberFormat", "type"), "DATE" ) expect_equal( purrr::pluck(x, "date", 1, "effectiveFormat", "numberFormat", "pattern"), "yyyy-mm-dd" ) expect_equal( purrr::pluck(x, "datetime", 1, "formattedValue"), format(dat$datetime[1]) ) expect_equal( purrr::pluck(x, "datetime", 1, "effectiveFormat", "numberFormat", "type"), "DATE_TIME" ) expect_equal( purrr::pluck(x, "datetime", 1, "effectiveFormat", "numberFormat", "pattern"), "yyyy-mm-dd hh:mm:ss" ) }) test_that("sheet_write() can figure out (work)sheet name", { skip_if_offline() skip_if_no_token() foofy <- data.frame(x = 1:3, y = letters[1:3]) ss <- local_ss(me_("sheetnames")) # get (work)sheet name from data frame's name sheet_write(foofy, ss) expect_equal(tail(sheet_names(ss), 1), "foofy") # we don't clobber existing (work)sheet if name was inferred sheet_write(foofy, ss) expect_equal(tail(sheet_names(ss), 1), "Sheet2") # we do write into existing (work)sheet if name is explicitly given sheet_write(foofy, ss, sheet = "foofy") expect_setequal(sheet_names(ss), c("Sheet1", "Sheet2", "foofy")) # we do write into existing (work)sheet if position is explicitly given sheet_write(foofy, ss, sheet = 2) expect_setequal(sheet_names(ss), c("Sheet1", "Sheet2", "foofy")) }) googlesheets4/tests/testthat/test-ctype.R0000644000176200001440000000405614074074641020270 0ustar liggesuserstest_that("ctype() errors for unanticipated inputs", { expect_error(ctype(NULL)) expect_error(ctype(data.frame(cell = "cell"))) }) test_that("ctype() works on a SHEET_CELL, when it should", { expect_identical( ctype(structure(1, class = c("wut", "SHEETS_CELL"))), NA_character_ ) expect_identical( ctype(structure(1, class = c("CELL_NUMERIC", "SHEETS_CELL"))), "CELL_NUMERIC" ) }) test_that("ctype() works on shortcodes, when it should", { expect_equal( unname(ctype(c("?", "-", "n", "z", "D"))), c("COL_GUESS", "COL_SKIP", "CELL_NUMERIC", NA, "CELL_DATE") ) }) test_that("ctype() works on lists, when it should", { list_of_cells <- list( structure(1, class = c("CELL_NUMERIC", "SHEETS_CELL")), "nope", NULL, structure(1, class = c("wut", "SHEETS_CELL")), structure(1, class = c("CELL_TEXT", "SHEETS_CELL")) ) expect_equal( ctype(list_of_cells), c("CELL_NUMERIC", NA, NA, NA, "CELL_TEXT") ) }) test_that("effective_cell_type() doesn't just pass ctype through", { ## neither the API nor JSON has a proper way to convey integer-ness expect_equal(unname(effective_cell_type("CELL_INTEGER")), "CELL_NUMERIC") ## conversion to date or time is lossy, so never guess that expect_equal(unname(effective_cell_type("CELL_DATE")), "CELL_DATETIME") expect_equal(unname(effective_cell_type("CELL_TIME")), "CELL_DATETIME") }) test_that("consensus_col_type() implements our type coercion DAG", { expect_identical( consensus_col_type(c("CELL_TEXT", "CELL_TEXT")), "CELL_TEXT" ) expect_identical( consensus_col_type(c("CELL_LOGICAL", "CELL_NUMERIC")), "CELL_NUMERIC" ) expect_identical( consensus_col_type(c("CELL_LOGICAL", "CELL_DATE")), "COL_LIST" ) expect_identical( consensus_col_type(c("CELL_DATE", "CELL_DATETIME")), "CELL_DATETIME" ) expect_identical( consensus_col_type(c("CELL_TEXT", "CELL_BLANK")), "CELL_TEXT" ) expect_identical(consensus_col_type("CELL_TEXT"), "CELL_TEXT") expect_identical(consensus_col_type("CELL_BLANK"), "CELL_LOGICAL") }) googlesheets4/tests/testthat/test-utils-ui.R0000644000176200001440000000126514074134430020707 0ustar liggesuserstest_that("gs4_quiet() falls back to NA if googlesheets4_quiet is unset", { withr::with_options( list(googlesheets4_quiet = NULL), expect_true(is.na(gs4_quiet())) ) }) test_that("gs4_abort() throws classed condition", { expect_error(gs4_abort("oops"), class = "googlesheets4_error") expect_gs4_error(gs4_abort("oops")) expect_gs4_error(gs4_abort("oops", class = "googlesheets4_foo")) expect_error( gs4_abort("oops", class = "googlesheets4_foo"), class = "googlesheets4_foo" ) }) test_that("abort_unsupported_conversion() works", { x <- structure(1, class = c("a", "b", "c")) expect_snapshot_error( abort_unsupported_conversion(x, "target_class") ) }) googlesheets4/tests/testthat/test-gs4_formula.R0000644000176200001440000000305014074074641021357 0ustar liggesuserstest_that("constructors return length-0 vector when called with no arguments", { expect_length(new_formula(), 0) expect_length(gs4_formula(), 0) }) test_that("low-level constructor errors for non-character input", { expect_error(new_formula(1:3), class = "vctrs_error_assert_ptype") }) test_that("user-friendly constructor works for coercible input", { expect_s3_class( gs4_formula(factor("=sum(A:A)")), "googlesheets4_formula" ) }) test_that("common type of googlesheets4_formula and character is character", { expect_identical( vctrs::vec_ptype2(character(), gs4_formula()), character() ) expect_identical( vctrs::vec_ptype2(gs4_formula(), character()), character() ) }) test_that("googlesheets4_formula and character are coercible", { expect_identical( vctrs::vec_cast("=sum(A:A)", gs4_formula()), gs4_formula("=sum(A:A)") ) expect_identical( vctrs::vec_cast(gs4_formula("=sum(A:A)"), character()), "=sum(A:A)" ) expect_identical( vctrs::vec_cast(gs4_formula("=sum(A:A)"), gs4_formula()), gs4_formula("=sum(A:A)") ) }) test_that("can concatenate googlesheets4_formula", { expect_identical( vctrs::vec_c( gs4_formula("=sum(A:A)"), gs4_formula("=sum(B:B)") ), gs4_formula(c("=sum(A:A)", "=sum(B:B)")) ) }) test_that("googlesheets4_formula can have missing elements", { out <- vctrs::vec_c( gs4_formula("=sum(A:A)"), NA, gs4_formula("=min(B2:G7"), NA ) expect_s3_class(out, "googlesheets4_formula") expect_true(all(is.na(out[c(2, 4)]))) }) googlesheets4/tests/testthat/test-sheet_copy.R0000644000176200001440000000144214074074641021302 0ustar liggesusers# ---- nm_fun ---- me_ <- nm_fun("TEST-sheet_copy") # ---- tests ---- test_that("internal copy works", { skip_if_offline() skip_if_no_token() ss <- local_ss( me_("internal"), sheets = list(iris = head(iris), chickwts = head(chickwts)) ) sheet_copy(ss, to_sheet = "xyz", .after = 1) out <- sheet_names(ss) expect_equal(out, c("iris", "xyz", "chickwts")) }) test_that("external copy works", { skip_if_offline() skip_if_no_token() ss_source <- local_ss( me_("source"), sheets = list(iris = head(iris), chickwts = head(chickwts)) ) ss_dest <- local_ss(me_("dest")) sheet_copy( ss_source, from_sheet = "chickwts", to_ss = ss_dest, to_sheet = "chicks-two", .before = 1 ) out <- sheet_names(ss_dest) expect_equal(out, c("chicks-two", "Sheet1")) }) googlesheets4/tests/testthat/test-sheet_relocate.R0000644000176200001440000000102014074074641022116 0ustar liggesusers# ---- nm_fun ---- me_ <- nm_fun("TEST-sheet_relocate") # ---- tests ---- test_that("relocation works", { skip_if_offline() skip_if_no_token() sheet_names <- c("alfa", "bravo", "charlie", "delta", "echo", "foxtrot") ss <- local_ss(me_(), sheets = sheet_names) sheet_relocate(ss, "echo", .before = "bravo") sheet_relocate(ss, list("foxtrot", 4)) sheet_relocate(ss, c("bravo", "alfa", "echo"), .after = 10) expect_equal( sheet_names(ss), c("foxtrot", "charlie", "delta", "bravo", "alfa", "echo") ) }) googlesheets4/tests/testthat/test-gs4_example.R0000644000176200001440000000322714075323365021354 0ustar liggesuserstest_that("gs4_examples() lists all examples, in a named drive_id object", { skip_if_offline() skip_on_cran() examples <- gs4_examples() expect_true(is.character(examples)) expect_true(length(examples) > 0) expect_true(is.character(names(examples))) expect_s3_class(examples, "drive_id") }) test_that("gs4_example() returns a sheets_id", { skip_if_offline() skip_on_cran() expect_s3_class(gs4_example("deaths"), "sheets_id") }) test_that("gs4_examples() requires a match if `matches` is supplied", { skip_if_offline() skip_on_cran() expect_error(gs4_examples("nope"), "Can't find") }) test_that("gs4_example() requires `matches`", { skip_if_offline() skip_on_cran() expect_error(gs4_example(), "missing") }) test_that("`matches` works in gs4_examples()", { skip_if_offline() skip_on_cran() examples <- gs4_examples("gap") expect_true(length(examples) > 0) expect_s3_class(examples, "drive_id") }) test_that("`matches` works in gs4_example()", { skip_if_offline() skip_on_cran() expect_error_free( example <- gs4_example("gapminder") ) expect_length(example, 1) expect_s3_class(example, "drive_id") expect_s3_class(example, "sheets_id") }) test_that("gs4_example() requires a unique match", { skip_if_offline() skip_on_cran() expect_error(gs4_example("gap"), "multiple") }) test_that("example functions work when deauth'd", { skip_if_offline() skip_on_cran() examples <- gs4_examples() gapminder <- gs4_example("gapminder") local_deauth() env_bind(.googlesheets4, example_and_test_sheets = zap()) expect_equal(gs4_examples(), examples) expect_equal(gs4_example("gapminder"), gapminder) }) googlesheets4/tests/testthat/ref/0000755000176200001440000000000014074074641016613 5ustar liggesusersgooglesheets4/tests/testthat/ref/dribble.rds0000644000176200001440000000332614075052625020733 0ustar liggesusersYYs6t2dھE]g<؎uX4S゗D2lCS,vX.KXl:6;E3uf~.[ 2α!e$$pT6SUj5uU̩~z!Lڨ{4$clTUv *I $VpJpS`8!&2EDXtǢawh} )$}O CS55Ȓ&H߀oPt9+RfTre٦!d휂e6+Yz{N5 r2WgRt =h %lc2Jv9(+sMqѭJF&=rI2\\`g9q\p6"=ܽ$#Ch@60m'<#۔A\}Ywz>˯L&T;9;eaC" e"+PHE:O_YX藱y/>ߘohc^z]F5#!W5-wm+('Ү hm ݖh /j0  GvwhNz`أtU";nKm*l~H51$."jED;D4Li|8$P=Ԉ^@ ?,Rj;_:?BYvb\ N6< l,خK5 cv|CS&- Ȧ.&31ކ؊V{[0A_!c l0c1vάp\7FlJ]W0q#:u;UaG<_16QpG "h1IOIѓꥥ'Sɲ{m Ce@m lul%cD.%AM#3}s%d*ʷBn[-ՏJ1H2ֱZjs{9CVBFt(\41V{wQm׷ΏrdrUv=ǯ5O$&A@҄qpz+|V Mo2l*J0n㡆o }([LCl1ѲŘgNϼ4fr`=8|:M]dz)Ptdz[U+gի >)Jˍw\hŃJdUnRрOt`ӟ,zQ`ǭ*fv$(R} jmjU xABNhݸmuA{ZI*MLS>ot?1,L`Y84krNbq1!D=?3"esKuTBuCC?$W<_r뀮w,fO.NozHKP.03T)1Ř$ e䗧T*B#с `gdBY,Iz Bn O7T,3NOOE/5E/5* sq q B<}]QTa F@8 K2sˋ|Y_!/(0@6+%$Q/cgooglesheets4/tests/testthat/test-utils.R0000644000176200001440000000343414074074641020303 0ustar liggesuserstest_that("check_length_one() works", { expect_error_free(check_length_one(1)) expect_error(check_length_one(1:2), "must have length 1") expect_error(check_length_one(letters), "letters") }) test_that("check_character() works", { expect_error_free(check_character(letters)) expect_error(check_character(1:2), "integer") }) test_that("vlookup() works", { df <- tibble::tibble( i = 1:3, letters = letters[i], dupes = c("a", "c", "c"), fctr = factor(letters) ) ## internal function, therefore it does not support unquoted variable names ## R <= 3.4.4 error msg is "object 'i' not found" ## R devel error msg is "is_string(key) is not TRUE" expect_error(vlookup("c", df, letters, i)) expect_identical(vlookup("c", df, "letters", "i"), 3L) expect_identical(vlookup(c("a", "c"), df, "letters", "i"), c(1L, 3L)) ## match() returns position of *first* match expect_identical(vlookup("c", df, "dupes", "i"), 2L) expect_identical(vlookup(c("c", "c"), df, "dupes", "i"), c(2L, 2L)) expect_identical(vlookup("b", df, "fctr", "i"), 2L) expect_identical(vlookup(c("b", "c", "a"), df, "fctr", "i"), c(2L, 3L, 1L)) }) test_that("enforce_na() works", { expect_error(enforce_na(1), "is.character(x) is not TRUE", fixed = TRUE) expect_error(enforce_na("a", 1), "is.character(na) is not TRUE", fixed = TRUE) expect_identical(enforce_na(character()), character()) expect_identical( enforce_na(c("a", "", "c")), c("a", NA, "c") ) expect_identical( enforce_na(c("a", "", "c"), na = "c"), c("a", "", NA) ) expect_identical( enforce_na(c("abc", "", "cab"), na = c("abc", "")), c( NA, NA, "cab") ) expect_identical( enforce_na(c("a", "", "c"), na = character()), c("a", "", "c") ) }) googlesheets4/tests/testthat/test-sheet_resize.R0000644000176200001440000000414314074074641021632 0ustar liggesusers# ---- nm_fun ---- me_ <- nm_fun("TEST-sheet_resize") # ---- tests ---- test_that("sheet_resize() works", { skip_if_offline() skip_if_no_token() ss <- local_ss(me_()) local_gs4_loud() # no resize occurs expect_message(sheet_resize(ss, nrow = 2, ncol = 6), "No need") %>% suppressMessages() # reduce sheet size suppressMessages(sheet_resize(ss, nrow = 5, ncol = 7, exact = TRUE)) props <- sheet_properties(ss) expect_equal(props$grid_rows, 5) expect_equal(props$grid_columns, 7) }) test_that("prepare_resize_request() works for resize & no resize", { n <- 3 m <- 5 sheet_info <- list(grid_rows = n, grid_columns = m) # (n - 1, n, n + 1) x (m - 1, m, m + 1) x (TRUE, FALSE) # 3 * 3 * 2 = 18 combinations # exact = FALSE df <- expand.grid(nrow_needed = n + -1:1, ncol_needed = m + -1:1, exact = FALSE) req <- pmap(df, prepare_resize_request, sheet_info = sheet_info) grid_properties <- purrr::map( req, c("updateSheetProperties", "properties", "gridProperties") ) # sheet is big enough --> no resize request purrr::walk( grid_properties[df$nrow_needed <= n & df$ncol_needed <= m], expect_null ) # not enough rows purrr::walk( grid_properties[df$nrow_needed > n], ~ expect_true(has_name(.x, "rowCount")) ) # not enough columns purrr::walk( grid_properties[df$ncol_needed > m], ~ expect_true(has_name(.x, "columnCount")) ) # exact = TRUE df <- expand.grid(nrow_needed = n + -1:1, ncol_needed = m + -1:1, exact = TRUE) req <- pmap(df, prepare_resize_request, sheet_info = sheet_info) grid_properties <- purrr::map( req, c("updateSheetProperties", "properties", "gridProperties") ) # sheet has correct size --> no resize request purrr::walk( grid_properties[df$nrow_needed == n & df$ncol_needed == m], expect_null ) # not enough rows or too many rows purrr::walk( grid_properties[df$nrow_needed != n], ~ expect_true(has_name(.x, "rowCount")) ) # not enough columns or too many columns purrr::walk( grid_properties[df$ncol_needed != m], ~ expect_true(has_name(.x, "columnCount")) ) }) googlesheets4/tests/testthat/test-schemas.R0000644000176200001440000000603014074074641020561 0ustar liggesuserstest_that("new() errors for non-existing id", { expect_error(new("I_don't_exist"), "Can't find") }) test_that("new() works (and doesn't require data)", { out <- new("Spreadsheet") expect_length(out, 0) expect_s3_class(out, "googlesheets4_schema_Spreadsheet") expect_s3_class(out, "googlesheets4_schema") expect_s3_class(attr(out, "schema"), "tbl_df") }) test_that("new() accepts data expected for schema", { out <- new("Spreadsheet", spreadsheetId = "abc") expect_identical(out$spreadsheetId, "abc") }) test_that("new() rejects data not expected for schema", { expect_snapshot( new("Spreadsheet", foofy = "blah"), error = TRUE ) expect_snapshot( new("Spreadsheet", foofy = "blah", foo = "bar"), error = TRUE ) }) test_that("new() ignores NULL-valued inputs", { out <- new("GridRange", sheetId = 123, startRowIndex = 2, endRowIndex = NULL) expect_false(has_name(out, "endRowIndex")) }) test_that("patch() fails informatively for non-schema input", { expect_error(patch(1), "Don't know how") }) test_that("patch() with no data passes input through", { out <- new("Spreadsheet", spreadsheetId = "abc") expect_identical(out, patch(out)) }) test_that("patch() accepts data expected for schema", { expect_identical( new("Spreadsheet", spreadsheetId = "abc"), new("Spreadsheet") %>% patch(spreadsheetId = "abc") ) }) test_that("patch() rejects data not expected for schema", { x <- new("Spreadsheet") expect_error(patch(x, foofy = "blah"), "not recognized") }) test_that("patch() overwrites existing data", { x <- new("Spreadsheet", spreadsheetId = "abc") x <- patch(x, spreadsheetId = "xyz") expect_identical(x$spreadsheetId, "xyz") expect_length(x, 1) }) test_that("patch() retains classes", { x <- new("Spreadsheet") classes_in <- class(x) x <- patch(x, spreadsheetId = "abc") classes_out <- class(x) expect_identical(classes_in, classes_out) }) test_that("patch() ignores NULL-valued inputs", { out <- new("GridRange", sheetId = 123) %>% patch(startRowIndex = 2, endRowIndex = NULL) expect_false(has_name(out, "endRowIndex")) }) test_that("check_against_schema() errors when no schema can be found", { x <- structure( list(google_thing = "a"), class = c("googlesheets4_schema_SomeThing", "googlesheets4_schema", "list") ) expect_snapshot( check_against_schema(x), error = TRUE ) }) test_that("id_from_class() works when schema class is present", { x <- structure( list(google_thing = "a"), class = c("googlesheets4_schema_SomeThing", "googlesheets4_schema", "list") ) expect_equal(id_from_class(x), "SomeThing") }) test_that("check_against_schema() errors if names aren't unique", { expect_error( check_against_schema( list(spreadsheetId = "abc", spreadsheetId = "def"), id = "Spreadsheet" ), "is_dictionaryish(x) is not TRUE", fixed = TRUE ) }) test_that("id_from_class() returns NA when schema class is absent", { x <- structure(list(google_thing = "a"), class = "list") expect_equal(id_from_class(x), NA_character_) }) googlesheets4/tests/testthat/test-range_flood.R0000644000176200001440000000222314074112220021377 0ustar liggesusers# ---- nm_fun ---- me_ <- nm_fun("TEST-range_flood") # ---- tests ---- test_that("range_flood() works", { skip_if_offline() skip_if_no_token() dat <- tibble::tibble(x = rep(1, 3), y = rep(2, 3), z = rep(3, 3)) ss <- local_ss(me_(), sheets = list(dat)) # clear values and format range_flood(ss, range = "A:A") # reset values and reformat range_flood(ss, range = "B:B", cell = "hi") # reset values, leave format unchanged range_flood(ss, range = "C:C", cell = "bye", reformat = FALSE) out <- range_read_cells(ss, cell_data = "full", discard_empty = FALSE) expect_equal( purrr::map_chr(out$cell, "formattedValue", .default = ""), rep(c("", "hi", "bye"), 4) ) column_A <- out[out$col == 1, ] fmts <- purrr::map(column_A$cell, "effectiveFormat") expect_true(all(purrr::map_lgl(fmts, is.null))) column_B <- out[out$col == 2, ] fmts <- purrr::map(column_B$cell, c("effectiveFormat", "backgroundColor")) expect_true(all(unlist(fmts) == 1)) column_C_header <- out[out$col == 3 & out$row == 1, ] fmt <- purrr::pluck(column_C_header, "cell", 1, "effectiveFormat", "backgroundColor") expect_true(all(unlist(fmt) < 1)) }) googlesheets4/tests/testthat/test-schema_GridCoordinate.R0000644000176200001440000000256414074074641023363 0ustar liggesuserstest_that("we can make a GridCoordinate from a range_spec, simplest case", { sheets_df <- tibble::tibble(name = "abc", id = 123) spec <- new_range_spec(sheet_name = "abc", sheets_df = sheets_df) out <- as_GridCoordinate(spec) expect_equal(out$sheetId, 123) expect_length(out, 1) spec <- new_range_spec( sheet_name = "abc", cell_range = "G3", sheets_df = sheets_df ) out <- as_GridCoordinate(spec) expect_equal(out$rowIndex, 2) expect_equal(out$columnIndex, 6) }) test_that("we can (or won't) make a GridCoordinate from a mutli-cell range", { sheets_df <- tibble::tibble(name = "abc", id = 123) spec <- new_range_spec( sheet_name = "abc", cell_range = "A3:B4", sheets_df = sheets_df ) expect_error(as_GridCoordinate(spec), "Invalid cell range") spec2 <- new_range_spec( sheet_name = "abc", cell_range = "A3", sheets_df = sheets_df ) expect_equal( as_GridCoordinate(spec, strict = FALSE), as_GridCoordinate(spec2) ) spec <- new_range_spec( sheet_name = "abc", cell_range = "A:B", sheets_df = sheets_df ) out <- as_GridCoordinate(spec, strict = FALSE) expect_null(out$rowIndex) expect_equal(out$columnIndex, 0) spec <- new_range_spec( sheet_name = "abc", cell_range = "2:4", sheets_df = sheets_df ) out <- as_GridCoordinate(spec, strict = FALSE) expect_equal(out$rowIndex, 1) expect_null(out$columnIndex) }) googlesheets4/tests/testthat/test-sheet_add.R0000644000176200001440000000212614074074641021060 0ustar liggesusers# ---- nm_fun ---- me_ <- nm_fun("TEST-sheet_add") # ---- tests ---- test_that("sheet_add() rejects non-character `sheet`", { expect_snapshot( sheet_add(test_sheet("googlesheets4-cell-tests"), sheet = 3), error = TRUE ) }) test_that("sheet_add() works", { skip_if_offline() skip_if_no_token() ss <- local_ss(me_()) expect_error_free( sheet_add(ss) ) expect_error_free( sheet_add(ss, "apple", .after = 1) ) expect_error_free( sheet_add(ss, "banana", .after = "apple") ) expect_error_free( sheet_add(ss, c("coconut", "dragonfruit")) ) expect_error_free( sheet_add( ss, sheet = "eggplant", .before = 1, gridProperties = list( rowCount = 3, columnCount = 6, frozenRowCount = 1 ) ) ) sheets_df <- sheet_properties(ss) expect_identical( sheets_df$name, c("eggplant", "Sheet1", "apple", "banana", "Sheet2", "coconut", "dragonfruit") ) expect_identical(vlookup("eggplant", sheets_df, "name", "grid_rows"), 3L) expect_identical(vlookup("eggplant", sheets_df, "name", "grid_columns"), 6L) }) googlesheets4/tests/testthat/test-gs4_endpoints.R0000644000176200001440000000063614074074641021724 0ustar liggesuserstest_that("endpoints can be retrieved en masse", { endpoints <- gs4_endpoints() expect_true(length(endpoints) >= 14) expect_match(names(endpoints), "^sheets\\.spreadsheets\\.") }) test_that("a single endpoint can be retrieved", { nm <- "sheets.spreadsheets.values.batchClear" endpoint <- gs4_endpoints(nm)[[1]] expect_true( all(c("id", "path", "parameters", "scopes") %in% names(endpoint)) ) }) googlesheets4/tests/testthat/test-gs4_find.R0000644000176200001440000000021214074074641020627 0ustar liggesuserstest_that("gs4_find() works", { skip_if_offline() skip_if_no_token() df <- gs4_find(n_max = 5) expect_s3_class(df, "dribble") }) googlesheets4/tests/testthat.R0000644000176200001440000000010613630372446016160 0ustar liggesuserslibrary(testthat) library(googlesheets4) test_check("googlesheets4") googlesheets4/R/0000755000176200001440000000000014075406416013236 5ustar liggesusersgooglesheets4/R/schema_ProtectedRange.R0000644000176200001440000000122114074074641017603 0ustar liggesusers#' @export as_tibble.googlesheets4_schema_ProtectedRange <- function(x, ...) { grid_range <- new("GridRange", !!!pluck(x, "range")) grid_range <- as_tibble(grid_range) tibble::tibble( protected_range_id = glean_int(x, "protectedRangeId"), description = glean_chr(x, "description"), requesting_user_can_edit = glean_lgl(x, "requestingUserCanEdit"), warning_only = glean_lgl(x, "warningOnly"), has_unprotected_ranges = rlang::has_name(x, "unprotectedRanges"), editors = x$editors %||% list(), named_range_id = glean_chr(x, "namedRangeId"), !!!grid_range ) } googlesheets4/R/gs4_browse.R0000644000176200001440000000111014074074641015430 0ustar liggesusers#' Visit a Sheet in a web browser #' #' Visits a Google Sheet in your default browser, if session is interactive. #' #' @inheritParams read_sheet #' #' @return The Sheet's browser URL, invisibly. #' @export #' @examples #' gs4_example("mini-gap") %>% gs4_browse() gs4_browse <- function(ss) { ## TO RECONSIDER AFTER AUTH: get the official link, if we're in auth state? # googledrive::drive_browse(as_sheets_id(ss)) ssid <- as_sheets_id(ss) url <- glue("https://docs.google.com/spreadsheets/d/{ssid}") if (is_interactive()) { utils::browseURL(url) } invisible(url) } googlesheets4/R/sheet_freeze.R0000644000176200001440000000410614074074641016032 0ustar liggesusers#' Freeze rows or columns in a (work)sheet #' #' @description #' *Note: not yet exported.* #' #' Sets the number of frozen rows or column for a (work)sheet. #' #' @eval param_ss() #' @eval param_sheet() #' @param nrow,ncol Desired number of frozen rows or columns, respectively. The #' default of `NULL` means to leave unchanged. #' #' @template ss-return #' @seealso Makes an `UpdateSheetPropertiesRequest`: #' * <# https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#UpdateSheetPropertiesRequest> #' #' @keywords internal #' @noRd #' #' @examples #' if (gs4_has_token()) { #' # create a data frame to use as initial data #' # intentionally has lots of rows and columns #' dat <- gs4_fodder(25) #' #' # create Sheet #' ss <- gs4_create("sheet-freeze-example", sheets = list(dat)) #' #' # look at it in the browser #' gs4_browse(ss) #' #' # freeze first 2 columns #' sheet_freeze(ss, ncol = 2) #' #' # clean up #' gs4_find("sheet-freeze-example") %>% #' googledrive::drive_trash() #' } sheet_freeze <- function(ss, sheet = NULL, nrow = NULL, ncol = NULL) { ssid <- as_sheets_id(ss) maybe_sheet(sheet) maybe_non_negative_integer(nrow) maybe_non_negative_integer(ncol) if (is.null(nrow) && is.null(ncol)) { gs4_bullets(c(i = "Nothing to be done.")) return(invisible(ssid)) } dims <- c( if (!is.null(nrow)) cli::pluralize("{nrow} row{?s}"), if (!is.null(ncol)) cli::pluralize("{ncol} column{?s}") ) dims <- glue_collapse(dims, sep = " and ") x <- gs4_get(ssid) s <- lookup_sheet(sheet, sheets_df = x$sheets) gs4_bullets(c( v = "Freezing {dims} on sheet {.w_sheet {s$name}} in {.s_sheet {x$name}}.")) freeze_req <- bureq_set_grid_properties( sheetId = s$id, frozenRowCount = nrow, frozenColumnCount = ncol ) req <- request_generate( "sheets.spreadsheets.batchUpdate", params = list( spreadsheetId = ssid, requests = freeze_req ) ) resp_raw <- request_make(req) gargle::response_process(resp_raw) invisible(ssid) } googlesheets4/R/schema_NamedRange.R0000644000176200001440000000111514074074641016700 0ustar liggesusers#' @export as_tibble.googlesheets4_schema_NamedRange <- function(x, ...) { grid_range <- new("GridRange", !!!pluck(x, "range")) grid_range <- as_tibble(grid_range) tibble::tibble( name = glean_chr(x, "name"), id = glean_chr(x, "namedRangeId"), !!!grid_range ) } as_NamedRange <- function(x, ...) { UseMethod("as_NamedRange") } #' @export as_NamedRange.default <- function(x, ...) { abort_unsupported_conversion(x, to = "NamedRange") } #' @export as_NamedRange.range_spec <- function(x, ..., name) { new("NamedRange", name = name, range = as_GridRange(x)) } googlesheets4/R/request_make.R0000644000176200001440000000551014075375056016054 0ustar liggesusers#' Make a Google Sheets API request #' #' Low-level function to execute a Sheets API request. Most users should, #' instead, use higher-level wrappers that facilitate common tasks, such as #' reading or writing worksheets or cell ranges. The functions here are intended #' for internal use and for programming around the Sheets API. #' #' `make_request()` is a very thin wrapper around [gargle::request_retry()], #' only adding the googlesheets4 user agent. Typically the input has been #' created with [request_generate()] or [gargle::request_build()] and the output #' is processed with `process_response()`. #' #' [gargle::request_retry()] retries requests that error with `429 #' RESOURCE_EXHAUSTED`. Its basic scheme is exponential backoff, with one tweak #' that is very specific to the Sheets API, which has documented [usage #' limits](https://developers.google.com/sheets/api/reference/limits): #' #' "a limit of 500 requests per 100 seconds per project and 100 requests per 100 #' seconds per user" #' #' Note that the "project" here means everyone using googlesheets4 who hasn't #' configured their own OAuth app. This is potentially a lot of users, all #' acting independently. #' #' If you hit the "100 requests per 100 seconds per **user**" limit (which #' really does mean YOU), the first wait time is a bit more than 100 seconds, #' then we revert to exponential backoff. #' #' If you experience lots of retries, especially with 100 second delays, it #' means your use of googlesheets4 is more than casual and **it's time for you #' to get your own OAuth app or use a service account token**. This is explained #' in the gargle vignette `vignette("get-api-credentials", package = "gargle")`. #' #' @param x List. Holds the components for an HTTP request, presumably created #' with [request_generate()] or [gargle::request_build()]. Must contain a #' `method` and `url`. If present, `body` and `token` are used. #' @param ... Optional arguments passed through to the HTTP method. #' @param encode If the body is a named list, how should it be encoded? This has #' the same meaning as `encode` in all the [httr::VERB()]s, such as #' [httr::POST()]. Note, however, that we default to `encode = "json"`, which #' is what you want most of the time when calling the Sheets API. The httr #' default is `"multipart"`. Other acceptable values are `"form"` and `"raw"`. #' #' @return Object of class `response` from [httr]. #' @export #' @family low-level API functions request_make <- function(x, ..., encode = "json") { gargle::request_retry( x, ..., encode = encode, user_agent = gs4_user_agent() ) } gs4_user_agent <- function() { httr::user_agent(paste0( "googlesheets4/", utils::packageVersion("googlesheets4"), " ", "(GPN:RStudio; )", " ", "gargle/", utils::packageVersion("gargle"), " ", "httr/", utils::packageVersion("httr") )) } googlesheets4/R/range_delete.R0000644000176200001440000001135614074074641016005 0ustar liggesusers#' Delete cells #' #' Deletes a range of cells and shifts other cells into the deleted area. There #' are several related tasks that are implemented by other functions: #' * To clear cells of their value and/or format, use [range_clear()]. #' * To delete an entire (work)sheet, use [sheet_delete()]. #' * To change the dimensions of a (work)sheet, use [sheet_resize()]. #' #' @eval param_ss() #' @eval param_sheet( #' action = "delete", #' "Ignored if the sheet is specified via `range`. If neither argument", #' "specifies the sheet, defaults to the first visible sheet." #' ) #' @param range Cells to delete. There are a couple differences between `range` #' here and how it works in other functions (e.g. [range_read()]): #' * `range` must be specified. #' * `range` must not be a named range. #' * `range` must not be the name of a (work) sheet. Instead, use #' [sheet_delete()] to delete an entire sheet. #' Row-only and column-only ranges are especially relevant, such as "2:6" or #' "D". Remember you can also use the helpers in [`cell-specification`], #' such as `cell_cols(4:6)`, or `cell_rows(5)`. #' @param shift Must be one of "up" or "left", if specified. Required if `range` #' is NOT a rows-only or column-only range (in which case, we can figure it #' out for you). Determines whether the deleted area is filled by shifting #' surrounding cells up or to the left. #' #' @template ss-return #' @export #' @family write functions #' @seealso Makes a `DeleteRangeRequest`: #' * #' #' @examples #' if (gs4_has_token()) { #' # create a data frame to use as initial data #' df <- gs4_fodder(10) #' #' # create Sheet #' ss <- gs4_create("range-delete-example", sheets = list(df)) #' #' # delete some rows #' range_delete(ss, range = "2:4") #' #' # delete a column #' range_delete(ss, range = "C") #' #' # delete a rectangle and specify how to shift remaining cells #' range_delete(ss, range = "B3:F4", shift = "left") #' #' # clean up #' gs4_find("range-delete-example") %>% #' googledrive::drive_trash() #' } range_delete <- function(ss, sheet = NULL, range, shift = NULL) { ssid <- as_sheets_id(ss) maybe_sheet(sheet) check_range(range) stopifnot(!is.null(range)) if (is.null(shift)) { shift_dimension <- NULL } else { shift <- match.arg(shift, c("up", "left")) shift_dimension <- switch(shift, up = "ROWS", left = "COLUMNS") } x <- gs4_get(ssid) gs4_bullets(c(v = "Editing {.s_sheet {x$name}}.")) # determine (work)sheet and range -------------------------------------------- range_spec <- as_range_spec( range, sheet = sheet, sheets_df = x$sheets, nr_df = x$named_ranges ) if (is.null(range_spec$cell_range) && is.null(range_spec$cell_limits)) { gs4_abort("{.fun range_delete} requires a cell range.") } range_spec$sheet_name <- range_spec$sheet_name %||% first_visible_name(x$sheets) # as_GridRange() throws an error for a named range grid_range <- as_GridRange(range_spec) gs4_bullets(c(v = "Deleting cells in sheet {.w_sheet {range_spec$sheet_name}}.")) # form batch update request -------------------------------------------------- shift_dimension <- shift_dimension %||% determine_shift(grid_range) if (is.null(shift_dimension)) { gs4_abort(c( "The {.arg shift} direction must be specified for this {.arg range}.", "It can't be automatically determined.")) } # form batch update request -------------------------------------------------- delete_req <- list(deleteRange = new( "DeleteRangeRequest", range = grid_range, shiftDimension = shift_dimension )) # do it ---------------------------------------------------------------------- req <- request_generate( "sheets.spreadsheets.batchUpdate", params = list( spreadsheetId = ssid, requests = list(delete_req) ) ) resp_raw <- request_make(req) gargle::response_process(resp_raw) invisible(ssid) } determine_shift <- function(gr) { stopifnot(inherits(gr, "googlesheets4_schema_GridRange")) bounded_on_bottom <- !is.null(gr$endRowIndex) && notNA(gr$endRowIndex) bounded_on_right <- !is.null(gr$endColumnIndex) && notNA(gr$endColumnIndex) if (bounded_on_bottom && bounded_on_right) { # user must specify shift return(NULL) } if (bounded_on_bottom) { # and not bounded_on_right return("ROWS") } if (bounded_on_right) { # and not bounded_on_bottom return("COLUMNS") } gs4_abort(c( "{.arg range} must be bounded on the bottom and/or on the right.", i = "Use {.fun sheet_delete} or {.fun sheet_resize} to delete or \\ resize a (work)sheet." )) } googlesheets4/R/utils-cell-ranges.R0000644000176200001440000001663414074134356016725 0ustar liggesusersA1_char_class <- "[a-zA-Z0-9:$]" compound_rx <- glue("(?^.+)!(?{A1_char_class}+$)") letter_part <- "[$]?[A-Za-z]{1,3}" number_part <- "[$]?[0-9]{1,7}" A1_rx <- glue("^{letter_part}{number_part}$|^{letter_part}$|^{number_part}$") A1_decomp <- glue("(?{letter_part})?(?{number_part})?") qualified_A1 <- function(sheet_name = NULL, cell_range = NULL) { n_missing <- is.null(sheet_name) + is.null(cell_range) if (n_missing == 2) { return() } sep <- if (n_missing == 0) "!" else "" # API docs: "For simplicity, it is safe to always surround the sheet name # with single quotes." as.character( glue("{sq_escape(sheet_name) %||% ''}{sep}{cell_range %||% ''}") ) } as_sheets_range <- function(x) { stopifnot(inherits(x, what = "cell_limits")) # TODO: we don't show people providing sheet name via cell_limits # so I proceed as if sheet is always specified elsewhere x$sheet <- NA_character_ x <- resolve_limits(x) limits <- x[c("ul", "lr")] if (noNA(unlist(limits))) { return(cellranger::as.range(x, fo = "A1")) } # cellranger::as.range() does the wrong thing for everything below here, # i.e. returns NA # But we can make valid A1 ranges for the Sheets API in many cases. # Until cellranger is capable, we must do it in googlesheets4. if (allNA(unlist(limits))) { return(NULL) } row_limits <- map_int(limits, 1) col_limits <- map_int(limits, 2) if (allNA(col_limits) && noNA(row_limits)) { return(paste0(row_limits, collapse = ":")) } if (allNA(row_limits) && noNA(col_limits)) { return(paste0(cellranger::num_to_letter(col_limits), collapse = ":")) } if (noNA(limits$ul) && sum(is.na(limits$lr)) == 1) { ul <- paste0(cellranger::num_to_letter(col_limits[1]), row_limits[1]) lr <- if (is.na(col_limits[2])) { row_limits[2] } else { cellranger::num_to_letter(col_limits[2]) } return(paste0(c(ul, lr), collapse = ":")) } # if resolve_limits() is doing its job, we should never get here gs4_abort(c( "Can't express these {.cls cell_limits} as an A1 range:", # cell_limits doesn't have a format method :( x = utils::capture.output(print(x)) )) } # think of cell_limits like so: # ul = upper left | lr = lower right # -----------------+------------------ # start_row end_row # start_col end_col # if start is specified, then so must be the end # # here we replace end_row or end_col in such cases with an actual number # # if provided, sheet_data is a list with two named elements: # * `grid_rows` = max row extent # * `grid_columns` = max col extent # probably obtained like so: # df <- gs4_get()$sheets # df[df$name == sheet, c("grid_rows", "grid_columns")] resolve_limits <- function(cell_limits, sheet_data = NULL) { # If no sheet_data, use theoretical maxima. # Rows: Max number of cells is 5 million. So that must be the maximum # number of rows (imagine a spreadsheet with 1 sheet and 1 column). # Columns: Max col is "ZZZ" = cellranger::letter_to_num("ZZZ") = 18278 MAX_ROW <- sheet_data$grid_rows %||% 5000000L MAX_COL <- sheet_data$grid_columns %||% 18278L limits <- c(cell_limits$ul, cell_limits$lr) if (noNA(limits) || allNA(limits)) { # rectangle is completely specified or completely unspecified return(cell_limits) } rlims <- function(cl) map_int(cl[c("ul", "lr")], 1) clims <- function(cl) map_int(cl[c("ul", "lr")], 2) # i:j, ?:j, i:? if (allNA(clims(cell_limits))) { cell_limits$ul[1] <- cell_limits$ul[1] %|% 1L cell_limits$lr[1] <- cell_limits$lr[1] %|% MAX_ROW return(cell_limits) } # X:Y, ?:Y, X:? if (allNA(rlims(cell_limits))) { cell_limits$ul[2] <- cell_limits$ul[2] %|% 1L cell_limits$lr[2] <- cell_limits$lr[2] %|% MAX_COL return(cell_limits) } # complete ul cell_limits$ul[1] <- cell_limits$ul[1] %|% 1L cell_limits$ul[2] <- cell_limits$ul[2] %|% 1L if (allNA(cell_limits$lr)) { # populate col of lr cell_limits$lr[2] <- cell_limits$lr[2] %|% MAX_COL } cell_limits } ## Note: this function is NOT vectorized, x is scalar as_cell_limits <- function(x) { check_character(x) check_length_one(x) ## match against !? parsed <- rematch2::re_match(x, compound_rx) ## successful match (and parse) if (notNA(parsed$`.match`)) { cell_limits <- limits_from_range(parsed$cell_range) cell_limits$sheet <- parsed$sheet return(cell_limits) } ## failed to match ## two possibilities: ## * An A1 cell reference or range ## * Name of a sheet or named region if (all(grepl(A1_rx, strsplit(x, split = ":")[[1]]))) { limits_from_range(x) } else { ## TO THINK: I am questioning if this should even be allowed ## perhaps you MUST use sheet argument for this, not range? ## to be clear: we're talking about passing a sheet name or name of a ## named range, without a '!A1:C4' type of range as suffix cell_limits(sheet = x) } ## TODO: above is still not sophisticated enough to detect that ## A, AA, AAA (strings of length less than 4) and ## 1, 12, ..., 1234567 (numbers with less than 8 digits) ## are not, I believe, valid ranges } limits_from_range <- function(x) { x_split <- strsplit(x, ":")[[1]] if (!length(x_split) %in% 1:2) {gs4_abort("Invalid range: {.range {x}}")} if (!all(grepl(A1_rx, x_split))) {gs4_abort("Invalid range: {.range {x}}")} corners <- rematch2::re_match(x_split, A1_decomp) if (anyNA(corners$.match)) {gs4_abort("Invalid range: {.range {x}}")} corners$column <- ifelse(nzchar(corners$column), corners$column, NA_character_) corners$row <- ifelse(nzchar(corners$row), corners$row, NA_character_) corners$row <- as.integer(corners$row) if (nrow(corners) == 1) { corners <- corners[c(1, 1), ] } cellranger::cell_limits( ul = c( corners$row[1] %|% NA_integer_, cellranger::letter_to_num(corners$column[1]) %|% NA_integer_ ), lr = c( corners$row[2] %|% NA_integer_, cellranger::letter_to_num(corners$column[2]) %|% NA_integer_ ) ) } check_range <- function(range = NULL) { if (is.null(range) || inherits(range, "cell_limits") || is_string(range)) { return(range) } gs4_abort(" {.arg range} must be {.code NULL}, a string, or a {.cls cell_limits} object.") } ## the `...` are used to absorb extra variables when this is used inside pmap() make_cell_range <- function(start_row, end_row, start_column, end_column, sheet_name, ...) { cl <- cellranger::cell_limits( ul = c(start_row, start_column), lr = c(end_row, end_column), sheet = glue::single_quote(sheet_name) ) as_sheets_range(cl) } ## A pair of functions for the (un)escaping of spreadsheet names ## for use in range strings like 'Sheet1'!A2:D4 sq_escape <- function(x) { if (is.null(x)) return() ## if string already starts and ends with single quote, pass it through is_not_quoted <- !map_lgl(x, ~ grepl("^'.*'$", .x)) ## duplicate each single quote and protect string with single quotes x[is_not_quoted] <- paste0("'", gsub("'", "''", x[is_not_quoted]), "'") x } sq_unescape <- function(x) { if (is.null(x)) return() ## only modify if string starts and ends with single quote is_quoted <- map_lgl(x, ~ grepl("^'.*'$", .x)) ## strip leading and trailing single quote and substitute 1 single quote ## for every pair of single quotes x[is_quoted] <- gsub("''", "'", sub("^'(.*)'$", "\\1", x[is_quoted])) x } googlesheets4/R/range_flood.R0000644000176200001440000000757014074074641015651 0ustar liggesusers#' Flood or clear a range of cells #' #' `range_flood()` "floods" a range of cells with the same content. #' `range_clear()` is a wrapper that handles the common special case of #' clearing the cell value. Both functions, by default, also clear the format, #' but this can be specified via `reformat`. #' #' @eval param_ss() #' @eval param_sheet(action = "write into") #' @template range #' @param cell The value to fill the cells in the `range` with. If unspecified, #' the default of `NULL` results in clearing the existing value. #' @template reformat #' #' @template ss-return #' @export #' @family write functions #' @seealso Makes a `RepeatCellRequest`: #' * #' #' @examples #' if (gs4_has_token()) { #' # create a data frame to use as initial data #' df <- gs4_fodder(10) #' #' # create Sheet #' ss <- gs4_create("range-flood-demo", sheets = list(df)) #' #' # default behavior (`cell = NULL`): clear value and format #' range_flood(ss, range = "A1:B3") #' #' # clear value but preserve format #' range_flood(ss, range = "C1:D3", reformat = FALSE) #' #' # send new value #' range_flood(ss, range = "4:5", cell = ";-)") #' #' # send formatting #' # WARNING: use these unexported, internal functions at your own risk! #' # This not (yet) officially supported, but it's possible. #' blue_background <- googlesheets4:::CellData( #' userEnteredFormat = googlesheets4:::new( #' "CellFormat", #' backgroundColor = googlesheets4:::new( #' "Color", #' red = 159 / 255, green = 183 / 255, blue = 196 / 255 #' ) #' ) #' ) #' range_flood(ss, range = "I:J", cell = blue_background) #' #' # range_clear() is a shortcut where `cell = NULL` always #' range_clear(ss, range = "9:9") #' range_clear(ss, range = "10:10", reformat = FALSE) #' #' # clean up #' gs4_find("range-flood-demo") %>% #' googledrive::drive_trash() #' } range_flood <- function(ss, sheet = NULL, range = NULL, cell = NULL, reformat = TRUE) { ssid <- as_sheets_id(ss) maybe_sheet(sheet) check_range(range) check_bool(reformat) x <- gs4_get(ssid) gs4_bullets(c(v = "Editing {.s_sheet {x$name}}.")) # determine (work)sheet ------------------------------------------------------ range_spec <- as_range_spec( range, sheet = sheet, sheets_df = x$sheets, nr_df = x$named_ranges ) range_spec$sheet_name <- range_spec$sheet_name %||% first_visible_name(x$sheets) s <- lookup_sheet(range_spec$sheet_name, sheets_df = x$sheets) gs4_bullets(c(v = "Editing sheet {.w_sheet {range_spec$sheet_name}}.")) # prepare cell and field mask ------------------------------------------------ # TODO: adapt here when CellData becomes a vctrs class if (is_CellData(cell)) { fields <- gargle::field_mask(cell) } else { cell <- as_CellData(cell %||% NA)[[1]] fields <- if (reformat) "userEnteredValue,userEnteredFormat" else "userEnteredValue" } # form batch update request -------------------------------------------------- repeat_req <- list(repeatCell = new( "RepeatCellRequest", range = as_GridRange(range_spec), cell = cell, fields = fields )) # do it ---------------------------------------------------------------------- req <- request_generate( "sheets.spreadsheets.batchUpdate", params = list( spreadsheetId = ssid, requests = list(repeat_req) ) ) resp_raw <- request_make(req) gargle::response_process(resp_raw) invisible(ssid) } #' @rdname range_flood #' @export range_clear <- function(ss, sheet = NULL, range = NULL, reformat = TRUE) { range_flood( ss = ss, sheet = sheet, range = range, reformat = reformat ) } googlesheets4/R/range_read_cells.R0000644000176200001440000000564714074074641016646 0ustar liggesusers#' Read cells from a Sheet #' #' This low-level function returns cell data in a tibble with one row per cell. #' This tibble has integer variables `row` and `col` (referring to location #' with the Google Sheet), an A1-style reference `loc`, and a `cell` #' list-column. The flagship function [read_sheet()], a.k.a. [range_read()], is #' what most users are looking for, rather than `range_read_cells()`. #' [read_sheet()] is basically `range_read_cells()` (this function), followed by #' [spread_sheet()], which looks after reshaping and column typing. But if you #' really want raw cell data from the API, `range_read_cells()` is for you! #' #' @eval param_ss() #' @eval param_sheet( #' action = "read", #' "Ignored if the sheet is specified via `range`. If neither argument", #' "specifies the sheet, defaults to the first visible sheet." #' ) #' @template range #' @template skip-read #' @template n_max #' @param cell_data How much detail to get for each cell. `"default"` retrieves #' the fields actually used when googlesheets4 guesses or imposes cell and #' column types. `"full"` retrieves all fields in the [`CellData` #' schema](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/cells#CellData). #' The main differences relate to cell formatting. #' @param discard_empty Whether to discard cells that have no data. Literally, #' we check for an `effectiveValue`, which is one of the fields in the #' [`CellData` #' schema](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/cells#CellData). #' #' @seealso Wraps the `spreadsheets.get` endpoint: #' * #' #' @return A tibble with one row per cell in the `range`. #' @export #' #' @examples #' if (gs4_has_token()) { #' range_read_cells(gs4_example("deaths"), range = "arts_data") #' #' # if you want detailed and exhaustive cell data, do this #' range_read_cells( #' gs4_example("formulas-and-formats"), #' cell_data = "full", #' discard_empty = FALSE #' ) #' } range_read_cells <- function(ss, sheet = NULL, range = NULL, skip = 0, n_max = Inf, cell_data = c("default", "full"), discard_empty = TRUE) { cell_data <- match.arg(cell_data) # range spec params are checked inside get_cells(): # ss, sheet, range, skip, n_max out <- get_cells( ss = ss, sheet = sheet, range = range, skip = skip, n_max = n_max, col_names_in_sheet = FALSE, detail_level = cell_data, discard_empty = discard_empty ) out$cell <- apply_ctype(out$cell) add_loc(out) } # I use this elsewhere during development, so handy to have in a function add_loc <- function(df) { tibble::add_column( df, loc = as.character(glue("{cellranger::num_to_letter(df$col)}{df$row}")), .before = "cell" ) } googlesheets4/R/sysdata.rda0000644000176200001440000014466114074074641015412 0ustar liggesusersBZh91AY&SYEJ%{=(_|Oygǧ@Nb}ᢖe,k;.x˼cn0s-8)JUةy+mVMNw hiz4(G@7 k@P;:{z]z PowC':3$7u8PPЩ'@aj^=tzϮzP4`=V΃/8 @gpn-'Msʹh{s ͳw76vPQؙŬmZmٝ-]a%Tvn(Qns˫sGs@ǡ[HHfٖ&wi랡 -O` h3bq-{kҪ Y{r KKXlKU]&Kp5d lMh*0lU[mOV@(h(@97]n绻6p w^ W­;etv0;[6 icc4;a I@ &@@Ɉ44h<hT(M=SM Sz=SjzLhD=OSHDB@ !$$2j{QTa45 d2`!@Chb 4 1h$i螓bxI6SzM4i4z&#=h M4G@ BQ$IP1 &=#C<􁦏P =@h@ gڙzڞPi4f4*$ ѠOHmM$J{HeOOLi⟥=MP FA" Hw l7HJ)@)SAhUhD)JUE^^ܢl "tDt4M *@MutMA G(D$&DE9E(Ax/c4S0vX. ]倣=c\2-~MCN$h0;GG,rr")sۨo0^_ߔò7'+'/YeyXKWd4ϟ?pϽD \ JBV':6Hɓ"BU!ȃo?6G)O [h,ы+PT~ojn A(܀ °@mnA4ƣw$ 8ֈ%|;~y tU1EIS\>9҂A`*7ҿ*0 _=uߩ2nf (IX$( &rLHݾ9-¬<%{!:5ƛjoߝxܣ$$1QV}ck5*ë-@TMs,$ߋ/[ƫ0rja*c:k=eoVqwܫ#9jft boYb[Uf!Xuѐm y rqu{+C7xW^9LW8g3M-~nCp~U=V0۹Rz9ߺ:Ttbu5u/B_6_v:|[158=^|ɿ ^/,wfz~8~;jq+9?/|̛Y;3O{ǥ#ׅ[g~Zq|ǯ^xFC9((]4ꔜ^  +whKD>9 Z*u9 ^1|8D:?sLgUx>=j7;>xI|#ֵ VUY{=i:Jc7+}[g(⻩~#lgѬy%՜.yLqq>rPG%S]i,kbb H*&0IEI$!Ic<܋YcEN Gho0pAIcn4DD\\5rv6jj/9×lU 73%b(UqO+NZ(#Wu\^&

p9QhV&a)SH<[-wy:&i^&Yl@zؓԁ>_s){AGz@0y̩}lh)|8o 67'ŘӪE\jB@Hm><WwvOJ1G(iQD %Ts22`R$|0K uTC( x7Qac39yB7 R%Uo H;$Hʈ><HBR 2௻FGfA @oFzOh⫋Xt]JōD;0q@Bݕ)rճN$eDHeW1ZM:Fg9^JPXi1$j jwNȻgM #{M0 GT8B裢&8IE2×Ю rIK&~`~ 'Z$PT*%AD"nh|:[n C,QR)%؆zȒ L06uRZ`Bޡ%HL,^.*@Qpͫci`HCʇ9=ѨNIPDTMH{ b8@ÈA@ f9|ܶF1IwγD)(+RNkMr4Ʒb-q\ݻp7.EQÆbixAp7P8k+Xo8^ךSOwt.Ѡc5 vJ&,2f|#&p21MII0ŚQl8ڟHPLd /8=J*(nɬku}$_>fYjg5utk(8 UDyfe~;bBwl' FI&ƫ#kQI%zadYՎABmbZ\GauTf "%X:JA*UV!V5l?I$:mɦ˥_/?XOn3x$qʺ8>A雥GD٠"xtͤnAD+A"fL)3P@)KCKJ3>9q@T=h -^h Aet+׉znG 6Dv ;XY]TNEvh82l -uUgd$/َ !$?&)`Da!2pY  ?TP)r 0aA7G덺ɚ~_$ג\.~;tMiߞ,A{$I"2١BN0"Q="JW~媀|6-(瘝3MT̛'CXiRJ(hnn+jDRPd#®YKWFi{ꗘ BGϚ|@G_N>R9o\خ#˖Q6f/Eqb1f%|W) 44.=dP ID "Fv|k{4yuC+t5HӷQpeAE%ibq 33|Y6Z ^cf81a2ffUY4_id+AFSöbfv廨{ΊGcQWKۛ1ە8Cnk瑈 ;E^msbm\ d嶵UlGs;f"֢)(*=mDɶy`:DT8:(0>MrD|QQ"5hiTI% %)Tr:kAIT=~b @tI>|a(B[{jH)@sn22#JE"soۛS{Ȑj .U0J?wYw]|Bt̚Xf5 >ZJ%Gz@PmqMD= E ,j )_ϺM+4Z_a}]7@4, fHElXL];a gS#wVi3+, E3*ژu }' 4B4e,i <bTjbOZDT%;T5+*, ?ek p6"K~\1R*4h ! ]9DqǝYNy?<k3S<؊-EMdzvJ+xBfq̋P@R43!lDe~)m(buG4?y#$# }øx59:hFFUuP}Tgٮh@S:j\##Tj{wk l-n; o_)䶞0J aY OUH ]Y> ?DF?tDfY&:H1+`~(3&-[0Px ڀ̆WAbMP;JRՁGy.׳sy'K3f Kqx`(zc7˱"#U;njvAslPbTWF޴dUd)VrXje"1b+ex5^@LNNM>v Drn&D^Pf^""YD[ DF\!p1{SNXs048G2p8}} 'OtPS9vbN&*ǚȶ[) fQNI7\5V oơ̅G|)ϟDm 4xz{к0"ϻ\ C!~ >BKF$о].=N-[!4HQFB EdH**D+x/8> rOmY#F٣T>iKP%ҭ44,HP(=4/_6{upVA/ad :L3H\mRܶQ+*osiUTq dZ<95JU4 bDBhIMIlZ] ,156E8ѣb4SE5YgC #hi&2?Fo~VB^'{y'[i9mAL#U[K/baxwy\Sxԋ0/אf#v0<\bhf cFYd 8gLF)kk"(W;κ.8l ؊+%=ZŎ [2l}foeiٌXºVK<ꊁbNP%ʜ1--RJ- [& *SuÌ-40lf/1[)W< i=  )}Gx+?CXqβڲ,A~=ҨTom#Ǐ?cJA׎xg0X~JÁ87e ?A؍r/+叹2:=}7g&K/[aל+wc[Kkg-$P¸ڤe=r{G־ŀg|ҥx[*L -v%`ۚ>`B/֯Da<:6HBb _5Ac{qf$65]EB6s|q3tC`36OoU҆RRGL&4ڕ9p2pi14BK-U7nފf٪eTe,.PTi@Q #Fq@-:'9׶`wqBRp l!ãiTIֱh %s35 >ugbD ͐hÖWg&PX_,/ˮ%OiA : /jYL@B(hC=(0>H^&ၐ&U^+G롖sP!45zoI hmd˫j+O>ZyؘC=ft~ьٍ80yL?]h5w 89~c=(ruL| ϻyMb7*e6(@AuLd w^JbhfkR ||_q`a`yLY3YG9pz(ԍ,X3 `!c>wG&oz8(c t-4ϯh#sYg`>´)1Vǚ@,Wh2lnfW>鹓q j?61CiA;Ue(Zx}񫴵ƇkqcM_ ۹cs;k-`$ `7x14RO4Zm]+97f+tmY$tiIѓ|3ozh#r{0fPDQr/bbe1AUqDcv-)z^@|w4?2kagy(4pq6G>[Xg`5OfO&K>p>ۯDlEVGX[AraNp"{߼*`6)T#W i  HTb;A7(~oe[g_efk' |af6sGd?q|К\{]ĠN?MҦ's֞1x94rae scFӞד漚q>Ϸs30uWeOadxzʾ::>>'V_|\ic{e9# mA$w?>r|5eҝ9^mc2RdЂEŕ+Ep3 _J<+ǵ41Im(|b1Ϧ0Y(ӫoPg&jQnQ^߾|`i0@p>RovN&O׬O۽N@6x#X0uDiᑊe8;\69\o*\\1W&{_tֵŪ+pӌU:ӯA- (:ʛ!E0tf s8ݰD;)AF:8e<,Dimh>:]W>k1h!ٔm t &}s~V ej$V`"Mf+ڈbfb$I1]m4"Yf<҉MP}Y٘`g^†:N ZyL$6IbD%βx0_E4$1 -wXP-ӣ L*Іg&Xl9/|`k0\WC8m[F '6g;IBPKm.\2ѤJR&au^D7~zcMgz~z-\)Hbka\;;+GGOLᳪ{#2WrY٫`rFVQo ߌBCxeQSc躣̽wF/4Os< ^.RTذ1)ШU)Xq1S|O4cLvmmddYbϖ k1Wv/}n^0c瞾:7>rps 'lF00v BowR<]baQEk(F`8JЏP:fK@LՈիیX3)زr! bkP'~C\\t0221JH!м K0lu%QYbgF>{kt|?IzZw w/W1Tr .3>9Sm vX zz +L0ef@+9+Xg2*bKufmnWmnjUcٓ) nLـk,3qJn9Fm}4Y+qmw>=^ :%۠ar4:Ұlo"z9UΘP# +'\9鹞Y/Vg|uz3;7rto6 0h XK!o/xM@&U^\#a~vd1Acl/Bw/e{dG4O5663x[20s:XN񿗘h4FXHl“jJ"i(E)sw71_F֝=gICDЋS'4@Ѭ0L|O %@!iw\HWW9aJT797[|=s\S,G4D sA~d;jw}~MA"229M3kjH WFt!V"gڌc渴gOG1n(R7ݓOs;cgͿ棏;l_}b4u?x@`t|P!.KgL<3lt}d =l@:с>P /ݦ6j:QOg qH}&CMkapw?Yޞ.һ3C},( 4ɼ؁C$OxD1eDTEV՗O]σX L0IwQ$5k1g3ACԵ]P~`#!d0W Tr2O̔֡yG n=q]<MZige:Ϝg }x2!B" =!i!#h2b *F<h})yǎu OM%tTs6Xa[ϟ[g?63냪F2DZьaWyWPz`lw|P !7zC`PYC×l)a꟎Qn('|-@ohtiյ ԇ!ΘH_6S2sմ \(fĕ¤-,WF }_zsW n6# KlyرB4[8x**%!pCNVL:TgmD{bC7LIL1WCԂX9`/3m%~oȿ 8}?d`zl4"cF|ScqTU-DM kLǟ漍r羹sN' MF`UwdaX);ߜ-+x eE橡ۊ0ɚXO$GDӎsQ[Or!P@ ,մB 3`4 yj)X*~3"%8 X)wNwYӧv:eYuykbךoћff'|NwϦt.}T.T0~@wA#"dϖ70P/2edsrƶDQ )hɧw`>!IcMا={ k^ _1Bdɝ?,üY>Jn A[R~Ya| `) (zvө8%Ӗ(t O5:71+HT@>23n/XW?* b ;>`F ǿ,Sϳmkя3|khрmUy$^yݚ u os  W8[?+ZB)lmk }ԃy+hvg:hQԁ s#:Zi QhA.:# ixq25ɃdgMasl #cK0sljt08iFd1$qɍ_!) Lv)K<=\MjOCf״f{ U Ou|d͈]±rW.YB#>Uy߃L(U)*N0X0n+(JE :h[ 'KD8. 1ϨiJJ"(@QƮ-tyKTbUeYX }Q${`_9Iwn%νe)߾2?0lCFp M/ctDlGpm2 9mmp{d~'ר} .|ފ?Τv1m`e۽e(4)=V} la43 F6Xx'X$dqArW7C >LY[ۿ<{Fx),4oZf``Nɡl xWRraAxn32b#[}fI嗳"cJy7DQt)bJ$ʃpm(UCS==]΃QƱ7HUL+ 9`b5p57O]=ΞDA;w`Z޽ )cotAǝ]!>&Φ+|g!) ٪= jDC1Y>&oza d{P#8Aٓ[ko;Y`yџl~/"y-|8[[_O#z-d/LՑ= @1Dxa26ΫS,6Ÿ}=H*XiMy$KK,aU%cٙwd7y;'.4Y OyNEE:֚#^ʈ:8Vvy@ww_C[Zoڌpxb6_؇kxA\`VmD乿+=CI3 000(+P͡Hy׌ {G3xatJs3c|XݱKtԭy?W!||Tg;( ($e~'W//jaȘYj~8b|dv ?A"{S̲DR'pgwuc:ՍI=hH 0svώ<___?m$׿FOcG,@p |洧m3Y'8o9Wz]{cƫj;91\^ꊿUL}oZǬϷ'8'{7α!wc]1[z4l# 1B Wu8ZvY+d]唖M "AP^ʈ"0y>C:(,ϳ뎅g;cm ޫ?"CC%W:vTv+SǾЀ0dQءe;!]קKHU@ $zJhO@>vҩ$~yXYۯ}Lj~NjXrqXME?8]eB+E)C4q2hD&u??k;J0U/ F cljpy3J0MtϬ3z5X{M O5s82>yªǝ1Gxf8ϛG맂/g[3߯QouJ!vzy>[{6 WAznWP>Bg };wن0~m R)J׏x*:Py!P@ќUH#UDfl;e*Uv_!]"F\_8j1a^X_ oL71D\8}fv=  ގ b|?8]V :d ,ayu=U]lE]y}:Ch 0Il@Hya`P9s#'̂&)hAh7f$jأ4%[xhOXػ 8U-G9>1C&,ΚrR U`-Ѕ"en#BQ!p1Tm (mwn6LƩ˜Q8%_^0 Ÿ]R8G"]Sƀ, Fڒ(;l]Y1@(qɗ3KQnNn[Gʼn ЯFBM SQZ1#,):[9Te1oPDEpw|Ⳡ#WBٰ#e0KRV #J ca=7:MHn@0JV1M'ab,3 @Cwn zҘ*sjkk^6'uh|@QKft:ox"cscϨ<_F[^fxo# AUY}wA9z|01YpӬU*K,#V1 C>2늋.6O}rzԕY>jk:lhL0})arnYfOQS^ia 113P[4+leaUNc_:S|2D' LC~| CBSTt PU, j|y: ~`,cdaVP_??j_>~O\ag҄Z<v'`~{\_ǣ;zxOw58|?@wo7x^o(-G?/oZ|^ݧI? qLK~#?>~.IQ0Fxpq x 󷉾7|X%$\rq-wȋ8Y7׉0ax6ޢ$)IV2 ; J=!؄+ -'%_a.J8~2@=?NvIFP;0"um̠g|5پ*V)>~?Eϩ_\NS-T_7{Q,]a_'|$g;eFјs?KZ]|#\e{ԇQ^0k=Ogb&J3T/-dȗ_~A.; T2-Q_2HDqfߙX2j~DFHt<y2h%?Dm9\V7='}I8ZBqgxBL6`l9;:󗺹#b݅W>w:rWa H0A `$`bJKkm/@ 'F:~rISmuw_pQHߐrPhn*\;ԗˎ/2?NȽe)>r+’|-^J(!3oP ݱMCh`!rd22̬ ʼnGn]$uk>cDh4G %Z'Ҏ+;R E@8RQG5YcR?n>!zR*Fk:"4޼!c2XQ oqsh*.q24ЗgF>/Օ|}si!;?j9?;ύvd(\Q3j ~xkV6Y0q5ͩMV/Ќԑf}_PP A߬CmqWe5qbވO=m*e )jdU)(sxE4 fn_ָ 6h&><#+>c@ xB4/Nt95_q.Fa$?}|jId"EiB* a(03!a:@T)M;5WM :N!אR;FEjDR5l6cy0Vqܕc_3444TZj"g H3MzXF20Ybe01Xy>\?r0wTScq3-T34FHc3~rrfgGoequx;t'698hFȏv ;l8 *-r#HUa%d1SMeS←~?va@Gt&㍎C.dRwp?Dzк0lĆ[ۜ?_x84^d/xM=la菠ot)K@ BMP_9"l>F䁃N?=~ױDN1Lb0X8'??ii4ΰ/`2I6!37M%8vsGv9U!K8'3ޗ|LXM6y/\.;!\n@%PD`}0I -@#c 8 HjJlbC"?|Pq^)h =JW:`t Lf~a I`UXT)*$”^:k>>+ߌDYUIN0U4a47rh(G8ߥ܀> ~g??$4}6/t|>K?oz?"aƇ8G4 =i V>qcDqqմ0&؍O(as89"bn yEd Yp bJ܌l,kJkfVi%9\*krE7"gu ^#٘HˌQ-WRFVjyxlߌpP #U&a#80G#_|o%߃d s.rB-r56怨/"P(^.\B9 DcEi(t]$]}%.Iom;8S$]`MS7zbB *d"GvW"i qD~ԗV{ܱ.U#R萊$H&!x3 HP%8G}9A;;aV;CB;+a#@ #ޟ=ᗇ݁r19$LFhɔY(t.H$B[44FMٞ00pG>F ~""Uul$<02f|[GFh]3UkL0! $'@ I͑Ғdq)2+PEH\Ba)@gZ* (8Q( Aq\cU ɤ_8/rfQx;^^Cmm;yyq%?`}Kc xe?\1%c.M-BLޗ=ea@wBHݑp;|c\ŢS,KtYh!ze}QNn %$/d]wPӳUvuVgdf#Z- 7p^kk/z+鮔SS0܏Ob]|Nb[u/5o);AC݉GQ( :ՂC` M&Ca<;޴:FA YAD L|ܤ -=Tɻy1'i׈S:NP)g#)T_iW޴Nʙg[ҟtIEgHURR,}Eد'k}UwQqYE5 %w+rI^>/Wo]dvFh**,{ GPLM6pD+Xlפjuj1d˳ACTliSTA|DŽj[XT0dZTSUӘh&`B,S6_JX*o([-}_95m1|j65nO{gԷ?05s8jtFG7#rvY-&qA3t;ԷNQbZ7(L%YeG1CU8zՙƱw&,ro-p1.|TŖq;yxcR2wM_wU+.Ew]l5,7G3lF8]8n تPTPXZ}(N~Ywz~9]bٜU9W F$g|I rP{L3$4 C,rOӟ}4(~{.Y {ds($69ra,RLR;&P`DbHl0R0d M|tދ?C\h}ulBDMBUN^/jZ'i02_Tg'C$ӌDu)Bw)AJ$O϶SRp\:ٌ~Ŝ=ai[wnQ[ߠ~{s©ڙJK*K;E <#,1e Qb ;'Pd`l{@0A蘪 Fe )$ &ƈ yZu:=:e0g770ۙ1M ,f ow_8Yd#G?@rT R?yǭ#O8L?oK󼞏^{&]>W%S 9WO;/~[y_?'oÂq/:'MY߾}PO?r1CL6 xC`=QD:Ap,ƺxàn5贸}u_οIe6BKoZߏˡ,ik|#*ߑ 7MѼ/&Icigz7y.onZwNtt?^t5'W|w}r?NҿtOy^g>_ǵ' x?3;6?<Ŷf!UFlocN;oC?GUS<#hx1hgϐ1\}?篱]rrN:rh9/X|]5_+O7;aFM1 ׿::r3)aٳϳ=IaE9_~o?a lkh~tx\_|q?a!#K O_ʠGJ# oM8.o=K3]|XwWk8͖_E4}f~to. SGG䙵xCb #Zy o fjgLGI 8`#m[䤽mggyPfHE?*|*,!PIV 0B(]<1pjTU|^v IRjS^fL3\ '|{>:Α[mP|htpO.s2+$~~Nws}Wb xX vt{9fY9uiÚ%^c79Uޝ0ľ2)wgwB8\kmk?gȏ#s#_z[-ʪ2RA A"E QUpL> anPeW~kut/Ŵgc?J=soT8pD4Y$i[]>#S6F-/{ju?q2Bx '`pBIG lpĚk푏ҩ5ϲ+޳\up\D-PxM-La}C[ Dxb[s[ U&pd!FA_,/a|7oɪYg8AdؙBr+E,h9pzţG1*]Ʉk B2^hdqpk1gʱBb8%wl9^{ceNCg9o )*ݒx*"DCkMk#Y;ߖw:4b(uRL*kFp~L7π2TqP>tpͺ&a8Mc9Dk kU2~Tʖafpx7qf p_fzx*.1JII$Sl< ':9Kƌ?3# ,Kw1c@܋FuZV@0 0RR1;@_2wd=.rvNܝ$l"DDD`J96p¸@)xi!HH]0az\1!""HzX^2&Ie!P! Y)UQbU1C @$4dhGJ1RDI Ap8 IZx$Vn!mpBXKD U) O12$r(z uK~%AU<&_G/is'8_oYKP4P8Y?2IPE@tuS$>P0Rbu,^L{B@<ƤPM$5U 0v)b Jښ 4 Yqy#촟bBY됉>G? D~4Nt0}G"4N 6?na4k,'5s袪C8@ xM1(I)i! bf{TԙPt`/93!I%xpiihFkMk5GR-La TeeYwezopNKgj" &SEG "?6}{` &aP/;o0mdTN&zI7ulG 3Ӏ8YW/q^zҋ Ҵň7FDT2j̈́Bxj((E i !"(RR"@"F h(Bh"A`XH ]Yb/?z 7* $ŏ j2qLl cO2A,)A`R)$ !baF8HژRN)_B,7d EUTC$ %Q%CA"t&CB Hi!ppPEIQ!E#I`1/˶0vL@tN ո^͡w~4xGRXZBEGN1bLsG@ߴMeb"I-dרOOc# Lhɥ1n_"#9+H'<˪"B/ ]Ht(OYp]:?6ΏeR|2xHu@4PHYl! $@I%Ve!H%24RY2F,E;.{y Ɵ=)"])|MvG:91pudr.161J^ҥ"@2([KC,Q1C)yDP_W vP5 M)3UW18ea^CBud߆Y"Hf&*)i "JEHDbU @"X (PBJ*F b `h H"$$h"*H(0!!!*`I" {JTlht?vZ>>Y 8LXUa{*D y1*"DPC:H;1D< n#[QPݒը ;hTQ4UU4K5-?iB;0ZeZZ4IjW %%C{Ewuq Id^>"' Pŷ79S@CE>jT*5 +BR]4`8ɛmQ2tA92k KplR<o8x3;ݜ-E8v<˴o>}H=IO49\+]hб#<Ljy! R8!qd Cf4<њ-RQAU - GB0 I/Lz{E:Xa%]wDp Hh|ypCz>hbk2A@&B~Ƙ_р{TaQE61c jJL ԥDpq1$m02< t'Lp),ї%ݼ'dž]}V?#_ T4a33'Ç\d2ICA6aB'CaT')#" =-$IơUIw d%QC0؄/xw-R Y$xȁ}p:OBeqԠ$Y7H|`IS0H:$k϶<>S傱ϰd#|~n(`)ce/J"lߌ~B_TlUge3 c!L3Ox΢a ;<ahF! 2i v;*^?c@#GCxBh ]_ T_8-r}zr(L з &4ͶM &HHMR@^(IDa=5<2ŇH HAAE):J#Q'&<2AΆ^c& ' 8G̞BPys͝69O^Η͙ ]$h (EOALxu$sTF-F 165af`qPtl--CBAJL1H)8*eD[t 3^5㢈XR$,"JP% * 2"8 "%D 0f +}JZFuؤ:o+K}a&a!FRANAeUxWp댒7Q;|#d!b`(R)FB$1Q,bCFIp4M-@Fr{|No˜㹀= 51KHa'A"=_>z<(d)'+Sv`(n`LC|^%ؽ(&j,a[h8߮:hy&sz8/dKb05  p VG<nא6؅܀|J8y a vB{_LЍ<cBH6™ A"2^rCї/jR*HA6AAh (/ R3A!ځ[ ,0itH5 &Ck уE lk&s`5dZwMdZ:镪ġ>ĀP0XfAO RSI*t@EV1 n r}GlTmg$*i:2$);1aqO騊rD^-h;{ͬ ot.P<؅.\;+Y98.RXb#G,a9Ƒ:c6*kAj+8⡳hpcJ2P'h0`1Y< rb*3,9ataկf|$KL9h`3=㶡taS<`L$msY'!Sid2/ lԋ-c2K(ot9Y+yZou;nitmf )]3MgJeH'!d qrlm!m%Lj.Y264ʄeGp<{:!h#ƭE1\0ѡ+-{"D`e"1cZp(qTuB|] k`NLIv8Kr lAl$tT-2-m窃feHڝǾ>L:G3,Pru֏YcdE-qGcmǬg j!ږ.~sp1 Qȅtd”pFYtu5wJٌl[52"vK- $zi6.rg;0 {JH [r&9aaPv5ږ4e59pݡ8c+-,a\f ΊfX֓,+rWŝrq%ػΙv3XGd(8qa 5.uh|zGM7߲%UoѹyI.]tkv#xCK(`ι,,Bw]XUgPs [ ŗXs_/\p539h~=k)HA 4o;&w4H6##efc-ۜaEjQɒg57]Q1p= m%m'rV5tc db4AplϚr aauk kpoD>Xq㾵zy:|S$O51;[L}l: rGՅ4"y/u`n̓22 Nin47RE):tIZHV`tgBb4FB,0YfS)ƘƎ#B<"9snoU:,z*"Ou 7'晭<5Esb4l89XiON:QtbҖl16*N ʎWCspœmIh5dI(FDw}ҦIpS"&AQ0 $V^DG涐O6 7D'uDxmpR2P JX  i[UYdҡd{*5ljzkxs Aw8N4K;, lIsȷT 2>=bC}ᾯقͅb~~׼J4dV7Z4WKTν+)b`vm&xxpɧhCɯfNV3VW6έj*"*b8s@=[lU&(`I aByUd=>q ]tux{TV&LJyz:H"w`YIYZ $fO P$UEwdSΠYBH Y!`ԉQn XXeʢl(4"ULģ T#G/5>f"*g>[ZZ6uU  l:RL&΁XӨ40 $l4`R(ܺIRlW14nr j%k v3ŌI +;pi&L@Hp(]*]Ԭ\)cCe`TEBRG0QO@'>X; pd(b"N o&!D7uT0ST, 2'PІd~luѰe?4iX>TĴ#=R1Ӝa~ u/~ Gn]>,2Hgc=:t^OG6p>Iع1ˠW@i hY, RCw`~ @p0\M2A ^Q}b6B@q%/Ex jElc 8j0of,L0Æ 7s犻{zxؼ %Bs.YLd\e+z fY4g Bz@e ) h#)LB45nF"3loEjB9)`A(#ыQ= *ls 6eS$L$h͙9LStȊza}dHA(CiWHèaV&j$jO@?|!Dp}D (H @(BB1KU!H}G&tBM1pцҐF\CKL$IĘ+0WL`#ppLі[&:-"[i$l!F٠l;j1i  bB"-YSA%tf49 B c0cTöC`,ɭ`q)u% N@Ln ̉GS\#Iͼ[ UI $~LC궉1AT{N1R'>ةȑ +xYt~*/R Fw?`NuВ3 P]x"=/ǭIϙ a0I"9H9HH}ِޫ CD (/PŠIhKbl%QGTxG&ЌnAA* Rc d$TAb1)~&3@ LJ EjMJX59Py)!{6Cd홄B%Sc̞:+V &`s;>)-wL@r~qib*ap. C PLQ5QVO똔=l!` zehꅰeIQGJs T26L0% WrPDCAEŌ1STKͬBPp10p'CîncQ=1'U'Rf%yRMi >Dꒉ($@Th uE?6>a=GJ :!>G A^˒5'Nq79AȾ~d21dZXABI4¯א- I'h)1f& ?=Gj!ӭ "|C(;Q #1qcl;9ޠZ l[^]:PJD IFHaZB$"U&!;1]bZ D9"*8s/ Ob" 3QUĎ' <@湀ԔKH28~$7b ⒅ (Q48$&[*!֦oA2H>aoɪk DbPU"ˎT%PN ="IW1@?#v(#u5H )ݼ  "*"PPBRRZ"T(%C``y>I)$$s (Ф-*D’d2$}B;G8CF""*#3xDihx[LHt$"ulH60@ҺC%!M BHS](Қ%I0z,ki''W0˔S>#Cx)J:߭<]6CJe‡?2p $.92};<:)Ї"4ٌ(I3Opl3d}t^svĉ1Hb"RP44M "Ns{RjoM"q|Pm#9`> دQ, !9s5tPDR1LUT%JuвP"m$M=$w/<1Nmw"R$9q[ C$hd+T'0 'K)ac(ppa^QNh,<-n4B$D-݂LGƃz;CO 'V;Ovz;GD?s쥋1S$fI #i,GhG{ߑJ;S6v&Pϳu2b #%Ҷ-To0%$Rf!B`&nsZ(lZ2ad Fc/A#(:j\ē!@PCn&FC|x Bh**&AVͩCV2-0O2N6[1dP[(5޺TߡjKT ϝ4bL꼷HL/{ LBw 'zE5\,âɢsX^h]}  hLXE& (&UI3Mx\®͟7nrokߘ=%kAEwĐW&pw{tߓL B]PYpa".+,'Î{n=T)aznppGM՜hvəEi[YMfLi TpQo&Df {tN I^9T)U!2gԞx|<s&1ƌB0< GE!Ȓt{^$wccLt2;OlIpHm}/R_^tϠ1("Pp0F:3>VDH*mRĉ0q'-GdL²]<~,BITt)cCu/L$;2 > MN GɲO-p/W ?&3c &q-6wVxA%8A"t3Ü 3$DBc¡-GPӦ銵RV3$$QyOPz,IO w>i:EO"t$is"بnQL䏀% EUTRMlDoi[6`":I[Z9Ød)ϩ@('$0=$N "yDbԨ색CH$r⑤șcRb18ufBa&f `JT*MX®`@3DA$U͈#,JRTv?(oL4pClRF 㒰x>G$chmA=1tA /L-3I[Tf-8Đk_A0nM%pԦ!j>h?U85Hdt=MDUFGpO tHz}_YҏMCIPq"4 Z5&IXnI8΁݃ < sr)¢ R$#.HJ {oN  LP-whVI=te^qXI@@9*Ą1da94:k:DP<&y xvHp6KO51YK_I WaHvQ`FBԈ`Xs4A7I*I 5`EZ 9h $TT=yہA m3A(NǞ ePHU Ml.w&V6:#6! LMÂz1f lH J(cB4aI|†0X\0 :2SU_Q)Z WyptG0!xz)|O&ǔIZ&*&&XH!)YZVj AI SR(B! $b# {gU@C2!@SB0>g0L!HDD 6c3U':|H,# BM)R! F&BRڅRZ"IB'YĩS$4R@JR z~2vbQ EdX¬i߆]ĥx4$r"򘆒Dl2,2"up"dB #*ݡWP~j~)<@NAz;yhw)+AH(44')_'9%B*"e$ A*Y@)TIF""*Hz|"vYj+<5Fc|8$HJJDM?G\4/. .OЈ*b%%aكGU#!2H rJ Q>}R,%'p8g<üTTceA3''yPu@57O 0"D% S0RLJH HGo G h/4s:QOE,*ؐZ(hD&iT>9u NXq 1b ?"=aEc#'d0o?s0:UOy˂\>bE (AM]D#e 1 5X5xd4AQ[v0w|zjJ0!D(H~a}Y?ͻO3{Hr048;w @:VIघ|'|7|!|}eC>OX @:ru(I!bi ) UbPZXR: E@4]e`O k"Д "T*(/- .;4#lj  ԂW@vV᪊Wo"z[Ji0oJVBcRRRtP0(C\Pw(߷d pa)|bFFa!/ 2E žNL+rA"doIGI^8} J=^$*(@)FeB8|!PlRy`k ,V; ,Q'm=oڟ|| ƃ8cPbo2`НaP U&Vqr !8V Ä#h*Nda.H1fGPi>~h6jt`s؊ ktUV2QLJA RS -J_k$UDPP5E313R튘JgءdM1|fH9ES\I=.RQ/ʌzЃ( ?2uIPT̲%Զ#bN ;IGȖ]'֩UJɺ~üob1kU0P`|Pf#kl-lNYeNܖܰ?g~IdE1]La|@ >)(p W.H31t}X*8@0?REP<pPdNDOw߲MOU򌇫^OjTC**8_/O@PI},06:L:-ċ|4peBȅ(J?3'yT\S0Œ$Nېu_\"wPuERPȇTC4Il:Ȗ/QP ZDҒT0tFˮvHMdxI$zɢI OBx^m(JZ(+i1:SZR!lYB6iw`S::=dxB ih%M"T4ȘE"Ԑk@akFȐH1O1T Lب@ (}pQ )@N ' $Wpng Ll2@~<.ǵ#-v_;j֪4盄@w8+CVL7mߑӷbq@TM")"-q)#eLb 6{xZG;@Zk::0FU}wq16? ì46NR٠`8bXD&G/l05|Ei:,^hfZ sź671Ynk޳[8|~$ bEiF2#^Jpá89…uh &^xE(hXu!EaSSlx0eC*C>ʼ ˌu殇2 5 yK#&n]0g><@Qy|mƣ):8]xY1.zu94T= 񨵄]RYT*9鄤^Bjc^LYiM^(0X(yx;93RfD=]26{FuXF.U62…f ƑUUv;]s-r.&Q89 w"Êb3MpLf G{.X^_2^J ( 7<"Ey 5cDKɅbD<"xZbh5M; 㨂&x~G7gD6dMt.?՘|_Eg#gmax4X !6U,vxI:=5{X* 'FOjB Ʀ֍@TKX,j&Uv.2i5GoYdJ $ZA(EwFa!~e7)6/@`G 1&w2w"8A:z 8xf4V0r4$ĜĔ,hR 3ŝRY,mȰb*>[dƺjjP88̄,brPBeɔcZV c\Tde=8^#YnensJĆt$Dw\4fzѭ!xgT8fl$ xQ Ii9mVK`0;& ƨO4“ꪊ.HkE]32(R1qm:څ;#2ey@64"yg59ܪqUK4,b஀ ǯi~6 Eex;``ќ29$TrXTe{]dkޭT7 3!Mpۡg1',J tjjӧL(&ƭ3 bd!83l\,C:ZkyS LaN!^n/$0ll9Rg5@wRU&jT9 q Qؓ/Rab뿦u#|mk qlcˢTGQa})X@{n !{YxYo`}>R'mvćJ9;>8{Ǯ@1xJKp2H$ξ.'{774JjpOhhL6#LHʉw ϳթq2=d`DX s@ /;80+Х#CH|czL2}0 8$) Z A=N 0sO5""G:tœ%#=^,e[ }d*! 2 A4zH7LY̊>rtp($ w'<8Q74i!#Z$!ڣ-j 'NI)ŷ|8yWl֦:E#:)[c;cVպJ\rof(NxuMp$|"1aLZ8&#+16KB(\I #X!7|i 5 .ycitTZ+gp ?wVͳTeUcXLDV83EK;(zk?!:.~#q_q%x1Eٮsj0#=civ{ dWbJBW^ p@}jXxn,m uJW 8M-}5J >O". 4IJvF2G%Q͎41}1.(e ծHKlA&f{Йfp$q+thU Q0!65TDrD`-*%$h"t|a̼恒N Ej,5Y&4;B fMt&MSZ9] 0jԏG rLj2Ij8*.[=])5CRf r#Ŗ6ȢYtYWQLUtos jiTr(COġ̗2gpu 62>Z"mZ_dj4)On+E 0՟&fi t'_к+pr6b"L-}8d", 3Skj&#Z\!`BaG'門*%` TG,?%LQJL~H hJA; Q~Ow)%#ax%^vݓNz@v4UUDWC)("JjmELCZ)WÇ({\Qݟ}57+pP>8 \ )ę7AK`X"ʘ+z֐:h#}2RjL 0w2zM%a`u g8+=M5uJ۽AxB܉;M2.%Yx2;u@qJUTpaEnyط Hf4srY4\LͤgdkJ)k!/DYC܉(`RA2KT9u*$X0:iF(IM>;QUQ5!AODqQ5 SH X!ZX(A`x>\ Si5`5v d;HO#] LD9RV͇\u٠vQhSԧPgeV3mN0wA=!!-Dx=H7_~Z,VcMhQ/90/H졅<@A+ѷ 6 x fhVՄ٢58z DE^)5ᔼNyz}ڶ" i f"V&QlF@aR@`EBYRF(;1\  @(hRm%(B$kǘ89s`Vpy8NG1tkr#‡ Q1w#N*d5H` GxT# B,)CejS*G@8mc⳸]YiM>.f$Ӄ2g*"`*2mД>13 MA*P @,- E!J4;W5[@tU L 3CD 3#/GM}/=b]D-T{e?byOlj7Z' 9O  pp>M8J1"DՂVcy`RӛI-mK>R( k0V N֡Wov;{Xs4mh2fi Ƙ "`؃Mh#X* _ AMQ,躱˲]7,w#/ UBIvr$w@a h@Q4jtL(Oa$r~ [:X hmm } GŶ l=C~,c~u)6il5u7G5OԲTl: :DqsD,2ue( SBhp1A^Z8T0\B$B"A'(KA!Ke@:@ Q}ݤNucNo"6\#b_"Ji1KARSZEkIc4*=lE"ł #2G*N@|Z Z`ajWRT:S>0P;W=|gAȓ|]~UT J]iCW#8NJt|$GXK!-9!gJhH=yA*,RҥA`@Q7Ql`QgB@-"ڤ~5$ Bqu.cP%0|| T 'IPz$x'z:xL}B4)*1@!L@ }(=Hk_m 8"&_(A8yԽ\ey96Lp h8NiYd)b|Pcy7 QS0}W4yt$N.Gdă4#J:gI>^OZ&i!(`j&`d9QАA ME,DBDy̸ $/ǝP<L|LKUZ"W+aAuPǟp@{$bE#5OQ\ !Iw$P|'KBh$~=#*D"sC mT DS 'z22um `b-3=y[\RPi)a@|M7TQdzj*ިmhB13u*rZwmʭ@q44Od05#kvf Ga&8W s45 *A[·TP^h[ q;^3hh4īZ0`3"مahdaYh yKC[:q,`z#|:gD!D񽢓z0Dp$1LSXkZ(kN-( we)"W&neRZA pgh"D] @TN8P䀻/Ӹ\CdZBQ! )Uh@&b$%*+Cqu>Ǔꆇ(AS'?2<> 4]0 ;Cӡ ABrA̴52]Sw"yG0@Q2()Eܐ"hjedUT&Czd.p Kgooglesheets4/R/batch-update-requests.R0000644000176200001440000000756714074074641017612 0ustar liggesusers# https://developers.google.com/sheets/api/samples/formatting#format_a_header_row # returns: a wrapped instance of RepeatCellRequest bureq_header_row <- function(row = 1, sheetId = NULL, backgroundColor = 0.92, horizontalAlignment = "CENTER", bold = TRUE) { row <- row - 1 # indices are zero-based; intervals are half open: [start, end) grid_range <- new( "GridRange", startRowIndex = row, endRowIndex = row + 1, sheetId = sheetId ) cell_format <- new( "CellFormat", horizontalAlignment = horizontalAlignment, backgroundColor = new( "Color", # I want a shade of grey red = backgroundColor, green = backgroundColor, blue = backgroundColor ), textFormat = new( "TextFormat", bold = bold ) ) cell_data <- new_CellData(userEnteredFormat = cell_format) # hard-coding because we really do want to reset child properties not # explicitly set here # example: TextFormat's other children, like fontFamily or underline # example: Color's other child, alpha fields <- "userEnteredFormat(horizontalAlignment,backgroundColor,textFormat)" list(repeatCell = new( "RepeatCellRequest", range = grid_range, cell = cell_data, fields = fields )) } # based on this, except I clear everything by sending 'fields = "*"' # https://developers.google.com/sheets/api/samples/sheet#clear_a_sheet_of_all_values_while_preserving_formats # returns: a wrapped instance of RepeatCellRequest bureq_clear_sheet <- function(sheetId) { list(repeatCell = new( "RepeatCellRequest", range = new("GridRange", sheetId = sheetId), fields = "*" )) } # https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#UpdateSheetPropertiesRequest bureq_set_grid_properties <- function(sheetId, nrow = NULL, ncol = NULL, frozenRowCount = 1, frozenColumnCount = NULL) { gp <- new("GridProperties", rowCount = nrow, columnCount = ncol) if (!is.null(frozenRowCount) && frozenRowCount > 0) { gp <- patch(gp, frozenRowCount = frozenRowCount) } if (!is.null(frozenColumnCount) && frozenColumnCount > 0) { gp <- patch(gp, frozenColumnCount = frozenColumnCount) } if (length(gp) == 0) { return(NULL) } sp <- new("SheetProperties", sheetId = sheetId, gridProperties = gp) list(updateSheetProperties = new( "UpdateSheetPropertiesRequest", properties = sp, fields = gargle::field_mask(sp) )) } # https://developers.google.com/sheets/api/samples/rowcolumn#automatically_resize_a_column # https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#AutoResizeDimensionsRequest bureq_auto_resize_dimensions <- function(sheetId, dimension = c("COLUMNS", "ROWS"), start = NULL, end = NULL) { dimension <- match.arg(dimension) # https://developers.google.com/sheets/api/reference/rest/v4/DimensionRange # A range along a single dimension on a sheet. All indexes are zero-based. # Indexes are half open: the start index is inclusive and the end index is # exclusive. Missing indexes indicate the range is unbounded on that side. dimension_range <- new( "DimensionRange", sheetId = sheetId, dimension = dimension ) if (!is.null(start) && notNA(start)) { check_non_negative_integer(start) dimension_range <- patch(dimension_range, startIndex = start - 1) } if (!is.null(end) && notNA(end)) { check_non_negative_integer(end) dimension_range <- patch(dimension_range, endIndex = end) } list(autoResizeDimensions = new( "AutoResizeDimensionsRequest", dimensions = dimension_range )) } googlesheets4/R/schema_RowData.R0000644000176200001440000000044614074060601016235 0ustar liggesusers# creates an array of instances of RowData as_RowData <- function(df, col_names = TRUE) { df_cells <- purrr::modify(df, as_CellData) df_rows <- pmap(df_cells, list) if (col_names) { df_rows <- c(list(as_CellData(names(df))), df_rows) } map(df_rows, ~ list(values = unname(.x))) } googlesheets4/R/utils.R0000644000176200001440000000556114074074641014530 0ustar liggesusers# for development only str1 <- function(x, ...) utils::str(x, ..., max.level = 1) noNA <- Negate(anyNA) allNA <- function(x) all(is.na(x)) notNA <- Negate(is.na) isFALSE <- function(x) identical(x, FALSE) is_string <- function(x) is.character(x) && length(x) == 1L is_integerish <- function(x) { floor(x) == x } check_data_frame <- function(x, nm = deparse(substitute(x))) { if (!is.data.frame(x)) { gs4_abort(c( "{.arg {nm}} must be a {.cls data.frame}:", x = "{.arg {nm}} has class {.cls {class(x)}}." )) } x } check_string <- function(x, nm = deparse(substitute(x))) { check_character(x, nm = nm) check_length_one(x, nm = nm) x } maybe_string <- function(x, nm = deparse(substitute(x))) { if (is.null(x)) { x } else { check_string(x, nm = nm) } } check_length_one <- function(x, nm = deparse(substitute(x))) { if (length(x) != 1) { gs4_abort("{.arg {nm}} must have length 1, not length {length(x)}.") } x } check_has_length <- function(x, nm = deparse(substitute(x))) { if (length(x) < 1) { gs4_abort("{.arg {nm}} must have length greater than zero.") } x } check_character <- function(x, nm = deparse(substitute(x))) { if (!is.character(x)) { gs4_abort(c( "{.arg {nm}} must be {.cls character}:", x = "{.arg {nm}} has class {.cls {class(x)}}." )) } x } maybe_character <- function(x, nm = deparse(substitute(x))) { if (is.null(x)) { x } else { check_character(x, nm = nm) } } check_non_negative_integer <- function(i, nm = deparse(substitute(i))) { if (length(i) != 1 || !is.numeric(i) || !is_integerish(i) || is.na(i) || i < 0) { gs4_abort(c( "{.arg {nm}} must be a positive integer:", x = "{.arg {nm}} has class {.cls {class(i)}}." )) } i } maybe_non_negative_integer <- function(i, nm = deparse(substitute(i))) { if (is.null(i)) { i } else { check_non_negative_integer(i, nm = nm) } } check_bool <- function(bool, nm = deparse(substitute(bool))) { if (!is_bool(bool)) { gs4_abort("{.arg {nm}} must be either {.code TRUE} or {.code FALSE}.") } bool } maybe_bool <- function(bool, nm = deparse(substitute(bool))) { if (is.null(bool)) { bool } else { check_bool(bool, nm = nm) } } vlookup <- function(this, data, key, value) { stopifnot(is_string(key), is_string(value)) m <- match(this, data[[key]]) data[[value]][m] } ## avoid the name `trim_ws` because it's an argument of several functions in ## this package ws_trim <- function(x) { sub("\\s*$", "", sub("^\\s*", "", x)) } enforce_na <- function(x, na = "") { stopifnot(is.character(x), is.character(na)) out <- x if (length(na) > 0) { out[x %in% na] <- NA_character_ } if (!("" %in% na)) { out[is.na(x)] <- "" } out } groom_text <- function(x, na = "", trim_ws = TRUE) { if (isTRUE(trim_ws)) { x <- ws_trim(x) } enforce_na(x, na) } googlesheets4/R/zzz.R0000644000176200001440000000076214074117213014214 0ustar liggesusers.onLoad <- function(libname, pkgname) { # .auth is created in R/gs4_auth.R # this is to insure we get an instance of gargle's AuthState using the # current, locally installed version of gargle utils::assignInMyNamespace( ".auth", gargle::init_AuthState(package = "googlesheets4", auth_active = TRUE) ) if (identical(Sys.getenv("IN_PKGDOWN"), "true")) { tryCatch( gs4_auth_docs(), googlesheets4_auth_internal_error = function(e) NULL ) } invisible() } googlesheets4/R/sheet_relocate.R0000644000176200001440000000676414074074641016364 0ustar liggesusers#' Relocate one or more (work)sheets #' #' @description #' Move (work)sheets around within a (spread)Sheet. The outcome is most #' predictable for these common and simple use cases: #' * Reorder and move one or more sheets to the front. #' * Move a single sheet to a specific (but arbitrary) location. #' * Move multiple sheets to the back with `.after = 100` (`.after` can be #' any number greater than or equal to the number of sheets). #' #' If your relocation task is more complicated and you are puzzled by the #' result, break it into a sequence of simpler calls to #' `sheet_relocate()`. #' #' @eval param_ss() #' @eval param_sheet( #' action = "relocate", #' "You can pass a vector to move multiple sheets at once or even a list,", #' "if you need to mix names and positions." #' ) #' @param .before,.after Specification of where to locate the sheets(s) #' identified by `sheet`. Exactly one of `.before` and `.after` must be #' specified. Refer to an existing sheet by name (via a string) or by position #' (via a number). #' #' @template ss-return #' @export #' @family worksheet functions #' @seealso #' Constructs a batch of `UpdateSheetPropertiesRequest`s (one per sheet): #' * #' #' @examples #' if (gs4_has_token()) { #' sheet_names <- c("alfa", "bravo", "charlie", "delta", "echo", "foxtrot") #' ss <- gs4_create("sheet-relocate-demo", sheets = sheet_names) #' sheet_names(ss) #' #' # move one sheet, forwards then backwards #' ss %>% #' sheet_relocate("echo", .before = "bravo") %>% #' sheet_names() #' ss %>% #' sheet_relocate("echo", .after = "delta") %>% #' sheet_names() #' #' # reorder and move multiple sheets to the front #' ss %>% #' sheet_relocate(list("foxtrot", 4)) %>% #' sheet_names() #' #' # put the sheets back in the original order #' ss %>% #' sheet_relocate(sheet_names) %>% #' sheet_names() #' #' # reorder and move multiple sheets to the back #' ss %>% #' sheet_relocate(c("bravo", "alfa", "echo"), .after = 10) %>% #' sheet_names() #' #' # clean up #' gs4_find("sheet-relocate-demo") %>% #' googledrive::drive_trash() #' } sheet_relocate <- function(ss, sheet, .before = if (is.null(.after)) 1, .after = NULL) { ssid <- as_sheets_id(ss) walk(sheet, check_sheet) maybe_sheet(.before) maybe_sheet(.after) x <- gs4_get(ssid) gs4_bullets(c(v = "Relocating sheets in {.s_sheet {x$name}}.")) if (!is.null(.before)) { sheet <- rev(sheet) } requests <- map( sheet, ~ make_UpdateSheetPropertiesRequest( sheet = .x, .before = .before, .after = .after, sheets_df = x$sheets ) ) req <- request_generate( "sheets.spreadsheets.batchUpdate", params = list( spreadsheetId = ssid, requests = requests ) ) resp_raw <- request_make(req) gargle::response_process(resp_raw) invisible(ssid) } make_UpdateSheetPropertiesRequest <- function(sheet, .before, .after, sheets_df) { s <- lookup_sheet(sheet, sheets_df = sheets_df) index <- resolve_index(sheets_df, .before, .after) sp <- new("SheetProperties", sheetId = s$id, index = index) update_req <- new( "UpdateSheetPropertiesRequest", properties = sp, fields = gargle::field_mask(sp) ) list(updateSheetProperties = update_req) } googlesheets4/R/sheet_write.R0000644000176200001440000001233314074074641015705 0ustar liggesusers#' (Over)write new data into a Sheet #' #' @description #' #' This is one of the main ways to write data with googlesheets4. This function #' writes a data frame into a (work)sheet inside a (spread)Sheet. The target #' sheet is styled as a table: #' * Special formatting is applied to the header row, which holds column #' names. #' * The first row (header row) is frozen. #' * The sheet's dimensions are set to "shrink wrap" the `data`. #' #' If no existing Sheet is specified via `ss`, this function delegates to #' [`gs4_create()`] and the new Sheet's name is randomly generated. If that's #' undesirable, call [`gs4_create()`] directly to get more control. #' #' If no `sheet` is specified or if `sheet` doesn't identify an existing sheet, #' a new sheet is added to receive the `data`. If `sheet` specifies an existing #' sheet, it is effectively overwritten! All pre-existing values, formats, and #' dimensions are cleared and the targeted sheet gets new values and dimensions #' from `data`. #' #' This function goes by two names, because we want it to make sense in two #' contexts: #' * `write_sheet()` evokes other table-writing functions, like #' `readr::write_csv()`. The `sheet` here technically refers to an individual #' (work)sheet (but also sort of refers to the associated Google #' (spread)Sheet). #' * `sheet_write()` is the right name according to the naming convention used #' throughout the googlesheets4 package. #' #' `write_sheet()` and `sheet_write()` are synonyms and you can use either one. #' #' @param data A data frame. If it has zero rows, we send one empty pseudo-row #' of data, so that we can apply the usual table styling. This empty row goes #' away (gets filled, actually) the first time you send more data with #' [sheet_append()]. #' @eval param_ss() #' @eval param_sheet(action = "write into") #' #' @template ss-return #' @export #' @family write functions #' @family worksheet functions #' #' @examples #' if (gs4_has_token()) { #' df <- data.frame( #' x = 1:3, #' y = letters[1:3] #' ) #' #' # specify only a data frame, get a new Sheet, with a random name #' ss <- write_sheet(df) #' read_sheet(ss) #' #' # clean up #' googledrive::drive_trash(ss) #' #' # create a Sheet with some initial, placeholder data #' ss <- gs4_create( #' "sheet-write-demo", #' sheets = list(alpha = data.frame(x = 1), omega = data.frame(x = 1)) #' ) #' #' # write df into its own, new sheet #' sheet_write(df, ss = ss) #' #' # write mtcars into the sheet named "omega" #' sheet_write(mtcars, ss = ss, sheet = "omega") #' #' # get an overview of the sheets #' sheet_properties(ss) #' #' # view your magnificent creation in the browser #' gs4_browse(ss) #' #' # clean up #' gs4_find("sheet-write-demo") %>% #' googledrive::drive_trash() #' } sheet_write <- function(data, ss = NULL, sheet = NULL) { data_quo <- enquo(data) data <- eval_tidy(data_quo) check_data_frame(data) # no Sheet provided --> call gs4_create() --------------------------------- if (is.null(ss)) { if (quo_is_symbol(data_quo)) { sheet <- sheet %||% as_name(data_quo) } if (is.null(sheet)) { return(gs4_create(sheets = data)) } else { check_string(sheet) return(gs4_create(sheets = list2(!!sheet := data))) } } # finish checking inputs ----------------------------------------------------- ssid <- as_sheets_id(ss) maybe_sheet(sheet) # retrieve spreadsheet metadata ---------------------------------------------- x <- gs4_get(ssid) gs4_bullets(c(v = "Writing to {.s_sheet {x$name}}.")) # no `sheet` ... but maybe we can name the sheet after the data -------------- if (is.null(sheet) && quo_is_symbol(data_quo)) { candidate <- as_name(data_quo) # accept proposed name iff it does not overwrite existing sheet if (!is.null(candidate)) { m <- match(candidate, x$sheets$name) sheet <- if (is.na(m)) candidate else NULL } } # initialize the batch update requests and the target sheet s ---------------- requests <- list() s <- NULL # ensure there's a target sheet, ready to receive data ----------------------- if (!is.null(sheet)) { s <- tryCatch( lookup_sheet(sheet, sheets_df = x$sheets), googlesheets4_error_sheet_not_found = function(cnd) NULL ) } if (is.null(s)) { x <- sheet_add_impl_(ssid, sheet_name = sheet) s <- lookup_sheet(nrow(x$sheets), sheets_df = x$sheets) } else { # create request to clear the data and formatting in pre-existing sheet requests <- c( requests, list(bureq_clear_sheet(s$id)) ) } gs4_bullets(c(v = "Writing to sheet {.w_sheet {s$name}}.")) # create request to write data frame into sheet ------------------------------ requests <- c( requests, prepare_df(s$id, data) ) # do it ---------------------------------------------------------------------- req <- request_generate( "sheets.spreadsheets.batchUpdate", params = list( spreadsheetId = ssid, requests = requests, responseIncludeGridData = FALSE ) ) resp_raw <- request_make(req) gargle::response_process(resp_raw) invisible(ssid) } #' @rdname sheet_write #' @export write_sheet <- sheet_write googlesheets4/R/schema_GridCoordinate.R0000644000176200001440000000176414074074641017606 0ustar liggesusersas_GridCoordinate <- function(x, ...) { UseMethod("as_GridCoordinate") } #' @export as_GridCoordinate.default <- function(x, ...) { abort_unsupported_conversion(x, to = "GridCoordinate") } #' @export as_GridCoordinate.range_spec <- function(x, ..., strict = TRUE) { grid_range <- as_GridRange(x) if (identical(names(grid_range), "sheetId")) { return(new("GridCoordinate", sheetId = grid_range$sheetId)) } if (strict) { row_index_diff <- grid_range$endRowIndex - grid_range$startRowIndex col_index_diff <- grid_range$endColumnIndex - grid_range$startColumnIndex if (row_index_diff != 1 || col_index_diff != 1) { gs4_abort(c( "Range must identify exactly 1 cell:", x = "Invalid cell range: {.range {x$cell_range}}" )) } } grid_range <- grid_range %>% discard(is.null) %>% discard(is.na) new( "GridCoordinate", sheetId = grid_range$sheetId, rowIndex = grid_range$startRowIndex, columnIndex = grid_range$startColumnIndex ) } googlesheets4/R/request_generate.R0000644000176200001440000000644714076034723016735 0ustar liggesusers#' Generate a Google Sheets API request #' #' @description Generate a request, using knowledge of the [Sheets #' API](https://developers.google.com/sheets/api/) from its Discovery #' Document (`https://www.googleapis.com/discovery/v1/apis/sheets/v4/rest`). Use #' [request_make()] to execute the request. Most users should, instead, use #' higher-level wrappers that facilitate common tasks, such as reading or #' writing worksheets or cell ranges. The functions here are intended for #' internal use and for programming around the Sheets API. #' #' @description `request_generate()` lets you provide the bare minimum of input. #' It takes a nickname for an endpoint and: #' * Uses the API spec to look up the `method`, `path`, and `base_url`. #' * Checks `params` for validity and completeness with respect to the #' endpoint. Uses `params` for URL endpoint substitution and separates #' remaining parameters into those destined for the body versus the query. #' * Adds an API key to the query if and only if `token = NULL`. #' #' @param endpoint Character. Nickname for one of the selected Sheets API v4 #' endpoints built into googlesheets4. Learn more in [gs4_endpoints()]. #' @param params Named list. Parameters destined for endpoint URL substitution, #' the query, or the body. #' @param key API key. Needed for requests that don't contain a token. The need #' for an API key in the absence of a token is explained in Google's document #' [Credentials, access, security, and #' identity](https://support.google.com/googleapi/answer/6158857?hl=en&ref_topic=7013279). #' In order of precedence, these sources are consulted: the formal `key` #' argument, a `key` parameter in `params`, a user-configured API key set up #' with [gs4_auth_configure()] and retrieved with [gs4_api_key()]. #' @param token Set this to `NULL` to suppress the inclusion of a token. Note #' that, if auth has been de-activated via [gs4_deauth()], #' `gs4_token()` will actually return `NULL`. #' #' @return `list()`\cr Components are `method`, `url`, `body`, and `token`, #' suitable as input for [request_make()]. #' @export #' @family low-level API functions #' @seealso [gargle::request_develop()], [gargle::request_build()], #' [gargle::request_make()] #' @examples #' req <- request_generate( #' "sheets.spreadsheets.get", #' list(spreadsheetId = gs4_example("deaths")), #' key = "PRETEND_I_AM_AN_API_KEY", #' token = NULL #' ) #' req request_generate <- function(endpoint = character(), params = list(), key = NULL, token = gs4_token()) { ept <- .endpoints[[endpoint]] if (is.null(ept)) { gs4_abort(c("Endpoint not recognized:", x = "{.field {endpoint}}")) } # if there are problems in `params`, such as a nonexistent item, # let's complain now force(params) ## modifications specific to googlesheets4 package params$key <- key %||% params$key %||% gs4_api_key() %||% gargle::tidyverse_api_key() req <- gargle::request_develop( endpoint = ept, params = params, base_url = attr(.endpoints, which = "base_url", exact = TRUE) ) gargle::request_build( path = req$path, method = req$method, params = req$params, body = req$body, token = token, base_url = req$base_url ) } googlesheets4/R/sheet_properties.R0000644000176200001440000000120114074074641016737 0ustar liggesusers#' Get data about (work)sheets #' #' Reveals full metadata or just the names for the (work)sheets inside a #' (spread)Sheet. #' #' @eval param_ss() #' #' @return #' * `sheet_properties()`: A tibble with one row per (work)sheet. #' * `sheet_names()`: A character vector of (work)sheet names. #' @export #' @family worksheet functions #' @examples #' if (gs4_has_token()) { #' ss <- gs4_example("gapminder") #' sheet_properties(ss) #' sheet_names(ss) #' } sheet_properties <- function(ss) { x <- gs4_get(ss) pluck(x, "sheets") } #' @export #' @rdname sheet_properties sheet_names <- function(ss) { sheet_properties(ss)$name } googlesheets4/R/schemas.R0000644000176200001440000000340314074074641015004 0ustar liggesusersnew <- function(id, ...) { schema <- .tidy_schemas[[id]] if (is.null(schema)) { gs4_abort("Can't find a tidy schema with id {.field {id}}.") } dots <- list2(...) dots <- discard(dots, is.null) check_against_schema(dots, schema = schema) structure( dots, # explicit 'list' class is a bit icky but makes jsonlite happy # in various vctrs futures, this could need revisiting class = c(id_as_class(id), "googlesheets4_schema", "list"), schema = schema ) } # TODO: if it proves necessary, this could do more meaningful checks check_against_schema <- function(x, schema = NULL, id = NA_character_) { schema <- schema %||% .tidy_schemas[[id %|% id_from_class(x)]] %||% attr(x, "schema") if (is.null(schema)) { gs4_abort(" Trying to check an object of class {.cls {class(x)}}, \\ but can't get a schema.") } stopifnot(is_dictionaryish(x)) unexpected <- setdiff(names(x), schema$property) if (length(unexpected) > 0) { gs4_abort(c( "Properties not recognized for the {.field {attr(schema, 'id')}} schema:", bulletize(gargle_map_cli(unexpected), bullet = "x") )) } x } id_as_class <- function(id) glue("googlesheets4_schema_{id}") id_from_class <- function(x) { m <- grepl("^googlesheets4_schema_", class(x)) if (!any(m)) { return(NA_character_) } m <- which(m)[1] sub("^googlesheets4_schema_", "", class(x)[m]) } # patch ---- patch <- function(x, ...) { UseMethod("patch") } #' @export patch.default <- function(x, ...) { gs4_abort(" Don't know how to {.fun patch} an object of class {.cls {class(x)}}.") } #' @export patch.googlesheets4_schema <- function(x, ...) { dots <- list2(...) dots <- discard(dots, is.null) x[names(dots)] <- dots check_against_schema(x) } googlesheets4/R/get_cells.R0000644000176200001440000001467214074074641015334 0ustar liggesusers## this is the "cell getter" for range_read_cells() and read_sheet() get_cells <- function(ss, sheet = NULL, range = NULL, col_names_in_sheet = TRUE, skip = 0, n_max = Inf, detail_level = c("default", "full"), discard_empty = TRUE) { ssid <- as_sheets_id(ss) maybe_sheet(sheet) check_range(range) check_bool(col_names_in_sheet) check_non_negative_integer(skip) check_non_negative_integer(n_max) detail_level <- match.arg(detail_level) check_bool(discard_empty) ## retrieve spreadsheet metadata -------------------------------------------- x <- gs4_get(ssid) gs4_bullets(c(v = "Reading from {.s_sheet {x$name}}.")) ## prepare range specification for API -------------------------------------- ## user's range, sheet, skip --> qualified A1 range, suitable for API range_spec <- as_range_spec( range, sheet = sheet, skip = skip, sheets_df = x$sheets, nr_df = x$named_ranges ) # if we send no range, we get all cells from all sheets; not what we want effective_range <- as_A1_range(range_spec) %||% first_visible_name(x$sheets) gs4_bullets(c(v = "Range {.range {effective_range}}.")) ## main GET ----------------------------------------------------------------- resp <- read_cells_impl_( ssid, ranges = effective_range, detail_level = detail_level ) out <- cells(resp) if (discard_empty) { # cells can be present, just because they bear a format (much like Excel) cell_is_empty <- map_lgl(out$cell, ~ is.null(pluck(.x, "effectiveValue"))) out <- out[!cell_is_empty, ] } ## enforce geometry on the cell data frame ---------------------------------- if (range_spec$shim) { range_spec$cell_limits <- range_spec$cell_limits %||% as_cell_limits(effective_range) out <- insert_shims(out, range_spec$cell_limits) ## guarantee: ## every row and every column spanned by user's range is represented by at ## least one cell, (could be placeholders w/ no content from API, though) ## ## NOTE: ## this does NOT imply that every spreadsheet cell spanned by user's range ## is represented by a cell in 'out' --> rectangling must be robust to holes } else if (n_max < Inf) { out <- enforce_n_max(out, n_max, col_names_in_sheet) } out } # https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/get # I want a separate worker so there is a version of this available that # accepts `fields` (or `includeGridData`), yet I don't want a user-facing # function that exposes those details read_cells_impl_ <- function(ssid, ranges, fields = NULL, detail_level = c("default", "full")) { # there are 2 ways to control the level of detail re: cell data: # 1. Supply a field mask. What we currently do. # 2. Set `includeGridData` to true. This gets *everything* about the # Spreadsheet and the Sheet(s). So far, this seems like TMI. detail_level <- match.arg(detail_level) cell_mask <- switch( detail_level, "default" = ".values(effectiveValue,formattedValue,effectiveFormat.numberFormat)", "full" = "" ) default_fields <- c( "spreadsheetId", "properties.title", "sheets.properties(sheetId,title)", glue("sheets.data(startRow,startColumn,rowData{cell_mask})") ) fields <- fields %||% glue_collapse(default_fields, sep = ",") req <- request_generate( "sheets.spreadsheets.get", params = list( spreadsheetId = ssid, ranges = ranges, fields = fields ) ) raw_resp <- request_make(req) gargle::response_process(raw_resp) } ## input: an instance of Spreadsheet ## https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets#Spreadsheet ## output: a tibble with one row per non-empty cell (row, column, cell) cells <- function(x = list()) { ## identify upper left cell of the rectangle ## values are absent in the response if equal to 0, hence the default ## return values are zero-based, hence we add 1 start_row <- (pluck(x, "sheets", 1, "data", 1, "startRow") %||% 0) + 1 start_column <- (pluck(x, "sheets", 1, "data", 1, "startColumn") %||% 0) + 1 # TODO: make this an as_tibble method? # TODO: deal with the merged cells # TODO: ensure this returns integer columns where appropriate row_data <- x %>% pluck("sheets", 1, "data", 1, "rowData") %>% map("values") ## an empty row can be present as an explicit NULL ## within a non-empty row, an empty cell can be present as list() ## rows are ragged and appear to end at the last non-empty cell row_lengths <- map_int(row_data, length) n_rows <- length(row_data) tibble::tibble( row = rep.int( seq.int(from = start_row, length.out = n_rows), times = row_lengths ), col = as.integer(start_column + sequence(row_lengths) - 1), cell = purrr::flatten(row_data) ) } insert_shims <- function(df, cell_limits) { ## emulating behaviour of readxl if (nrow(df) == 0) { return(df) } df$row <- as.integer(df$row) df$col <- as.integer(df$col) ## 1-based indices, referring to cell coordinates in the spreadsheet start_row <- cell_limits$ul[[1]] end_row <- cell_limits$lr[[1]] start_col <- cell_limits$ul[[2]] end_col <- cell_limits$lr[[2]] shim_up <- notNA(start_row) && start_row < min(df$row) shim_left <- notNA(start_col) && start_col < min(df$col) shim_down <- notNA(end_row) && end_row > max(df$row) shim_right <- notNA(end_col) && end_col > max(df$col) ## add placeholder to establish upper left corner if (shim_up || shim_left) { df <- tibble::add_row( df, row = start_row %|% min(df$row), col = start_col %|% min(df$col), cell = list(list()), .before = 1 ) } ## add placeholder to establish lower right corner if (shim_down || shim_right) { df <- tibble::add_row( df, row = end_row %|% max(df$row), col = end_col %|% max(df$col), cell = list(list()) ) } df } enforce_n_max <- function(out, n_max, col_names_in_sheet) { row_max <- realize_n_max(n_max, out$row, col_names_in_sheet) out[out$row <= row_max, ] } realize_n_max <- function(n_max, rows, col_names_in_sheet) { start_row <- min(rows) end_row <- max(rows) n_read <- end_row - start_row + 1 to_read <- n_max + col_names_in_sheet if (n_read <= to_read) { Inf } else { start_row + to_read - 1 } } googlesheets4/R/range_add_named.R0000644000176200001440000000466614074074641016445 0ustar liggesusers#' Add a named range #' #' Adds a named range. Not really ready for showtime yet, so not exported. But #' I need it to (re)create the 'deaths' example Sheet. #' #' @noRd #' #' @eval param_ss() #' @param name Name for the new named range. #' @eval param_sheet(action = "SOMETHING") #' @template range #' #' @template ss-return #' @keywords internal #' @examples #' if (gs4_has_token()) { #' dat <- data.frame(x = 1:3, y = letters[1:3]) #' ss <- gs4_create("range-add-named-demo", sheets = list(alpha = dat)) #' #' ss %>% #' range_add_named("two_rows", sheet = "alpha", range = "A2:B3") #' #' # notice the 'two_rows' named range reported here #' ss #' #' # clean up #' gs4_find("range-add-named-demo") %>% #' googledrive::drive_trash() #' } range_add_named <- function(ss, name, sheet = NULL, range = NULL) { ssid <- as_sheets_id(ss) name <- check_string(name) maybe_sheet(sheet) check_range(range) x <- gs4_get(ssid) gs4_bullets(c(v = "Working in {.s_sheet {x$name}}.")) # determine (work)sheet ------------------------------------------------------ range_spec <- as_range_spec( range, sheet = sheet, sheets_df = x$sheets, nr_df = x$named_ranges ) range_spec$sheet_name <- range_spec$sheet_name %||% first_visible_name(x$sheets) # form batch update request -------------------------------------------------- req <- list(addNamedRange = new( "AddNamedRangeRequest", namedRange = as_NamedRange(range_spec, name = name) )) # do it ---------------------------------------------------------------------- req <- request_generate( "sheets.spreadsheets.batchUpdate", params = list( spreadsheetId = ssid, requests = list(req) ) ) resp_raw <- request_make(req) reply <- gargle::response_process(resp_raw) reply <- pluck(reply, "replies", 1, "addNamedRange", "namedRange") reply <- new("NamedRange", !!!reply) # TODO: this would not be so janky if new_googlesheets4_spreadsheet() were # factored in a way I could make better use of its logic reply <- as.list(as_tibble(reply)) reply$sheet_name <- vlookup( reply$sheet_id, data = x$sheets, key = "id", value = "name" ) A1_range <- qualified_A1(reply$sheet_name, do.call(make_cell_range, reply)) gs4_bullets(c( v = "Created new range named {.range {reply$name}} \\ representing {.range {A1_range}}.")) invisible(ssid) } googlesheets4/R/utils-ui.R0000644000176200001440000001106214074134671015134 0ustar liggesusersgs4_theme <- function() { list( span.field = list(transform = single_quote_if_no_color), # This is same as custom `.drivepath` style in googledrive span.s_sheet = list(color = "cyan", fmt = double_quote_weird_name), span.w_sheet = list(color = "green", fmt = single_quote_weird_name), span.range = list(color = "yellow", fmt = single_quote_weird_name), # since we're using color so much elsewhere, I think the standard bullet # should be "normal" color; matches what I do in googledrive ".memo .memo-item-*" = list( "text-exdent" = 2, before = function(x) paste0(cli::symbol$bullet, " ") ) ) } single_quote_weird_name <- function(x) { utils::getFromNamespace("quote_weird_name", "cli")(x) } # this is just the body of cli's quote_weird_name() but with a double quote double_quote_weird_name <- function(x) { x2 <- utils::getFromNamespace("quote_weird_name0", "cli")(x) if (x2[[2]] || cli::num_ansi_colors() == 1) { x2[[1]] <- paste0('"', x2[[1]], '"') } x2[[1]] } single_quote_if_no_color <- function(x) quote_if_no_color(x, "'") double_quote_if_no_color <- function(x) quote_if_no_color(x, '"') quote_if_no_color <- function(x, quote = "'") { # TODO: if a better way appears in cli, use it # @gabor says: "if you want to have before and after for the no-color case # only, we can have a selector for that, such as: # span.field::no-color # (but, at the time I write this, cli does not support this yet) if (cli::num_ansi_colors() > 1) { x } else { paste0(quote, x, quote) } } # useful to me during development, so I can see how my messages look w/o color local_no_color <- function(.envir = parent.frame()) { withr::local_envvar(c("NO_COLOR" = 1), .local_envir = .envir) } with_no_color <- function(code) { withr::with_envvar(c("NO_COLOR" = 1), code) } message <- function(...) { gs4_abort(" Internal error: use the UI functions in {.pkg googlesheets4} \\ instead of {.fun message}") } fr <- function(x) format(x, justify = 'right') fl <- function(x) format(x, justify = 'left') gs4_quiet <- function() { getOption("googlesheets4_quiet", default = NA) } #' @export #' @rdname googlesheets4-configuration #' @param env The environment to use for scoping #' @examples #' if (gs4_has_token()) { #' # message: "Creating new Sheet ..." #' (ss <- gs4_create("gs4-quiet-demo", sheets = "alpha")) #' #' # message: "Editing ..., Writing ..." #' range_write(ss, data = data.frame(x = 1, y = "a")) #' #' # suppress messages for a small amount of code #' with_gs4_quiet( #' ss %>% sheet_append(data.frame(x = 2, y = "b")) #' ) #' #' # message: "Writing ..., Appending ..." #' ss %>% sheet_append(data.frame(x = 3, y = "c")) #' #' # suppress messages until end of current scope #' local_gs4_quiet() #' ss %>% sheet_append(data.frame(x = 4, y = "d")) #' #' # see that all the data was, in fact, written #' read_sheet(ss) #' #' # clean up #' gs4_find("gs4-quiet-demo") %>% #' googledrive::drive_trash() #' } local_gs4_quiet <- function(env = parent.frame()) { withr::local_options(list(googlesheets4_quiet = TRUE), .local_envir = env) } local_gs4_loud <- function(env = parent.frame()) { withr::local_options(list(googlesheets4_quiet = FALSE), .local_envir = env) } #' @export #' @rdname googlesheets4-configuration #' @param code Code to execute quietly with_gs4_quiet <- function(code) { withr::with_options(list(googlesheets4_quiet = TRUE), code = code) } is_testing <- function() { identical(Sys.getenv("TESTTHAT"), "true") } gs4_bullets <- function(text, .envir = parent.frame()) { quiet <- gs4_quiet() %|% is_testing() if (quiet) { return(invisible()) } cli::cli_div(theme = gs4_theme()) cli::cli_bullets(text = text, .envir = .envir) } #' Error conditions for the googlesheets4 package #' #' @param class Use only if you want to subclass beyond `googlesheets4_error` #' #' @keywords internal #' @name gs4-errors #' @noRd NULL gs4_abort <- function(message, ..., class = NULL, .envir = parent.frame()) { cli::cli_div(theme = gs4_theme()) cli::cli_abort( message = message, ..., class = c(class, "googlesheets4_error"), .envir = .envir ) } # helpful in the default method of an as_{to} generic # exists mostly to template the message abort_unsupported_conversion <- function(from, to) { if (is.null(from)) { msg_from <- "{.code NULL}" } else { msg_from <- "something of class {.cls {class(from)}}" } msg <- glue(" Don't know how to make an instance of {.cls {to}} from <>.", .open = "<<", .close = ">>" ) gs4_abort(msg) } googlesheets4/R/range_add_protection.R0000644000176200001440000001566314074074641017546 0ustar liggesusers#' Protect a cell range #' #' @description #' *Note: not yet exported, still very alpha. Usage still requires using #' low-level helpers. This documentation is for ME.* #' #' `range_add_protection()` protects a range of cells against editing. #' #' @eval param_ss() #' @eval param_sheet() #' @param range Cells to protect. This `range` argument works very much like #' `range` in, for example, [range_read()]). Specific things to note: #' You can omit `range` to protect a whole sheet and `range` can be a named #' range. #' @param ... Optional arguments used when constructing the `ProtectedRange` #' object. Use this is you want to set `description`, `warningOnly`, #' `unprotectedRanges`, or `editors`. For advanced use. #' #' @template ss-return #' @seealso Makes an `AddProtectedRangeRequest`: #' * #' #' Documentation on the `ProtectedRange` object: #' * #' #' @keywords internal #' @noRd #' #' @examples #' if (gs4_has_token()) { #' # create a data frame to use as initial data #' dat <- gs4_fodder(3) #' #' # create Sheet, add a couple more sheets #' ss <- gs4_create("range-add-protection-example", sheets = dat) #' sheet_write(head(chickwts), ss, sheet = "chickwts") #' sheet_write(head(mtcars), ss, sheet = "mtcars") #' sheet_write(ToothGrowth, ss, sheet = "ToothGrowth") #' #' # add myself and get it open in the browser #' gs4_share(ss, type = "user", emailAddress = "jenny@rstudio.com", role = "writer") #' gs4_browse(ss) #' #' # protect a whole sheet #' ss %>% #' range_add_protection(sheet = "dat", description = "whole sheet") #' #' # create a named range, then protect it #' ss %>% #' range_add_named("feed", sheet = "chickwts", range = "B:B") %>% #' range_add_protection(range = "feed", description = "named range") #' #' # protect an arbitrary rectangle and add an editor #' ss %>% #' range_add_protection( #' range = "mtcars!1:1", #' description = "single row", #' editors = new("Editors", users = "jenny@rstudio.com") #' ) #' #' # check in on the protected ranges we've created #' ss_info <- gs4_get(ss) #' ss_info$protected_ranges #' #' # protect a sheet EXCEPT certain columns that can be edited #' unprotect_this <- as_range_spec( #' "C:C", #' sheet = "ToothGrowth", #' sheets_df = ss_info$sheets, nr_df = ss_info$named_ranges #' ) #' unprotect_range <- as_GridRange(unprotect_this) #' ss %>% #' range_add_protection( #' sheet = "ToothGrowth", #' description = "sheet MINUS some cols", #' unprotectedRanges = unprotect_range #' ) #' #' # look at the editors for our protected ranges #' ss_info <- gs4_get(ss) #' ss_info$protected_ranges #' ss_info$protected_ranges$editors #' #' # add an editor to a protected range #' id <- ss_info$protected_ranges$protected_range_id[[1]] #' range_update_protection( #' ss, #' protectedRangeId = id, #' editors = new("Editors", users = "jenny@rstudio.com") #' ) #' #' # confirm the editor change happened #' ss_info <- gs4_get(ss) #' ss_info$protected_ranges$editors #' #' # delete protections from a range #' id <- ss_info$protected_ranges$protected_range_id[[3]] #' range_delete_protection(ss, id = id) #' #' # confirm the deletion happened #' ss_info <- gs4_get(ss) #' ss_info$protected_ranges #' #' # clean up #' gs4_find("range-add-protection-example") %>% #' googledrive::drive_trash() #' } range_add_protection <- function(ss, sheet = NULL, range = NULL, ...) { ssid <- as_sheets_id(ss) maybe_sheet(sheet) check_range(range) x <- gs4_get(ssid) gs4_bullets(c(v = "Editing {.s_sheet {x$name}}.")) # determine range ------------------------------------------------------------ range_spec <- as_range_spec( range, sheet = sheet, sheets_df = x$sheets, nr_df = x$named_ranges ) if (is.null(range_spec$named_range)) { range_spec$sheet_name <- range_spec$sheet_name %||% first_visible_name(x$sheets) gs4_bullets(c( v = "Protecting cells on sheet: {.w_sheet {range_spec$sheet_name}}.")) } else { gs4_bullets(c( v = "Protecting named range: {.range {range_spec$named_range}}.")) } # form batch update request -------------------------------------------------- prot_req <- list(addProtectedRange = new( "AddProtectedRangeRequest", protectedRange = new_ProtectedRange(range_spec, ...) )) # do it ---------------------------------------------------------------------- req <- request_generate( "sheets.spreadsheets.batchUpdate", params = list( spreadsheetId = ssid, requests = list(prot_req) ) ) resp_raw <- request_make(req) gargle::response_process(resp_raw) invisible(ssid) } # helpers ---- new_ProtectedRange <- function(range_spec, ...) { if (is.null(range_spec$named_range)) { out <- new("ProtectedRange", range = as_GridRange(range_spec)) } else { out <- new( "ProtectedRange", namedRangeId = vlookup(range_spec$named_range, range_spec$nr_df, "name", "id") ) } out <- patch(out, editors = new("Editors", domainUsersCanEdit = FALSE)) patch(out, ...) } # even less polished one-offs used during development range_update_protection <- function(ss, ...) { ssid <- as_sheets_id(ss) x <- gs4_get(ssid) gs4_bullets(c(v = "Editing {.s_sheet {x$name}}.")) # form batch update request -------------------------------------------------- protected_range <- new("ProtectedRange", ...) mask <- gargle::field_mask(protected_range) # I have no idea why this is necessary, but it's the only way I've been able # to updated editors mask <- sub("editors.users", "editors", mask) prot_req <- list(updateProtectedRange = new( "UpdateProtectedRangeRequest", protectedRange = protected_range, fields = mask )) # do it ---------------------------------------------------------------------- req <- request_generate( "sheets.spreadsheets.batchUpdate", params = list( spreadsheetId = ssid, requests = list(prot_req) ) ) resp_raw <- request_make(req) gargle::response_process(resp_raw) invisible(ssid) } range_delete_protection <- function(ss, id) { ssid <- as_sheets_id(ss) x <- gs4_get(ssid) gs4_bullets(c(v = "Editing {.s_sheet {x$name}}.")) # form batch update request -------------------------------------------------- prot_req <- list(deleteProtectedRange = new( "DeleteProtectedRangeRequest", protectedRangeId = id )) # do it ---------------------------------------------------------------------- req <- request_generate( "sheets.spreadsheets.batchUpdate", params = list( spreadsheetId = ssid, requests = list(prot_req) ) ) resp_raw <- request_make(req) gargle::response_process(resp_raw) invisible(ssid) } googlesheets4/R/gs4_fodder.R0000644000176200001440000000174414074074641015407 0ustar liggesusers#' Create useful spreadsheet filler #' #' Creates a data frame that is useful for filling a spreadsheet, when you just #' need a sheet to experiment with. The data frame has `n` rows and `m` columns #' with these properties: #' * Column names match what Sheets displays: "A", "B", "C", and so on. #' * Inner cell values reflect the coordinates where each value will land in #' the sheet, in A1-notation. So the first row is "B2", "C2", and so on. #' Note that this `n`-row data frame will occupy `n + 1` rows in the sheet, #' because the column names occupy the first row. #' #' @param n Number of rows. #' @param m Number of columns. #' #' @return A data frame of character vectors. #' @export #' #' @examples #' gs4_fodder() #' gs4_fodder(5, 3) gs4_fodder <- function(n = 10, m = n) { columns <- LETTERS[seq_len(m)] names(columns) <- columns f <- function(number, letter) paste0(letter, number) as.data.frame( outer(seq_len(n) + 1, columns, f), stringsAsFactors = FALSE ) } googlesheets4/R/gs4_formula.R0000644000176200001440000000734114074074641015610 0ustar liggesusersnew_formula <- function(x = character()) { vec_assert(x, character()) new_vctr(x, class = "googlesheets4_formula") } #' Class for Google Sheets formulas #' #' In order to write a formula into Google Sheets, you need to store it as an #' object of class `googlesheets4_formula`. This is how we distinguish a #' "regular" character string from a string that should be interpreted as a #' formula. `googlesheets4_formula` is an S3 class implemented using the [vctrs #' package](https://vctrs.r-lib.org/articles/s3-vector.html). #' #' @param x Character. #' #' @return An S3 vector of class `googlesheets4_formula`. #' @export #' @family write functions #' #' @examples #' if (gs4_has_token()) { #' dat <- data.frame(x = c(1, 5, 3, 2, 4, 6)) #' #' ss <- gs4_create("gs4-formula-demo", sheets = dat) #' ss #' #' summaries <- tibble::tribble( #' ~desc, ~summaries, #' "max", "=max(A:A)", #' "sum", "=sum(A:A)", #' "min", "=min(A:A)", #' "sparkline", "=SPARKLINE(A:A, {\"color\", \"blue\"})" #' ) #' #' # explicitly declare a column as `googlesheets4_formula` #' summaries$summaries <- gs4_formula(summaries$summaries) #' summaries #' #' range_write(ss, data = summaries, range = "C1", reformat = FALSE) #' #' miscellany <- tibble::tribble( #' ~desc, ~example, #' "hyperlink", "=HYPERLINK(\"http://www.google.com/\",\"Google\")", #' "image", "=IMAGE(\"https://www.google.com/images/srpr/logo3w.png\")" #' ) #' miscellany$example <- gs4_formula(miscellany$example) #' miscellany #' #' sheet_write(miscellany, ss = ss) #' #' # clean up #' gs4_find("gs4-formula-demo") %>% #' googledrive::drive_trash() #' } gs4_formula <- function(x = character()) { x <- vec_cast(x, character()) new_formula(x) } #' @importFrom methods setOldClass setOldClass(c("googlesheets4_formula", "vctrs_vctr")) #' @export vec_ptype_abbr.googlesheets4_formula <- function(x, ...) { "fmla" } #' @method vec_ptype2 googlesheets4_formula #' @export vec_ptype2.googlesheets4_formula #' @export #' @rdname googlesheets4-vctrs vec_ptype2.googlesheets4_formula <- function(x, y, ...) { UseMethod("vec_ptype2.googlesheets4_formula", y) } #' @method vec_ptype2.googlesheets4_formula default #' @export vec_ptype2.googlesheets4_formula.default <- function(x, y, ..., x_arg = "x", y_arg = "y") { vec_default_ptype2(x, y, x_arg = x_arg, y_arg = y_arg) } #' @method vec_ptype2.googlesheets4_formula googlesheets4_formula #' @export vec_ptype2.googlesheets4_formula.googlesheets4_formula <- function(x, y, ...) { new_formula() } #' @method vec_ptype2.googlesheets4_formula character #' @export vec_ptype2.googlesheets4_formula.character <- function(x, y, ...) character() #' @method vec_ptype2.character googlesheets4_formula #' @export vec_ptype2.character.googlesheets4_formula <- function(x, y, ...) character() #' @method vec_cast googlesheets4_formula #' @export vec_cast.googlesheets4_formula #' @export #' @rdname googlesheets4-vctrs vec_cast.googlesheets4_formula <- function(x, to, ...) { UseMethod("vec_cast.googlesheets4_formula") } #' @method vec_cast.googlesheets4_formula default #' @export vec_cast.googlesheets4_formula.default <- function(x, to, ...) { vec_default_cast(x, to) } #' @method vec_cast.googlesheets4_formula googlesheets4_formula #' @export vec_cast.googlesheets4_formula.googlesheets4_formula <- function(x, to, ...) { x } #' @method vec_cast.googlesheets4_formula character #' @export vec_cast.googlesheets4_formula.character <- function(x, to, ...) { gs4_formula(x) } #' @method vec_cast.character googlesheets4_formula #' @export vec_cast.character.googlesheets4_formula <- function(x, to, ...) { vec_data(x) } googlesheets4/R/aaa.R0000644000176200001440000000005714075142116014077 0ustar liggesusers.googlesheets4 <- new.env(parent = emptyenv()) googlesheets4/R/gs4_example.R0000644000176200001440000000747314075153236015603 0ustar liggesusers#' Example Sheets #' #' googlesheets4 makes a variety of world-readable example Sheets available for #' use in documentation and reprexes. These functions help you access the #' example Sheets. See `vignette("example-sheets", package = "googlesheets4")` #' for more. #' #' @param matches A regular expression that matches the name of the desired #' example Sheet(s). `matches` is optional for the plural `gs4_examples()` #' and, if provided, it can match multiple Sheets. The singular #' `gs4_example()` requires `matches` and it must match exactly one Sheet. #' #' @return #' * `gs4_example()`: a [sheets_id] #' * `gs4_examples()`: a named vector of all built-in examples, with class #' [`drive_id`][googledrive::as_id] #' #' @name gs4_examples #' @examplesIf gs4_has_token() #' gs4_examples() #' gs4_examples("gap") #' #' gs4_example("gapminder") #' gs4_example("deaths") NULL #' @rdname gs4_examples #' @export gs4_examples <- function(matches) { many_sheets( needle = matches, haystack = example_and_test_sheets("example"), adjective = "example" ) } #' @rdname gs4_examples #' @export gs4_example <- function(matches) { one_sheet( needle = matches, haystack = example_and_test_sheets("example"), adjective = "example" ) } many_sheets <- function(needle, haystack, adjective) { out <- haystack if (!missing(needle)) { check_string(needle) sel <- grepl(needle, names(out), ignore.case = TRUE) if (!any(sel)) { gs4_abort("Can't find {adjective} Sheet that matches {.q {needle}}.") } out <- as_id(out[sel]) } out } one_sheet <- function(needle, haystack, adjective) { check_string(needle) out <- many_sheets(needle = needle, haystack = haystack, adjective = adjective) if (length(out) > 1) { gs4_abort(c( "Found multiple matching {adjective} Sheets:", bulletize(gargle_map_cli(names(out), template = "{.s_sheet <>}")), i = "Make the {.arg matches} regular expression more specific." )) } as_sheets_id(out) } example_and_test_sheets <- function(purpose = NULL) { # inlining env_cache() logic, so I don't need bleeding edge rlang if (!env_has(.googlesheets4, "example_and_test_sheets")) { inventory_id <- "1dSIZ2NkEPDWiEbsg9G80Hr9Xe7HZglEAPwGhVa-OSyA" local_gs4_quiet() if (!gs4_has_token()) { # don't trigger auth just for this local_deauth() } dat <- read_sheet(as_sheets_id(inventory_id)) env_poke(.googlesheets4, "example_and_test_sheets", dat) } dat <- env_get(.googlesheets4, "example_and_test_sheets") if (!is.null(purpose)) { dat <- dat[dat$purpose == purpose, ] } out <- dat$id names(out) <- dat$name as_id(out) } # test sheet management ---- test_sheets <- function(matches) { many_sheets( needle = matches, haystack = example_and_test_sheets("test"), adjective = "test" ) } test_sheet <- function(matches = "googlesheets4-cell-tests") { one_sheet( needle = matches, haystack = example_and_test_sheets("test"), adjective = "test" ) } test_sheet_create <- function(name = "googlesheets4-cell-tests") { stopifnot(is_string(name)) user <- gs4_user() if (!grepl("^googlesheets4-sheet-keeper", user)) { user <- sub("@.+$", "", user) gs4_abort(" Must be auth'd as {.email googlesheets4-sheet-keeper}, \\ not {.email {user}}.") } existing <- gs4_find() m <- match(name, existing$name) if (is.na(m)) { gs4_bullets(c(v = "Creating {.s_sheet {name}}.")) ss <- gs4_create(name) } else { gs4_bullets(c( v = "Testing sheet named {.s_sheet {name}} already exists ... using that." )) ss <- existing$id[[m]] } ssid <- as_sheets_id(ss) # it's fiddly to check current sharing status, so just re-share gs4_bullets(c(v = 'Making sure "anyone with a link" can read {.s_sheet {name}}.')) gs4_share(ssid) ssid } googlesheets4/R/utils-pipe.R0000644000176200001440000000031213257600057015446 0ustar liggesusers#' Pipe operator #' #' See \code{magrittr::\link[magrittr]{\%>\%}} for details. #' #' @name %>% #' @rdname pipe #' @keywords internal #' @export #' @importFrom magrittr %>% #' @usage lhs \%>\% rhs NULL googlesheets4/R/schema_Sheet.R0000644000176200001440000000225414074074641015754 0ustar liggesusers#' @export as_tibble.googlesheets4_schema_Sheet <- function(x, ...) { out <- as_tibble(new("SheetProperties", !!!x$properties)) # TODO: come back to deal with `data` tibble::add_column(out, data = list(NULL)) } as_Sheet <- function(x, ...) { UseMethod("as_Sheet") } #' @export as_Sheet.default <- function(x, ...) { abort_unsupported_conversion(x, to = "Sheet") } #' @export as_Sheet.NULL <- function(x, ...) { return(new(id = "Sheet", properties = NULL)) } #' @export as_Sheet.character <- function(x, ...) { check_length_one(x) new( "Sheet", properties = new(id = "SheetProperties", title = x), ... ) } #' @export as_Sheet.data.frame <- function(x, ...) { # do first, so that gridProperties derived from x overwrite anything passed # via `...` sp <- new("SheetProperties", ...) sp <- patch( sp, gridProperties = new( "GridProperties", rowCount = nrow(x) + 1, # make room for column names columnCount = ncol(x), ) ) new( "Sheet", properties = sp, data = list( # an array of instances of GridData list( rowData = as_RowData(x) # an array of instances of RowData ) ) ) } googlesheets4/R/roxygen.R0000644000176200001440000000270714075054251015056 0ustar liggesusers# functions to help reduce duplication and increase consistency in the docs ### ss ---- param_ss <- function(..., pname = "ss") { template <- glue(" @param {pname} \\ Something that identifies a Google Sheet: * its file id as a string or [`drive_id`][googledrive::as_id] * a URL from which we can recover the id * a one-row [`dribble`][googledrive::dribble], which is how googledrive represents Drive files * an instance of `googlesheets4_spreadsheet`, which is what [gs4_get()] returns Processed through [as_sheets_id()].") dots <- list2(...) if (length(dots) > 0) { template <- c(template, dots) } glue_collapse(template, sep = " ") } ### sheet ---- param_sheet <- function(..., action = "act on", pname = "sheet") { template <- glue(" @param {pname} \\ Sheet to {action}, in the sense of \"worksheet\" or \"tab\". \\ You can identify a sheet by name, with a string, or by position, \\ with a number. ") dots <- list2(...) if (length(dots) > 0) { template <- c(template, dots) } glue_collapse(template, sep = " ") } param_before_after <- function(sheet_text) { glue(" @param .before,.after \\ Optional specification of where to put the new {sheet_text}. \\ Specify, at most, one of `.before` and `.after`. Refer to an existing \\ sheet by name (via a string) or by position (via a number). If \\ unspecified, Sheets puts the new {sheet_text} at the end. ") } googlesheets4/R/schema_SheetProperties.R0000644000176200001440000000105213635427346020032 0ustar liggesusers#' @export as_tibble.googlesheets4_schema_SheetProperties <- function(x, ...) { tibble::tibble( # TODO: open question whether I should explicitly unescape title here name = glean_chr(x, "title"), index = glean_int(x, "index"), id = glean_int(x, "sheetId"), type = glean_chr(x, "sheetType"), visible = !glean_lgl(x, "hidden", .default = FALSE), grid_rows = glean_int(x, c("gridProperties", "rowCount")), grid_columns = glean_int(x, c("gridProperties", "columnCount")) ) } googlesheets4/R/sheet_append.R0000644000176200001440000000441314074141425016015 0ustar liggesusers#' Append rows to a sheet #' #' Adds one or more new rows after the last row with data in a (work)sheet, #' increasing the row dimension of the sheet if necessary. #' #' @eval param_ss() #' @param data A data frame. #' @eval param_sheet(action = "append to") #' #' @template ss-return #' @export #' @family write functions #' @family worksheet functions #' @seealso Makes an `AppendCellsRequest`: #' * #' #' @examples #' if (gs4_has_token()) { #' # we will recreate the table of "other" deaths from this example Sheet #' (deaths <- gs4_example("deaths") %>% #' range_read(range = "other_data", col_types = "????DD")) #' #' # split the data into 3 pieces, which we will send separately #' deaths_one <- deaths[ 1:5, ] #' deaths_two <- deaths[ 6, ] #' deaths_three <- deaths[7:10, ] #' #' # create a Sheet and send the first chunk of data #' ss <- gs4_create("sheet-append-demo", sheets = list(deaths = deaths_one)) #' #' # append a single row #' ss %>% sheet_append(deaths_two) #' #' # append remaining rows #' ss %>% sheet_append(deaths_three) #' #' # read and check against the original #' deaths_replica <- range_read(ss, col_types = "????DD") #' identical(deaths, deaths_replica) #' #' # clean up #' gs4_find("sheet-append-demo") %>% #' googledrive::drive_trash() #' } sheet_append <- function(ss, data, sheet = 1) { check_data_frame(data) ssid <- as_sheets_id(ss) check_sheet(sheet) x <- gs4_get(ssid) gs4_bullets(c(v = "Writing to {.s_sheet {x$name}}.")) s <- lookup_sheet(sheet, sheets_df = x$sheets) gs4_bullets(c(v = "Appending {nrow(data)} row{?s} to {.w_sheet {s$name}}.")) req <- request_generate( "sheets.spreadsheets.batchUpdate", params = list( spreadsheetId = ssid, requests = prepare_rows(s$id, data), responseIncludeGridData = FALSE ) ) resp_raw <- request_make(req) gargle::response_process(resp_raw) invisible(ssid) } prepare_rows <- function(sheet_id, df) { list(appendCells = new( "AppendCellsRequest", sheetId = sheet_id, rows = as_RowData(df, col_names = FALSE), # an array of instances of RowData fields = "userEnteredValue,userEnteredFormat.numberFormat" )) } googlesheets4/R/cell-specification.R0000644000176200001440000000403414074074641017117 0ustar liggesusers## this file represents the interface with the cellranger package #' Specify cells #' #' Many functions in googlesheets4 use a `range` argument to target specific #' cells. The Sheets v4 API expects user-specified ranges to be expressed via #' [its A1 #' notation](https://developers.google.com/sheets/api/guides/concepts#a1_notation), #' but googlesheets4 accepts and converts a few alternative specifications #' provided by the functions in the [cellranger][cellranger] package. Of course, #' you can always provide A1-style ranges directly to functions like #' [read_sheet()] or [range_read_cells()]. Why would you use the #' [cellranger][cellranger] helpers? Some ranges are practically impossible to #' express in A1 notation, specifically when you want to describe rectangles #' with some bounds that are specified and others determined by the data. #' #' @name cell-specification #' #' @examples #' if (gs4_has_token() && interactive()) { #' ss <- gs4_example("mini-gap") #' #' # Specify only the rows or only the columns #' read_sheet(ss, range = cell_rows(1:3)) #' read_sheet(ss, range = cell_cols("C:D")) #' read_sheet(ss, range = cell_cols(1)) #' #' # Specify upper or lower bound on row or column #' read_sheet(ss, range = cell_rows(c(NA, 4))) #' read_sheet(ss, range = cell_cols(c(NA, "D"))) #' read_sheet(ss, range = cell_rows(c(3, NA))) #' read_sheet(ss, range = cell_cols(c(2, NA))) #' read_sheet(ss, range = cell_cols(c("C", NA))) #' #' # Specify a partially open rectangle #' read_sheet(ss, range = cell_limits(c(2, 3), c(NA, NA)), col_names = FALSE) #' read_sheet(ss, range = cell_limits(c(1, 2), c(NA, 4))) #' } NULL #' @importFrom cellranger cell_limits #' @name cell_limits #' @export #' @rdname cell-specification NULL #' @importFrom cellranger cell_rows #' @name cell_rows #' @export #' @rdname cell-specification NULL #' @importFrom cellranger cell_cols #' @name cell_cols #' @export #' @rdname cell-specification NULL #' @importFrom cellranger anchored #' @name anchored #' @export #' @rdname cell-specification NULL googlesheets4/R/sheet_add.R0000644000176200001440000000735714074074641015315 0ustar liggesusers#' Add one or more (work)sheets #' #' Adds one or more (work)sheets to an existing (spread)Sheet. Note that sheet #' names must be unique. #' #' @eval param_ss() #' @param sheet One or more new sheet names. If unspecified, one new sheet is #' added and Sheets autogenerates a name of the form "SheetN". #' @param ... Optional parameters to specify additional properties, common to #' all of the new sheet(s). Not relevant to most users. Specify fields of the #' [`SheetProperties` #' schema](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/sheets#SheetProperties) #' in `name = value` form. #' @eval param_before_after("sheet(s)") #' #' @template ss-return #' #' @export #' @family worksheet functions #' @seealso #' Makes a batch of `AddSheetRequest`s (one per sheet): #' * #' #' @examples #' if (gs4_has_token()) { #' ss <- gs4_create("add-sheets-to-me") #' #' # the only required argument is the target spreadsheet #' ss %>% sheet_add() #' #' # but you CAN specify sheet name and/or position #' ss %>% sheet_add("apple", .after = 1) #' ss %>% sheet_add("banana", .after = "apple") #' #' # add multiple sheets at once #' ss %>% sheet_add(c("coconut", "dragonfruit")) #' #' # keeners can even specify additional sheet properties #' ss %>% #' sheet_add( #' sheet = "eggplant", #' .before = 1, #' gridProperties = list( #' rowCount = 3, columnCount = 6, frozenRowCount = 1 #' ) #' ) #' #' # get an overview of the sheets #' sheet_properties(ss) #' #' # clean up #' gs4_find("add-sheets-to-me") %>% #' googledrive::drive_trash() #' } sheet_add <- function(ss, sheet = NULL, ..., .before = NULL, .after = NULL) { ssid <- as_sheets_id(ss) maybe_character(sheet) x <- gs4_get(ssid) index <- resolve_index(x$sheets, .before, .after) n_new <- if (is.null(sheet)) 1 else length(sheet) gs4_bullets(c(v = "Adding {n_new} sheet{?s} to {.s_sheet {x$name}}:")) ss <- sheet_add_impl_(ssid, sheet_name = sheet, index = index, ...) new_sheet_names <- setdiff(ss$sheets$name, x$sheets$name) gs4_bullets( bulletize(gargle_map_cli(new_sheet_names, template = "{.w_sheet <>}")) ) invisible(ssid) } sheet_add_impl_ <- function(ssid, sheet_name = NULL, index = NULL, ...) { sheet_name <- sheet_name %||% list(NULL) dots <- list2(...) requests <- map( sheet_name, ~ make_addSheet(title = .x, index = index, dots = dots) ) req <- request_generate( "sheets.spreadsheets.batchUpdate", params = list( spreadsheetId = ssid, requests = requests, includeSpreadsheetInResponse = TRUE, responseIncludeGridData = FALSE ) ) resp_raw <- request_make(req) resp <- gargle::response_process(resp_raw) new_googlesheets4_spreadsheet(resp$updatedSpreadsheet) } resolve_index <- function(sheets_df, .before = NULL, .after = NULL) { if (is.null(.before) && is.null(.after)) { return(NULL) } if (is.null(.after)) { s <- lookup_sheet(.before, sheets_df = sheets_df) return(s$index) } if (is.numeric(.after)) { .after <- min(.after, nrow(sheets_df)) } s <- lookup_sheet(.after, sheets_df = sheets_df) s$index + 1 } make_addSheet <- function(title = NULL, index = NULL, dots = list()) { if (length(title) + length(index) + length(dots) == 0) { # if sending no sheet properties, this must be NULL and not list() return(list(addSheet = NULL)) } list(addSheet = new( "AddSheetRequest", properties = new("SheetProperties", title = title, index = index, !!!dots) )) } googlesheets4/R/schema_CellData.R0000644000176200001440000001062414074106643016353 0ustar liggesusers# Why not `new("CellData", ...)`? It seems excessive to store the schema as # an attribute for each cell. Possibly a premature concern. new_CellData <- function(...) { # explicit 'list' class is a bit icky but it makes jsonlite happy structure(list2(...), class = c( "googlesheets4_schema_CellData", "googlesheets4_schema", "list" )) } # Use this instead of `new_CellData()` when (light) validation makes sense. CellData <- function(...) { dots <- list2(...) stopifnot(is_dictionaryish(dots)) check_against_schema(dots, id = "CellData") new_CellData(...) } is_CellData <- function(x) inherits(x, "googlesheets4_schema_CellData") as_CellData <- function(x, .na = NULL) { UseMethod("as_CellData") } #' @export as_CellData.default <- function(x, .na = NULL) { abort_unsupported_conversion(x, to = "CellData") } # I want to centralize what we send for NA, even though -- for now, at # least -- I have not exposed this in user-facing functions. You could imagine # generalizing to allow user to request we send #N/A. # More about #N/A: # https://support.google.com/docs/answer/3093359?hl=en # However, again, for now, we leave NA as NA and let jsonlite do its usual # thing, which is to encode as JSON `null`. empty_cell <- function(.na = NULL) { if (is.null(.na)) { new_CellData(userEnteredValue = NA) } else { CellData(!!!.na) } } # Note that this always returns a **list** of instances of # googlesheets4_schema_CellData # of the same length as x. cell_data <- function(x, val_type, .na = NULL) { force(val_type) f <- function(y) { new_CellData(userEnteredValue = list2(!!val_type := y)) } out <- map(x, f) out[is.na(x)] <- list(empty_cell(.na = .na)) out } #' @export as_CellData.NULL <- function(x, .na = NULL) { list(empty_cell(.na)) } #' @export as_CellData.googlesheets4_schema_CellData <- function(x, .na = NULL) { x } #' @export as_CellData.logical <- function(x, .na = NULL) { cell_data(x, val_type = "boolValue", .na = .na) } #' @export as_CellData.character <- function(x, .na = NULL) { cell_data(x, val_type = "stringValue", .na = .na) } #' @export as_CellData.factor <- function(x, .na = NULL) { as_CellData(as.character(x), .na = .na) } #' @export as_CellData.numeric <- function(x, .na = NULL) { cell_data(x, val_type = "numberValue", .na = .na) } #' @export as_CellData.list <- function(x, .na = NULL) { out <- map(x, as_CellData, .na = .na) # awkwardness possibly solved by using vctrs to create an S3 class for # CellData ... but not pursuing at this time needs_flatten <- !map_lgl(x, is_CellData) out[needs_flatten] <- purrr::flatten(out[needs_flatten]) out } #' @export as_CellData.googlesheets4_formula <- function(x, .na = NULL) { cell_data(vec_data(x), val_type = "formulaValue", .na = .na) } add_format <- function(x, fmt) { x[["userEnteredFormat"]] <- list(numberFormat = list2(!!!fmt)) x } #' @export as_CellData.Date <- function(x, .na = NULL) { # 25569 = DATEVALUE("1970-01-01), i.e. Unix epoch as a serial date, when the # date origin is December 30th 1899 x <- unclass(x) + 25569 x <- cell_data(x, val_type = "numberValue", .na = .na) map(x, add_format, fmt = list(type = "DATE", pattern = "yyyy-mm-dd")) } #' @export as_CellData.POSIXct <- function(x, .na = NULL) { # 86400 = 60 * 60 * 24 = number of seconds in a day x <- (unclass(x) / 86400) + 25569 x <- cell_data(x, val_type = "numberValue", .na = .na) map( x, add_format, # I decided that going with R's default format was more important than # a militant stance re: ISO 8601 # the space (vs. a 'T') between date and time is "blessed" in RFC 3339 # https://tools.ietf.org/html/rfc3339#section-5.6 fmt = list(type = "DATE_TIME", pattern = "yyyy-mm-dd hh:mm:ss") ) } # Currently (overly) focused on userEnteredValue, because I am thinking about # writing. But with a reading focus, one would want to see effectiveValue. format.googlesheets4_schema_CellData <- function(x, ...) { # TODO: convey something about userEnteredFormat? user_entered_value <- pluck(x, "userEnteredValue") if (is.null(user_entered_value) || is.na(user_entered_value)) { return("--no userEnteredValue --") } nm <- pluck(user_entered_value, names) fval <- format(user_entered_value) as.character(glue("{nm}: {fval}")) } print.googlesheets4_schema_CellData <- function(x, ...) { header <- as.character(glue("")) cat(c(header, format(x)), sep = "\n") invisible(x) } googlesheets4/R/sheet_copy.R0000644000176200001440000001375014074074641015531 0ustar liggesusers#' Copy a (work)sheet #' #' Copies a (work)sheet, within its current (spread)Sheet or to another Sheet. #' #' @eval param_ss(pname = "from_ss") #' @eval param_sheet( #' pname = "from_sheet", #' action = "copy", #' "Defaults to the first visible sheet." #' ) #' @param to_ss The Sheet to copy *to*. Accepts all the same types of input as #' `from_ss`, which is also what this defaults to, if unspecified. #' @param to_sheet Optional. Name of the new sheet, as a string. If you don't #' specify this, Google generates a name, along the lines of "Copy of blah". #' Note that sheet names must be unique within a Sheet, so if the automatic #' name would violate this, Google also de-duplicates it for you, meaning you #' could conceivably end up with "Copy of blah 2". If you have better ideas #' about sheet names, specify `to_sheet`. #' @eval param_before_after("sheet") #' #' @return The receiving Sheet, `to_ ss`, as an instance of [`sheets_id`]. #' @export #' @family worksheet functions #' @seealso #' If the copy happens within one Sheet, makes a `DuplicateSheetRequest`: #' * #' #' If the copy is from one Sheet to another, wraps the #' `spreadsheets.sheets/copyTo` endpoint: #' * #' #' and possibly makes a subsequent `UpdateSheetPropertiesRequest`: #' * #' #' @examples #' if (gs4_has_token()) { #' ss_aaa <- gs4_create( #' "sheet-copy-demo-aaa", #' sheets = list(mtcars = head(mtcars), chickwts = head(chickwts)) #' ) #' #' # copy 'mtcars' sheet within existing Sheet, accept autogenerated name #' ss_aaa %>% #' sheet_copy() #' #' # copy 'mtcars' sheet within existing Sheet #' # specify new sheet's name and location #' ss_aaa %>% #' sheet_copy(to_sheet = "mtcars-the-sequel", .after = 1) #' #' # make a second Sheet #' ss_bbb <- gs4_create("sheet-copy-demo-bbb") #' #' # copy 'chickwts' sheet from first Sheet to second #' # accept auto-generated name and default location #' ss_aaa %>% #' sheet_copy("chickwts", to_ss = ss_bbb) #' #' # copy 'chickwts' sheet from first Sheet to second, #' # WITH a specific name and into a specific location #' ss_aaa %>% #' sheet_copy( #' "chickwts", #' to_ss = ss_bbb, to_sheet = "chicks-two", .before = 1 #' ) #' #' # clean up #' gs4_find("sheet-copy-demo") %>% #' googledrive::drive_trash() #' } sheet_copy <- function(from_ss, from_sheet = NULL, to_ss = from_ss, to_sheet = NULL, .before = NULL, .after = NULL) { from_ssid <- as_sheets_id(from_ss) to_ssid <- as_sheets_id(to_ss) maybe_sheet(from_sheet) if (identical(from_ssid, to_ssid)) { sheet_copy_internal( ssid = from_ssid, from_sheet = from_sheet, to_sheet = to_sheet, .before = .before, .after = .after ) } else { sheet_copy_external( from_ssid = from_ssid, from_sheet = from_sheet, to_ssid = to_ssid, to_sheet = to_sheet, .before = .before, .after = .after ) } } sheet_copy_internal <- function(ssid, from_sheet = NULL, to_sheet = NULL, .before = NULL, .after = NULL) { maybe_string(to_sheet) x <- gs4_get(ssid) s <- lookup_sheet(from_sheet, sheets_df = x$sheets) gs4_bullets(c(v = "Duplicating sheet {.w_sheet {s$name}} in {.s_sheet {x$name}}.")) index <- resolve_index(x$sheets, .before = .before, .after = .after) dup_request <- new( "DuplicateSheetRequest", sourceSheetId = s$id, insertSheetIndex = index, newSheetName = to_sheet ) req <- request_generate( "sheets.spreadsheets.batchUpdate", params = list( spreadsheetId = ssid, requests = list(duplicateSheet = dup_request) ) ) resp_raw <- request_make(req) resp <- gargle::response_process(resp_raw) to_name <- pluck(resp, "replies", 1, "duplicateSheet", "properties", "title") gs4_bullets(c(v = "Copied as {.w_sheet {to_name}}.")) invisible(ssid) } sheet_copy_external <- function(from_ssid, from_sheet = NULL, to_ssid, to_sheet = NULL, .before = NULL, .after = NULL) { from_x <- gs4_get(from_ssid) to_x <- gs4_get(to_ssid) maybe_string(to_sheet, "sheet_copy") from_s <- lookup_sheet(from_sheet, sheets_df = from_x$sheets) gs4_bullets(c( v = "Copying sheet {.w_sheet {from_s$name}} from \\ {.s_sheet {from_x$name}} to {.s_sheet {to_x$name}}.")) req <- request_generate( "sheets.spreadsheets.sheets.copyTo", params = list( spreadsheetId = from_ssid, sheetId = from_s$id, destinationSpreadsheetId = as.character(to_ssid) ) ) resp_raw <- request_make(req) to_s <- gargle::response_process(resp_raw) # early exit if no need to relocate and/or rename copied sheet index <- resolve_index(to_x$sheets, .before, .after) if (is.null(index) && is.null(to_sheet)) { gs4_bullets(c(v = "Copied as {.w_sheet {to_s$title}}.")) return(invisible(to_ssid)) } sp <- new( "SheetProperties", sheetId = to_s$sheetId, title = to_sheet, index = index ) update_req <- new( "UpdateSheetPropertiesRequest", properties = sp, fields = gargle::field_mask(sp) ) req <- request_generate( "sheets.spreadsheets.batchUpdate", params = list( spreadsheetId = to_ssid, requests = list(updateSheetProperties = update_req) ) ) resp_raw <- request_make(req) gargle::response_process(resp_raw) gs4_bullets(c(v = "Copied as {.w_sheet {to_sheet}}.")) invisible(to_ssid) } googlesheets4/R/range_add_validation.R0000644000176200001440000001115114074074641017476 0ustar liggesusers#' Add a data validation rule to a cell range #' #' @description #' *Note: not yet exported, still very alpha. Usage still requires using #' low-level helpers.* #' #' `range_add_validation()` adds a data validation rule to a range of cells. #' #' @eval param_ss() #' @eval param_sheet() #' @param range Cells to apply data validation to. This `range` argument has #' important similarities and differences to `range` elsewhere (e.g. #' [range_read()]): #' * Similarities: Can be a cell range, using A1 notation ("A1:D3") or using #' the helpers in [`cell-specification`]. Can combine sheet name and cell #' range ("Sheet1!A5:A") or refer to a sheet by name (`range = "Sheet1"`, #' although `sheet = "Sheet1"` is preferred for clarity). #' * Difference: Can NOT be a named range. #' @param rule An instance of `googlesheets4_schema_DataValidationRule`, which #' implements the #' [DataValidationRule](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/cells#datavalidationrule) #' schema. #' #' @template ss-return #' @seealso Makes a `SetDataValidationRequest`: #' * #' #' @keywords internal #' @noRd #' #' @examples #' if (gs4_has_token()) { #' # create a data frame to use as initial data #' df <- data.frame( #' id = 1:3, #' "Hungry?" = NA, #' ice_cream = NA, #' check.names = FALSE #' ) #' #' # create Sheet #' ss <- gs4_create("range-add-validation-demo", sheets = list(df)) #' #' # create a column that presents as a basic TRUE/FALSE checkbox #' rule_checkbox <- googlesheets4:::new( #' "DataValidationRule", #' condition = googlesheets4:::new_BooleanCondition(type = "BOOLEAN"), #' inputMessage = "Please let us know if you are hungry.", #' strict = TRUE, #' showCustomUi = TRUE #' ) #' googlesheets4:::range_add_validation( #' ss, range = "Sheet1!B2:B", rule = rule_checkbox #' ) #' #' # create a column that presents as a dropdown list #' rule_dropdown_list <- googlesheets4:::new( #' "DataValidationRule", #' condition = googlesheets4:::new_BooleanCondition( #' type = "ONE_OF_LIST", values = c("vanilla", "chocolate", "strawberry") #' ), #' inputMessage = "Which ice cream flavor do you want?", #' strict = TRUE, #' showCustomUi = TRUE #' ) #' googlesheets4:::range_add_validation( #' ss, range = "Sheet1!C2:C", rule = rule_dropdown_list #' ) #' #' read_sheet(ss) #' #' # clean up #' gs4_find("range-add-validation-demo") %>% #' googledrive::drive_trash() #' } range_add_validation <- function(ss, sheet = NULL, range = NULL, rule) { ssid <- as_sheets_id(ss) maybe_sheet(sheet) check_range(range) if (!is.null(rule)) { stopifnot(inherits(rule, "googlesheets4_schema_DataValidationRule")) } x <- gs4_get(ssid) gs4_bullets(c(v = "Editing {.s_sheet {x$name}}.")) # determine (work)sheet ------------------------------------------------------ range_spec <- as_range_spec( range, sheet = sheet, sheets_df = x$sheets, nr_df = x$named_ranges ) range_spec$sheet_name <- range_spec$sheet_name %||% first_visible_name(x$sheets) s <- lookup_sheet(range_spec$sheet_name, sheets_df = x$sheets) gs4_bullets(c(v = "Editing sheet {.w_sheet {range_spec$sheet_name}}.")) # form batch update request -------------------------------------------------- sdv_req <- list(setDataValidation = new( "SetDataValidationRequest", range = as_GridRange(range_spec), rule = rule )) # do it ---------------------------------------------------------------------- req <- request_generate( "sheets.spreadsheets.batchUpdate", params = list( spreadsheetId = ssid, requests = list(sdv_req) ) ) resp_raw <- request_make(req) gargle::response_process(resp_raw) invisible(ssid) } # helpers ---- new_BooleanCondition <- function(type = "NOT_BLANK", values = NULL) { out <- new("BooleanCondition", type = type) # TODO: build enum checking into our schema-based construction schema <- attr(out, "schema") enum <- schema$enum[[which(schema$property == "type")]] stopifnot(type %in% enum$enum) if (length(values) < 1) { return(out) } needs_relative_date <- c( "DATE_BEFORE", "DATE_AFTER", "DATE_ON_OR_BEFORE", "DATE_ON_OR_AFTER" ) if (type %in% needs_relative_date) { gs4_abort(" {.field relativeDate} not yet supported as a {.code conditionValue}.") } patch(out, values = map(values, ~ list(userEnteredValue = as.character(.x)))) } googlesheets4/R/range_spec.R0000644000176200001440000001421414074074641015471 0ustar liggesusers## range_spec is an "internal-use only" S3 class ---- new_range_spec <- function(...) { l <- list2(...) structure( list( sheet_name = l$sheet_name %||% NULL, named_range = l$named_range %||% NULL, cell_range = l$cell_range %||% NULL, cell_limits = l$cell_limits %||% NULL, shim = FALSE, sheets_df = l$sheets_df %||% NULL, nr_df = l$nr_df %||% NULL ), # useful when debugging range specification, but otherwise this is TMI # .input = l$.input, class = "range_spec" ) } as_range_spec <- function(x, ...) { UseMethod("as_range_spec") } #' @export as_range_spec.default <- function(x, ...) { gs4_abort(c( "Can't make a range suitable for the Sheets API from the supplied \\ {.arg range}.", x = "{.arg range} has class {.cls {class(x)}}.", i = "{.arg range} must be {.code NULL}, a string, or \\ a {.cls cell_limits} object." )) } ## as_range_spec.character ---- # anticipated inputs to the character method for x (= range) # **** means "doesn't matter, never consulted" # # sheet range skip # -------------------------------------- # **** Sheet1!A1:B2 **** # **** Named_range **** # **** Sheet1 i weird, but I guess we roll with it (re-dispatch) # A1:B2 **** # Sheet1 A1:B2 **** # 3 A1:B2 **** #' @export as_range_spec.character <- function(x, ..., sheet = NULL, skip = 0, sheets_df = NULL, nr_df = NULL) { check_length_one(x) out <- new_range_spec( sheets_df = sheets_df, nr_df = nr_df, .input = list( sheet = sheet, range = x, skip = skip ) ) m <- rematch2::re_match(x, compound_rx) # range looks like: Sheet1!A1:B2 if (notNA(m[[".match"]])) { out$sheet_name <- lookup_sheet_name(m$sheet, sheets_df) out$cell_range <- m$cell_range out$shim <- TRUE return(out) } # check if range matches a named range m <- match(x, nr_df$name) if (notNA(m)) { out$named_range <- x return(out) } # check if range matches a sheet name # API docs: "When a named range conflicts with a sheet's name, the named range # is preferred." m <- match(x, sheets_df$name) if (notNA(m)) { # Re-dispatch as if provided as `sheet`. Which it should have been. return(as_range_spec(NULL, sheet = x, skip = skip, sheets_df = sheets_df)) } # range must be in A1 notation m <- grepl(A1_rx, strsplit(x, split = ":")[[1]]) if (!all(m)) { gs4_abort(c( "{.arg range} doesn't appear to be a range in A1 notation, a named \\ range, or a sheet name:", x = "{.range {x}}" )) } out$cell_range <- x if (!is.null(sheet)) { out$sheet_name <- lookup_sheet_name(sheet, sheets_df) } out$shim <- TRUE out } ## as_range_spec.NULL ---- # anticipated inputs to the NULL method for x (= range) # # sheet skip # -------------------------------------- # 0 This is what "nothing" looks like. Send nothing. # Sheet1 / 2 0 Send sheet name. # >0 Express skip request in cell_limits object and re-dispatch. # Sheet1 / 2 >0 #' @export as_range_spec.NULL <- function(x, ..., sheet = NULL, skip = 0, sheets_df = NULL) { out <- new_range_spec( sheets_df = sheets_df, .input = list(sheet = sheet, skip = skip) ) if (skip < 1) { if (!is.null(sheet)) { out$sheet_name <- lookup_sheet_name(sheet, sheets_df) } return(out) } as_range_spec( cell_rows(c(skip + 1, NA)), sheet = sheet, sheets_df = sheets_df, shim = FALSE ) } ## as_range_spec.cell_limits ---- # anticipated inputs to the cell_limits method for x (= range) # # sheet range # -------------------------------------- # cell_limits Send A1 representation of cell_limits. Let the API # figure out the sheet. API docs imply it will be the # "first visible sheet". # Sheet1 / 2 cell_limits Resolve sheet name, make A1 range, send combined # result. #' @export as_range_spec.cell_limits <- function(x, ..., shim = TRUE, sheet = NULL, sheets_df = NULL) { out <- new_range_spec( sheets_df = sheets_df, .input = list(sheet = sheet, range = x, shim = shim) ) out$cell_limits <- x if (!is.null(sheet)) { out$sheet_name <- lookup_sheet_name(sheet, sheets_df) } out$shim <- shim out } #' @export format.range_spec <- function(x, ...) { is_df <- names(x) %in% c("sheets_df", "nr_df") x[is_df & !map_lgl(x, is.null)] <- "" glue("{fr(names(x))}: {x}") } #' @export print.range_spec <- function(x, ...) { cat(format(x), sep = "\n") invisible(x) } as_A1_range <- function(x) { stopifnot(inherits(x, "range_spec")) if (!is.null(x$named_range)) { return(x$named_range) } if (!is.null(x$cell_limits)) { x$cell_range <- as_sheets_range(x$cell_limits) } qualified_A1(x$sheet_name, x$cell_range) } # has been useful during development, at times # sheets_A1_range <- function(ss, # sheet = NULL, # range = NULL, # skip = 0) { # ssid <- as_sheets_id(ss) # maybe_sheet(sheet) # check_range(range) # check_non_negative_integer(skip) # # # retrieve spreadsheet metadata ---------------------------------------------- # x <- gs4_get(ssid) # gs4_bullets(c(i = "Spreadsheet name: {.s_sheet {x$name}}")) # # # range specification -------------------------------------------------------- # range_spec <- as_range_spec( # range, sheet = sheet, skip = skip, # sheets_df = x$sheets, nr_df = x$named_ranges # ) # A1_range <- as_A1_range(range_spec) # gs4_bullets(c(i = "A1 range {.range {A1_range}}")) # # range_spec # } googlesheets4/R/range_write.R0000644000176200001440000002037714074074641015700 0ustar liggesusers#' (Over)write new data into a range #' #' @description #' #' Writes a data frame into a range of cells. Main differences from #' [sheet_write()] (a.k.a. [write_sheet()]): #' * Narrower scope. `range_write()` literally targets some cells, not a whole #' (work)sheet. #' * The edited rectangle is not explicitly styled as a table. Nothing special #' is done re: formatting a header row or freezing rows. #' * Column names can be suppressed. This means that, although `data` must #' be a data frame (at least for now), `range_write()` can actually be used #' to write arbitrary data. #' * The target (spread)Sheet and (work)sheet must already exist. There is no #' ability to create a Sheet or add a worksheet. #' * The target sheet dimensions are not "trimmed" to shrink-wrap the `data`. #' However, the sheet might gain rows and/or columns, in order to write #' `data` to the user-specified `range`. #' #' If you just want to add rows to an existing table, the function you probably #' want is [sheet_append()]. #' #' @section Range specification: #' The `range` argument of `range_write()` is special, because the Sheets API #' can implement it in 2 different ways: #' * If `range` represents exactly 1 cell, like "B3", it is taken as the *start* #' (or upper left corner) of the targeted cell rectangle. The edited cells are #' determined implicitly by the extent of the `data` we are writing. This #' frees you from doing fiddly range computations based on the dimensions of #' the `data`. #' * If `range` describes a rectangle with multiple cells, it is interpreted #' as the *actual* rectangle to edit. It is possible to describe a rectangle #' that is unbounded on the right (e.g. "B2:4"), on the bottom (e.g. "A4:C"), #' or on both the right and the bottom (e.g. #' `cell_limits(c(2, 3), c(NA, NA))`. Note that **all cells** inside the #' rectangle receive updated data and format. Important implication: if the #' `data` object isn't big enough to fill the target rectangle, the cells that #' don't receive new data are effectively cleared, i.e. the existing value #' and format are deleted. #' #' @eval param_ss() #' @param data A data frame. #' @eval param_sheet( #' action = "write into", #' "Ignored if the sheet is specified via `range`. If neither argument", #' "specifies the sheet, defaults to the first visible sheet." #' ) #' @param range Where to write. This `range` argument has important similarities #' and differences to `range` elsewhere (e.g. [range_read()]): #' * Similarities: Can be a cell range, using A1 notation ("A1:D3") or using #' the helpers in [`cell-specification`]. Can combine sheet name and cell #' range ("Sheet1!A5:A") or refer to a sheet by name (`range = "Sheet1"`, #' although `sheet = "Sheet1"` is preferred for clarity). #' * Difference: Can NOT be a named range. #' * Difference: `range` can be interpreted as the *start* of the target #' rectangle (the upper left corner) or, more literally, as the actual #' target rectangle. See the "Range specification" section for details. #' @param col_names Logical, indicates whether to send the column names of #' `data`. #' @template reformat #' #' @template ss-return #' @export #' @family write functions #' @seealso #' If sheet size needs to change, makes an `UpdateSheetPropertiesRequest`: #' * #' #' The main data write is done via an `UpdateCellsRequest`: #' * #' #' @examples #' if (gs4_has_token()) { #' # create a Sheet with some initial, empty (work)sheets #' (ss <- gs4_create("range-write-demo", sheets = c("alpha", "beta"))) #' #' df <- data.frame( #' x = 1:3, #' y = letters[1:3] #' ) #' #' # write df somewhere other than the "upper left corner" #' range_write(ss, data = df, range = "D6") #' #' # view your magnificent creation in the browser #' gs4_browse(ss) #' #' # send data of disparate types to a 1-row rectangle #' dat <- tibble::tibble( #' string = "string", #' logical = TRUE, #' datetime = Sys.time() #' ) #' range_write(ss, data = dat, sheet = "beta", col_names = FALSE) #' #' # send data of disparate types to a 1-column rectangle #' dat <- tibble::tibble( #' x = list(Sys.time(), FALSE, "string") #' ) #' range_write(ss, data = dat, range = "beta!C5", col_names = FALSE) #' #' # clean up #' gs4_find("range-write-demo") %>% #' googledrive::drive_trash() #' } range_write <- function(ss, data, sheet = NULL, range = NULL, col_names = TRUE, # not sure about this default reformat = TRUE) { ssid <- as_sheets_id(ss) check_data_frame(data) maybe_sheet(sheet) check_range(range) check_bool(col_names) check_bool(reformat) x <- gs4_get(ssid) gs4_bullets(c(v = "Editing {.s_sheet {x$name}}.")) # determine (work)sheet ------------------------------------------------------ range_spec <- as_range_spec( range, sheet = sheet, sheets_df = x$sheets, nr_df = x$named_ranges ) range_spec$sheet_name <- range_spec$sheet_name %||% first_visible_name(x$sheets) gs4_bullets(c(v = "Writing to sheet {.w_sheet {range_spec$sheet_name}}.")) # initialize the batch update requests; store details on target sheet s ------ requests <- list() s <- lookup_sheet(range_spec$sheet_name , sheets_df = x$sheets) # package the write location as `start` or `range` --------------------------- loc <- prepare_loc(range_spec) # figure out if we need to resize the sheet ---------------------------------- dims_needed <- prepare_dims(loc, data, col_names) resize_req <- prepare_resize_request( s, nrow_needed = dims_needed$nrow, ncol_needed = dims_needed$ncol, exact = FALSE ) if (!is.null(resize_req)) { new_dims <- pluck( resize_req, "updateSheetProperties", "properties", "gridProperties" ) gs4_bullets(c( v = "Changing dims: ({s$grid_rows} x {s$grid_columns}) --> \\ ({new_dims$rowCount %||% s$grid_rows} x \\ {new_dims$columnCount %||% s$grid_columns}).")) requests <- c(requests, list(resize_req)) } # pack the data, specify field mask ------------------------------------------ fields <- if (reformat) "userEnteredValue,userEnteredFormat" else "userEnteredValue" data_req <- new( "UpdateCellsRequest", rows = as_RowData(data, col_names = col_names), fields = fields, !!!loc ) requests <- c(requests, list(list(updateCells = data_req))) # do it ---------------------------------------------------------------------- req <- request_generate( "sheets.spreadsheets.batchUpdate", params = list( spreadsheetId = ssid, requests = requests ) ) resp_raw <- request_make(req) gargle::response_process(resp_raw) invisible(ssid) } prepare_loc <- function(x) { if (is.null(x$cell_limits)) { if (is.null(x$cell_range)) { return(list(start = as_GridCoordinate(x))) } x$cell_limits <- limits_from_range(x$cell_range) } if (more_than_one_cell(x$cell_limits)) { list(range = as_GridRange(x)) } else { list(start = as_GridCoordinate(x)) } } more_than_one_cell <- function(cl) { if (anyNA(cl$ul) || anyNA(cl$lr)) { return(TRUE) } nrows <- cl$lr[1] - cl$ul[1] + 1 ncols <- cl$lr[2] - cl$ul[2] + 1 nrows > 1 || ncols > 1 } prepare_dims <- function(write_loc, data, col_names) { # Here it is actually useful to us that the row and column indices inside # `write_loc` are zero-indexed. Recall that: # * `start` is an instance of GridCoordinate # * `range` is an instance of GridRange if (has_name(write_loc, "start")) { return(list( nrow = (write_loc$start$rowIndex %||% 0) + nrow(data) + col_names, ncol = (write_loc$start$columnIndex %||% 0) + ncol(data) )) } # we must be writing to a `range` # take explicit end indices literally # otherwise infer from start indices + size of `data` gr <- write_loc$range list( nrow = gr$endRowIndex %||% ((gr$startRowIndex %||% 0) + nrow(data) + col_names), ncol = gr$endColumnIndex %||% ((gr$startColumnIndex %||% 0) + ncol(data)) ) } googlesheets4/R/schema_Spreadsheet.R0000644000176200001440000000657214074074641017162 0ustar liggesusers# input: a named list, usually an instance of googlesheets4_schema_Spreadsheet # output: instance of googlesheets4_spreadsheet, which is actually useful new_googlesheets4_spreadsheet <- function(x = list()) { ours_theirs <- list( spreadsheet_id = "spreadsheetId", spreadsheet_url = "spreadsheetUrl", name = list("properties", "title"), locale = list("properties", "locale"), time_zone = list("properties", "timeZone") ) out <- map(ours_theirs, ~ pluck(x, !!!.x, .default = "")) if (!is.null(x$sheets)) { sheets <- map(x$sheets, ~ new("Sheet", !!!.x)) sheet_properties <- map(sheets, as_tibble) out$sheets <- do.call(rbind, sheet_properties) protected_ranges <- map(sheets, "protectedRanges") protected_ranges <- purrr::flatten(protected_ranges) protected_ranges <- map(protected_ranges, ~ new("ProtectedRange", !!!.x)) protected_ranges <- map(protected_ranges, as_tibble) out$protected_ranges <- do.call(rbind, protected_ranges) } if (!is.null(x$namedRanges)) { named_ranges <- map(x$namedRanges, ~ new("NamedRange", !!!.x)) named_ranges <- map(named_ranges, as_tibble) named_ranges <- do.call(rbind, named_ranges) # if there is only 1 sheet, sheetId might not be sent! # https://github.com/tidyverse/googlesheets4/issues/29 needs_sheet_id <- is.na(named_ranges$sheet_id) if (any(needs_sheet_id)) { # if sheetId is missing, I assume it's the "first" (visible?) sheet named_ranges$sheet_id[needs_sheet_id] <- first_visible_id(out$sheets) } named_ranges$sheet_name <- vlookup( named_ranges$sheet_id, data = out$sheets, key = "id", value = "name" ) named_ranges$cell_range <- pmap_chr(named_ranges, make_cell_range) named_ranges$A1_range <- qualified_A1( named_ranges$sheet_name, named_ranges$cell_range ) out$named_ranges <- named_ranges } structure(out, class = c("googlesheets4_spreadsheet", "list")) } #' @export format.googlesheets4_spreadsheet <- function(x, ...) { meta <- tibble::tribble( ~col1, ~col2, "Spreadsheet name", x$name, "ID", as.character(x$spreadsheet_id), "Locale", x$locale, "Time zone", x$time_zone, "# of sheets", if (rlang::has_name(x, "sheets")) { as.character(nrow(x$sheets)) } else "" ) if (!is.null(x$named_ranges)) { meta <- tibble::add_row( meta, col1 = "# of named ranges", col2 = as.character(nrow(x$named_ranges)) ) } if (!is.null(x$protected_ranges)) { meta <- tibble::add_row( meta, col1 = "# of protected ranges", col2 = as.character(nrow(x$protected_ranges)) ) } meta <- glue_data(meta, "{fr(col1)}: {col2}") if (!is.null(x$sheets)) { col1 <- fr(c("(Sheet name)", x$sheets$name)) col2 <- c( "(Nominal extent in rows x columns)", glue_data(x$sheets, "{grid_rows} x {grid_columns}") ) meta <- c( meta, "", glue_data(list(col1 = col1, col2 = col2), "{col1}: {col2}") ) } if (!is.null(x$named_ranges)) { col1 <- fr(c("(Named range)", x$named_ranges$name)) col2 <- fl(c("(A1 range)", x$named_ranges$A1_range)) meta <- c( meta, "", glue_data(list(col1 = col1, col2 = col2), "{col1}: {col2}") ) } meta } #' @export print.googlesheets4_spreadsheet <- function(x, ...) { cat(format(x), sep = "\n") invisible(x) } googlesheets4/R/make_column.R0000644000176200001440000001101014074074641015644 0ustar liggesusersmake_column <- function(df, ctype, ..., nr, guess_max = min(1000, nr)) { ## must resolve COL_GUESS here (vs when parsing) because need to know ctype ## here, when making the column ctype <- resolve_col_type(df$cell[df$row <= guess_max], ctype) parsed <- parse(df$cell, ctype, ...) if (is.null(parsed)) { return() } fodder <- rep_len(NA, length.out = nr) column <- switch( ctype, ## NAs must be numeric in order to initialize datetimes with a timezone CELL_DATE = as_Date(as.numeric(fodder)), ## TODO: time of day not really implemented yet CELL_TIME = as_POSIXct(as.numeric(fodder)), CELL_DATETIME = as_POSIXct(as.numeric(fodder)), COL_LIST = vector(mode = "list", length = nr), as.vector(fodder, mode = typeof(parsed)) ) if (ctype == "CELL_TEXT") { dots <- list2(...) column <- enforce_na(column, na = dots$na) } column[df$row] <- parsed column } resolve_col_type <- function(cell, ctype = "COL_GUESS") { if (ctype != "COL_GUESS") { return(ctype) } cell %>% ctype() %>% effective_cell_type() %>% consensus_col_type() } parse <- function(x, ctype, ...) { stopifnot(is_string(ctype)) parse_fun <- switch( ctype, COL_SKIP = as_skip, CELL_LOGICAL = as_logical, CELL_INTEGER = as_integer, CELL_NUMERIC = as_double, CELL_DATE = as_date, # TODO: CELL_TIME not really implemented yet CELL_TIME = as_datetime, CELL_DATETIME = as_datetime, CELL_TEXT = as_character, COL_CELL = as_cell, COL_LIST = as_list, ## TODO: factor, duration gs4_abort("Not a recognized column type: {.field {ctype}}") ) if (inherits(x, "SHEETS_CELL")) { x <- list(x) } parse_fun(x, ...) } as_skip <- function(cell, ...) NULL as_cell <- function(cell, ...) cell as_list <- function(cell, ...) { ctypes <- cell %>% ctype() %>% effective_cell_type() %>% blank_to_logical() map2(cell, ctypes, parse, ...) } ## prepare to coerce to logical, integer, double cell_content <- function(cell, na = "", trim_ws = TRUE) { switch( ctype(cell), CELL_BLANK = NA, CELL_LOGICAL = pluck(cell, "effectiveValue", "boolValue"), CELL_NUMERIC = pluck(cell, "effectiveValue", "numberValue"), CELL_DATE = NA_real_, CELL_TIME = NA_real_, CELL_DATETIME = NA_real_, CELL_TEXT = cell %>% pluck("effectiveValue", "stringValue") %>% groom_text(na = na, trim_ws = trim_ws) ) } as_logical <- function(cell, na = "", trim_ws = TRUE) { cell %>% map(cell_content, na = na, trim_ws = trim_ws) %>% map_lgl(as.logical) } as_integer <- function(cell, na = "", trim_ws = TRUE) { cell %>% map(cell_content, na = na, trim_ws = trim_ws) %>% map_int(as.integer) } as_double <- function(cell, na = "", trim_ws = TRUE) { cell %>% map(cell_content, na = na, trim_ws = trim_ws) %>% map_dbl(as.double) } ## prepare to coerce to date, time, datetime cell_content_datetime <- function(cell, na = "", trim_ws = TRUE) { switch( ctype(cell), CELL_BLANK = NA, CELL_LOGICAL = NA, CELL_NUMERIC = NA, CELL_DATE = pluck(cell, "effectiveValue", "numberValue"), CELL_TIME = pluck(cell, "effectiveValue", "numberValue"), CELL_DATETIME = pluck(cell, "effectiveValue", "numberValue"), CELL_TEXT = NA ) } as_datetime <- function(cell, na = "", trim_ws = TRUE) { cell %>% map(cell_content_datetime, na = na, trim_ws = trim_ws) %>% map_dbl(as.double) %>% map_dbl(`*`, 24 * 60 * 60) %>% as_POSIXct() } as_date <- function(cell, na = "", trim_ws = TRUE) { cell %>% map(cell_content_datetime, na = na, trim_ws = trim_ws) %>% map_dbl(as.double) %>% as_Date() } ## TODO: not wired up yet (body is same as as_datetime) # as_time <- function(cell, na = "", trim_ws = TRUE) { # cell %>% # map(cell_content_datetime, na = na, trim_ws = trim_ws) %>% # map_dbl(as.double) %>% # `*`(24 * 60 * 60) %>% # as_POSIXct() # } ## prepare to coerce to character cell_content_chr <- function(cell, na = "", trim_ws = TRUE) { fv <- pluck(cell, "formattedValue", .default = NA_character_) groom_text(fv, na = na, trim_ws = trim_ws) } as_character <- function(cell, na = "", trim_ws = TRUE) { cell %>% map(cell_content_chr, na = na, trim_ws = trim_ws) %>% map_chr(as.character) } as_Date <- function(x = NA_real_, origin = "1899-12-30", tz = "UTC", ...) { as.Date(x, origin = origin, tz = tz, ...) } as_POSIXct <- function(x = NA_real_, origin = "1899-12-30", tz = "UTC", ...) { as.POSIXct(x, origin = origin, tz = tz, ...) } googlesheets4/R/gs4_get.R0000644000176200001440000000236514074074641014723 0ustar liggesusers#' Get Sheet metadata #' #' Retrieve spreadsheet-specific metadata, such as details on the individual #' (work)sheets or named ranges. #' * `gs4_get()` complements [googledrive::drive_get()], which #' returns metadata that exists for any file on Drive. #' #' @eval param_ss() #' #' @return A list with S3 class `googlesheets4_spreadsheet`, for printing #' purposes. #' @export #' @seealso Wraps the `spreadsheets.get` endpoint: #' * #' #' @examples #' if (gs4_has_token()) { #' gs4_get(gs4_example("mini-gap")) #' } gs4_get <- function(ss) { resp <- gs4_get_impl_(as_sheets_id(ss)) new_googlesheets4_spreadsheet(resp) } ## I want a separate worker so there is a version of this available that ## accepts `fields`, yet I don't want a user-facing function with `fields` arg gs4_get_impl_ <- function(ssid, fields = NULL) { fields <- fields %||% "spreadsheetId,properties,spreadsheetUrl,sheets.properties,sheets.protectedRanges,namedRanges" req <- request_generate( "sheets.spreadsheets.get", params = list( spreadsheetId = ssid, fields = fields ) ) raw_resp <- request_make(req) gargle::response_process(raw_resp) } googlesheets4/R/gs4_find.R0000644000176200001440000000253514074074641015063 0ustar liggesusers#' Find Google Sheets #' #' Finds your Google Sheets. This is a very thin wrapper around #' [googledrive::drive_find()], that specifies you want to list Drive files #' where `type = "spreadsheet"`. Therefore, note that this will require auth for #' googledrive! See the article [Using googlesheets4 with #' googledrive](https://googlesheets4.tidyverse.org/articles/articles/drive-and-sheets.html) #' if you want to coordinate auth between googlesheets4 and googledrive. This #' function will emit an informational message if you are currently logged in #' with both googlesheets4 and googledrive, but as different users. #' #' @param ... Arguments (other than `type`, which is hard-wired as `type = #' "spreadsheet"`) that are passed along to [googledrive::drive_find()]. #' #' @inherit googledrive::drive_find return #' @export #' #' @examples #' if (gs4_has_token()) { #' # see all your Sheets #' gs4_find() #' #' # see 5 Sheets, prioritized by creation time #' x <- gs4_find(order_by = "createdTime desc", n_max = 5) #' x #' #' # hoist the creation date, using other packages in the tidyverse #' # x %>% #' # tidyr::hoist(drive_resource, created_on = "createdTime") %>% #' # dplyr::mutate(created_on = as.Date(created_on)) #' } gs4_find <- function(...) { check_gs4_email_is_drive_email() googledrive::drive_find(..., type = "spreadsheet") } googlesheets4/R/googlesheets4-package.R0000644000176200001440000000545614074325150017532 0ustar liggesusers#' @keywords internal #' @import rlang "_PACKAGE" ## usethis namespace: start #' @importFrom gargle bulletize #' @importFrom gargle gargle_map_cli #' @importFrom glue glue #' @importFrom glue glue_collapse #' @importFrom glue glue_data #' @importFrom googledrive as_id #' @importFrom purrr %||% #' @importFrom purrr compact #' @importFrom purrr discard #' @importFrom purrr imap #' @importFrom purrr keep #' @importFrom purrr map #' @importFrom purrr map_chr #' @importFrom purrr map_dbl #' @importFrom purrr map_int #' @importFrom purrr map_lgl #' @importFrom purrr map2 #' @importFrom purrr modify_if #' @importFrom purrr pluck #' @importFrom purrr pmap #' @importFrom purrr pmap_chr #' @importFrom purrr transpose #' @importFrom purrr walk #' @importFrom tibble as_tibble ## usethis namespace: end NULL #' Internal vctrs methods #' #' @import vctrs #' @keywords internal #' @name googlesheets4-vctrs NULL #' googlesheets4 configuration #' #' @description #' Some aspects of googlesheets4 behaviour can be controlled via an option. #' #' @section Messages: #' #' The `googlesheets4_quiet` option can be used to suppress messages from #' googlesheets4. By default, googlesheets4 always messages, i.e. it is *not* #' quiet. #' #' Set `googlesheets4_quiet` to `TRUE` to suppress messages, by one of these #' means, in order of decreasing scope: #' * Put `options(googlesheets4_quiet = TRUE)` in a start-up file, such as #' `.Rprofile`, or in your R script #' * Use `local_gs4_quiet()` to silence googlesheets4 in a specific scope #' * Use `with_gs4_quiet()` to run a small bit of code silently #' #' `local_gs4_quiet()` and `with_gs4_quiet()` follow the conventions of the #' the withr package (). #' #' @section Auth: #' #' Read about googlesheets4's main auth function, [gs4_auth()]. It is powered #' by the gargle package, which consults several options: #' * Default Google user or, more precisely, `email`: see #' [gargle::gargle_oauth_email()] #' * Whether or where to cache OAuth tokens: see #' [gargle::gargle_oauth_cache()] #' * Whether to prefer "out-of-band" auth: see #' [gargle::gargle_oob_default()] #' * Application Default Credentials: see [gargle::credentials_app_default()] #' #' @name googlesheets4-configuration NULL # used for building functions that construct Sheet names in tests ---- nm_fun <- function(context, user_run = TRUE) { user_run <- if (isTRUE(user_run)) nm_user_run() else NULL y <- purrr::compact(list(context, user_run)) function(x = character()) as.character(glue_collapse(c(x, y), sep = "-")) } nm_user_run <- function() { if(as.logical(Sys.getenv("GITHUB_ACTIONS", unset = "false"))) { glue("gha-{Sys.getenv('GITHUB_WORKFLOW')}-{Sys.getenv('GITHUB_RUN_ID')}") } else { random_id <- ids::proquint(n = 1, n_words = 2) glue("{Sys.info()['user']}-{random_id}") } } googlesheets4/R/gs4_create.R0000644000176200001440000001071714074074641015407 0ustar liggesusers#' Create a new Sheet #' #' @description #' #' Creates an entirely new (spread)Sheet (or, in Excel-speak, workbook). #' Optionally, you can also provide names and/or data for the initial set of #' (work)sheets. Any initial data provided via `sheets` is styled as a table, #' as described in [sheet_write()]. #' #' @seealso #' Wraps the `spreadsheets.create` endpoint: #' * #' #' There is an article on writing Sheets: #' * #' #' @param name The name of the new spreadsheet. #' @param ... Optional spreadsheet properties that can be set through this API #' endpoint, such as locale and time zone. #' @param sheets Optional input for initializing (work)sheets. If unspecified, #' the Sheets API automatically creates an empty "Sheet1". You can provide a #' vector of sheet names, a data frame, or a (possibly named) list of data #' frames. See the examples. #' #' @template ss-return #' @export #' @family write functions #' #' @examples #' if (gs4_has_token()) { #' gs4_create("gs4-create-demo-1") #' #' gs4_create("gs4-create-demo-2", locale = "en_CA") #' #' gs4_create( #' "gs4-create-demo-3", #' locale = "fr_FR", #' timeZone = "Europe/Paris" #' ) #' #' gs4_create( #' "gs4-create-demo-4", #' sheets = c("alpha", "beta") #' ) #' #' my_data <- data.frame(x = 1) #' gs4_create( #' "gs4-create-demo-5", #' sheets = my_data #' ) #' #' gs4_create( #' "gs4-create-demo-6", #' sheets = list(chickwts = head(chickwts), mtcars = head(mtcars)) #' ) #' #' # clean up #' gs4_find("gs4-create-demo") %>% #' googledrive::drive_trash() #' } gs4_create <- function(name = gs4_random(), ..., sheets = NULL) { sheets <- enlist_sheets(enquo(sheets)) sheets_given <- !is.null(sheets) data_given <- sheets_given && !is.null(unlist(sheets$value)) # create the (spread)Sheet --------------------------------------------------- gs4_bullets(c(v = "Creating new Sheet: {.s_sheet {name}}.")) ss_body <- new( "Spreadsheet", properties = new("SpreadsheetProperties", title = name, ...) ) if (sheets_given) { ss_body <- ss_body %>% patch(sheets = map(sheets$name, as_Sheet)) } req <- request_generate( "sheets.spreadsheets.create", params = ss_body ) resp_raw <- request_make(req) resp_create <- gargle::response_process(resp_raw) ss <- new_googlesheets4_spreadsheet(resp_create) ssid <- as_sheets_id(ss) if (!data_given) { return(invisible(ssid)) } request_populate_sheets <- map2(ss$sheets$id, sheets$value, prepare_df) request_populate_sheets <- purrr::flatten(request_populate_sheets) req <- request_generate( "sheets.spreadsheets.batchUpdate", params = list( spreadsheetId = ssid, requests = request_populate_sheets, responseIncludeGridData = FALSE ) ) resp_raw <- request_make(req) gargle::response_process(resp_raw) invisible(ssid) } prepare_df <- function(sheet_id, df, skip = 0) { # if df is a 0-row data frame, we must send a 1-row data frame of NAs in order # to shrink wrap the data and freeze the top row # https://github.com/tidyverse/googlesheets4/issues/92 if (nrow(df) == 0) { df <- vec_init(df, n = 1) } # pack the data -------------------------------------------------------------- # `start` (or `range`) must be sent, even if `skip = 0` start <- new("GridCoordinate", sheetId = sheet_id) if (skip > 0) { start <- patch(start, rowIndex = skip) } request_values <- list(updateCells = new( "UpdateCellsRequest", start = start, rows = as_RowData(df), # an array of instances of RowData fields = "userEnteredValue,userEnteredFormat" )) # set sheet dimensions and freeze top row ------------------------------------- request_sheet_properties <- bureq_set_grid_properties( sheetId = sheet_id, nrow = nrow(df) + skip + 1, ncol = ncol(df), frozenRowCount = skip + 1 ) c( list(request_sheet_properties), list(request_values), list(bureq_header_row(sheetId = sheet_id, row = skip + 1)) ) } #' Generate a random Sheet name #' #' Generates a random name, suitable for a newly created Sheet, using #' [ids::adjective_animal()]. #' #' @param n Number of names to generate. #' #' @return A character vector. #' @export #' #' @examples #' gs4_random() gs4_random <- function(n = 1) { ids::adjective_animal(n = n, max_len = 10, style = "kebab") } googlesheets4/R/range_read.R0000644000176200001440000002725614074074641015464 0ustar liggesusers#' Read a Sheet into a data frame #' #' @description #' This is the main "read" function of the googlesheets4 package. It goes by two #' names, because we want it to make sense in two contexts: #' * `read_sheet()` evokes other table-reading functions, like #' `readr::read_csv()` and `readxl::read_excel()`. The `sheet` in this case #' refers to a Google (spread)Sheet. #' * `range_read()` is the right name according to the naming convention used #' throughout the googlesheets4 package. #' #' `read_sheet()` and `range_read()` are synonyms and you can use either one. #' #' @section Column Specification: #' #' Column types must be specified in a single string of readr-style short #' codes, e.g. "cci?l" means "character, character, integer, guess, logical". #' This is not where googlesheets4's col spec will end up, but it gets the #' ball rolling in a way that is consistent with readr and doesn't reinvent #' any wheels. #' #' Shortcodes for column types: #' * `_` or `-`: Skip. Data in a skipped column is still requested from the #' API (the high-level functions in this package are rectangle-oriented), but #' is not parsed into the data frame output. #' * `?`: Guess. A type is guessed for each cell and then a consensus type is #' selected for the column. If no atomic type is suitable for all cells, a #' list-column is created, in which each cell is converted to an R object of #' "best" type. If no column types are specified, i.e. `col_types = NULL`, #' all types are guessed. #' * `l`: Logical. #' * `i`: Integer. This type is never guessed from the data, because Sheets #' have no formal cell type for integers. #' * `d` or `n`: Numeric, in the sense of "double". #' * `D`: Date. This type is never guessed from the data, because date cells #' are just serial datetimes that bear a "date" format. #' * `t`: Time of day. This type is never guessed from the data, because time #' cells are just serial datetimes that bear a "time" format. *Not implemented #' yet; returns POSIXct.* #' * `T`: Datetime, specifically POSIXct. #' * `c`: Character. #' * `C`: Cell. This type is unique to googlesheets4. This returns raw cell #' data, as an R list, which consists of everything sent by the Sheets API for #' that cell. Has S3 type of `"CELL_SOMETHING"` and `"SHEETS_CELL"`. Mostly #' useful internally, but exposed for those who want direct access to, e.g., #' formulas and formats. #' * `L`: List, as in "list-column". Each cell is a length-1 atomic vector of #' its discovered type. #' * *Still to come*: duration (code will be `:`) and factor (code will be #' `f`). #' #' @inheritParams range_read_cells #' @param col_names `TRUE` to use the first row as column names, `FALSE` to get #' default names, or a character vector to provide column names directly. If #' user provides `col_types`, `col_names` can have one entry per column or one #' entry per unskipped column. #' @param col_types Column types. Either `NULL` to guess all from the #' spreadsheet or a string of readr-style shortcodes, with one character or #' code per column. If exactly one `col_type` is specified, it is recycled. #' See Column Specification for more. #' @param na Character vector of strings to interpret as missing values. By #' default, blank cells are treated as missing data. #' @param trim_ws Logical. Should leading and trailing whitespace be trimmed #' from cell contents? #' @param guess_max Maximum number of data rows to use for guessing column #' types. #' @param .name_repair Handling of column names. By default, googlesheets4 #' ensures column names are not empty and are unique. There is full support #' for `.name_repair` as documented in [tibble::tibble()]. #' #' @return A [tibble][tibble::tibble-package] #' @export #' #' @examples #' if (gs4_has_token()) { #' ss <- gs4_example("deaths") #' read_sheet(ss, range = "A5:F15") #' read_sheet(ss, range = "other!A5:F15", col_types = "ccilDD") #' read_sheet(ss, range = "arts_data", col_types = "ccilDD") #' #' read_sheet(gs4_example("mini-gap")) #' read_sheet( #' gs4_example("mini-gap"), #' sheet = "Europe", #' range = "A:D", #' col_types = "ccid" #' ) #' } range_read <- function(ss, sheet = NULL, range = NULL, col_names = TRUE, col_types = NULL, na = "", trim_ws = TRUE, skip = 0, n_max = Inf, guess_max = min(1000, n_max), .name_repair = "unique") { # check these first, so we don't download cells in vain col_spec <- standardise_col_spec(col_names, col_types) check_character(na) check_bool(trim_ws) check_non_negative_integer(guess_max) # range spec params are checked inside get_cells(): # ss, sheet, range, skip, n_max df <- get_cells( ss = ss, sheet = sheet, range = range, col_names_in_sheet = isTRUE(col_spec$col_names), skip = skip, n_max = n_max ) spread_sheet_impl_( df, col_spec = col_spec, na = na, trim_ws = trim_ws, guess_max = guess_max, .name_repair = .name_repair ) } #' @rdname range_read #' @export read_sheet <- range_read #' Spread a data frame of cells into spreadsheet shape #' #' Reshapes a data frame of cells (presumably the output of #' [range_read_cells()]) into another data frame, i.e., puts it back into the #' shape of the source spreadsheet. This function exists primarily for internal #' use and for testing. The flagship function [range_read()], a.k.a. #' [read_sheet()], is what most users are looking for. It is basically #' [range_read_cells()] + `spread_sheet()`. #' #' @inheritParams range_read #' @param df A data frame with one row per (nonempty) cell, integer variables #' `row` and `column` (probably referring to location within the spreadsheet), #' and a list-column `cell` of `SHEET_CELL` objects. #' #' @return A tibble in the shape of the original spreadsheet, but enforcing #' user's wishes regarding column names, column types, `NA` strings, and #' whitespace trimming. #' @export #' #' @examples #' if (gs4_has_token()) { #' df <- gs4_example("mini-gap") %>% #' range_read_cells() #' spread_sheet(df) #' #' # ^^ gets same result as ... #' read_sheet(gs4_example("mini-gap")) #' } spread_sheet <- function(df, col_names = TRUE, col_types = NULL, na = "", trim_ws = TRUE, guess_max = min(1000, max(df$row)), .name_repair = "unique") { col_spec <- standardise_col_spec(col_names, col_types) check_character(na) check_bool(trim_ws) check_non_negative_integer(guess_max) spread_sheet_impl_( df, col_spec = col_spec, na = na, trim_ws = trim_ws, guess_max = guess_max, .name_repair = .name_repair ) } spread_sheet_impl_ <- function(df, col_spec = list( col_names = TRUE, col_types = NULL ), na = "", trim_ws = TRUE, guess_max = min(1000, max(df$row)), .name_repair = "unique") { if (nrow(df) == 0) { return(tibble::tibble()) } col_names <- col_spec$col_names ctypes <- col_spec$ctypes col_names_in_sheet <- isTRUE(col_names) # absolute spreadsheet coordinates no longer relevant # update row, col to refer to location in output data frame # row 0 holds cells designated as column names df$row <- df$row - min(df$row) + !col_names_in_sheet nr <- max(df$row) df$col <- df$col - min(df$col) + 1 if (is.logical(col_names)) { # if col_names is logical, this is first chance to check/set length of # ctypes, using the cell data ctypes <- rep_ctypes(max(df$col), ctypes, "column{?s} found in sheet") } # drop cells in skipped cols, update df$col and ctypes skipped_col <- ctypes == "COL_SKIP" if (any(skipped_col)) { df <- df[!df$col %in% which(skipped_col), ] df$col <- match(df$col, sort(unique(df$col))) ctypes <- ctypes[!skipped_col] } nc <- max(df$col) # if column names were provided explicitly, we need to check that length # of col_names (and, therefore, ctypes) == nc if (is.character(col_names) && length(col_names) != nc) { gs4_abort(c( "Length of {.arg col_names} is not compatible with the data:", "*" = "{.arg col_names} has length {length(col_names)}.", x = "But data has {nc} un-skipped column{?s}." )) } df$cell <- apply_ctype(df$cell, na = na, trim_ws = trim_ws) if (is.logical(col_names)) { col_names <- character(length = nc) } if (col_names_in_sheet) { this <- df$row == 0 col_names[df$col[this]] <- as_character(df$cell[this], na = na, trim_ws = trim_ws) df <- df[!this, ] } df_split <- map(seq_len(nc), ~ df[df$col == .x, ]) out_scratch <- map2( df_split, ctypes, make_column, na = na, trim_ws = trim_ws, nr = nr, guess_max = guess_max ) %>% set_names(col_names) %>% discard(is.null) as_tibble(out_scratch, .name_repair = .name_repair) } ## helpers --------------------------------------------------------------------- standardise_col_spec <- function(col_names, col_types) { check_col_names(col_names) ctypes <- standardise_ctypes(col_types) if (is.character(col_names)) { ctypes <- rep_ctypes(length(col_names), ctypes, "column name{?s}") col_names <- filter_col_names(col_names, ctypes) # if column names were provided explicitly, this is now true # length(col_names) == length(ctypes[ctypes != "COL_SKIP"]) } list(col_names = col_names, ctypes = ctypes) } check_col_names <- function(col_names) { if (is.logical(col_names)) { return(check_bool(col_names)) } check_character(col_names) check_has_length(col_names) } # input: a string of readr-style shortcodes or NULL # output: a vector of col types of length >= 1 standardise_ctypes <- function(col_types) { col_types <- col_types %||% "?" check_string(col_types) if (identical(col_types, "")) { gs4_abort(" {.arg col_types}, when provided, must be a string that contains at \\ least one readr-style shortcode.") } accepted_codes <- keep(names(.ctypes), nzchar) col_types_split <- strsplit(col_types, split = "")[[1]] ok <- col_types_split %in% accepted_codes if (!all(ok)) { gs4_abort(c( "{.arg col_types} must be a string of readr-style shortcodes. \\ Unrecognized code{?s}{cli::qty(sum(!ok))}:", bulletize(gargle_map_cli(col_types_split[!ok]), bullet = "x") )) } ctypes <- ctype(col_types_split) if (all(ctypes == "COL_SKIP")) { gs4_abort("{.arg col_types} can't request that all columns be skipped.") } ctypes } # makes sure there are n ctypes or n ctypes that are not COL_SKIP rep_ctypes <- function(n, ctypes, comparator = "n") { if (length(ctypes) == n) { return(ctypes) } n_col_types <- sum(ctypes != "COL_SKIP") if (n_col_types == n) { return(ctypes) } if (length(ctypes) == 1) { return(rep_len(ctypes, length.out = n)) } # must pre-pluralize the comparator, e.g. # column{?s} found in sheet # column name{?s} comparator <- cli::pluralize(sprintf("{cli::qty(n)}%s{?s}", comparator)) gs4_abort(c( "Length of {.arg col_types} is not compatible with {comparator}:", x = "{length(ctypes)} column type{?s} specified.", x = "{n_col_types} un-skipped column type{?s} specified.", x = "But there {cli::qty(n)}{?is/are} {n} {comparator}." )) } # removes col_names for skipped columns # rep_ctypes() is called before and ensures that col_names and ctypes are # conformable (hence the non-user facing stopifnot()) filter_col_names <- function(col_names, ctypes) { stopifnot(length(col_names) <= length(ctypes)) col_names[ctypes != "COL_SKIP"] } googlesheets4/R/ctype.R0000644000176200001440000001707014074074641014512 0ustar liggesusers## ctype = cell or column type ## most types are valid for a cell or a column ## however, a couple are valid only for cells or only for a column ## Type can be Type can be Type can be ## shortcode discovered guessed for imposed on ## = ctype from a cell a column a column .ctypes <- c( `_` = "COL_SKIP", # -- no yes `-` = "COL_SKIP", "CELL_BLANK", # yes no no l = "CELL_LOGICAL", # yes yes yes i = "CELL_INTEGER", # no no yes d = "CELL_NUMERIC", # yes yes yes n = "CELL_NUMERIC", # D = "CELL_DATE", # yes no yes t = "CELL_TIME", # yes no yes `T` = "CELL_DATETIME", # yes yes yes c = "CELL_TEXT", # yes yes yes C = "COL_CELL", # -- no yes L = "COL_LIST", # -- yes yes `?` = "COL_GUESS" # -- -- -- ) ## TODO: add to above: ## CELL_DURATION ## COL_FACTOR ## this generic is "dumb": it only reports ctype ## it doesn't implement any logic about guessing, coercion, etc. ctype <- function(x, ...) { UseMethod("ctype") } #' @export ctype.NULL <- function(x, ...) { abort_unsupported_conversion(x, to = "ctype") } #' @export ctype.SHEETS_CELL <- function(x, ...) { out <- class(x)[[1]] if (out %in% .ctypes) { out } else { NA_character_ } } #' @export ctype.character <- function(x, ...) .ctypes[x] #' @export ctype.list <- function(x, ...) { out <- rep_along(x, NA_character_) is_SHEETS_CELL <- map_lgl(x, inherits, what = "SHEETS_CELL") out[is_SHEETS_CELL] <- map_chr(x[is_SHEETS_CELL], ctype) out } #' @export ctype.default <- function(x, ...) { abort_unsupported_conversion(x, to = "ctype") } .discovered_to_effective_type <- c( ## If discovered Then effective ## cell type is: cell type is: CELL_BLANK = "CELL_BLANK", CELL_LOGICAL = "CELL_LOGICAL", CELL_INTEGER = "CELL_NUMERIC", ## integers are jsonlite being helpful CELL_NUMERIC = "CELL_NUMERIC", CELL_DATE = "CELL_DATETIME", ## "date" is just a format in Sheets CELL_TIME = "CELL_DATETIME", ## "time" is just a format in Sheets CELL_DATETIME = "CELL_DATETIME", CELL_TEXT = "CELL_TEXT" ) ## input: cell type, presumably discovered ## output: effective cell type ## ## Where do we use this? ## * To choose cell-specific parser when col type is COL_LIST == "L" ## * Pre-processing cell types prior to forming a consensus for an entire ## column when col type is COL_GUESS = "?" ## This is the where we store type-guessing fiddliness that is specific to ## Google Sheets. effective_cell_type <- function(ctype) .discovered_to_effective_type[ctype] ## input: a ctype ## output: vector of ctypes that can hold such input with no data loss, going ## from most generic (list) to most specific (type of that cell) ## examples: ## CELL_LOGICAL --> COL_LIST, CELL_NUMERIC, CELL_INTEGER, CELL_LOGICAL ## CELL_DATE --> COL_LIST, CELL_DATETIME, CELL_DATE ## CELL_BLANK --> NULL admissible_types <- function(x) { z <- c( CELL_LOGICAL = "CELL_INTEGER", CELL_INTEGER = "CELL_NUMERIC", CELL_NUMERIC = "COL_LIST", CELL_DATE = "CELL_DATETIME", CELL_DATETIME = "COL_LIST", CELL_TIME = "COL_LIST", CELL_TEXT = "COL_LIST" ) if (x[[1]] == "COL_LIST") return(x) if (! x[[1]] %in% names(z)) return() c(admissible_types(z[[x[[1]]]]), x) } ## find the most specific ctype that is admissible for a pair of ctypes ## the limiting case is COL_LIST ## HOWEVER use ctypes that are good for cells, i.e. "two blanks make a blank" upper_type <- function(x, y) { upper_bound(admissible_types(x), admissible_types(y)) %||% "CELL_BLANK" } ## find the most specific ctype that is admissible for a set of ctypes ## HOWEVER use ctypes that are good for columns, i.e. "two blanks make a ## logical" consensus_col_type <- function(ctype) { out <- Reduce(upper_type, unique(ctype), init = "CELL_BLANK") blank_to_logical(out) } blank_to_logical <- function(ctype) { modify_if(ctype, ~ identical(.x, "CELL_BLANK"), ~ "CELL_LOGICAL") } ## input: an instance of CellData ## https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets#CellData ## returns same, but applies a class vector: ## [1] a ctype, inspired by the CellType enum in readxl ## [2] SHEETS_CELL apply_ctype <- function(cell_list, na = "", trim_ws = TRUE) { ctypes <- map_chr(cell_list, infer_ctype, na = na, trim_ws = trim_ws) map2(cell_list, ctypes, ~ structure(.x, class = c(.y, "SHEETS_CELL"))) } infer_ctype <- function(cell, na = "", trim_ws = TRUE) { # Blank cell criteria # * cell is NULL or list() # * cell has no effectiveValue # * formattedValue matches an `na` string if ( length(cell) == 0 || length(cell[["effectiveValue"]]) == 0 || is_na_string(cell[["formattedValue"]], na = na, trim_ws = trim_ws) ) { return("CELL_BLANK") } effective_type <- .extended_value[[names(cell[["effectiveValue"]])]] if (!identical(effective_type, "number")) { return(switch( effective_type, error = "CELL_BLANK", string = "CELL_TEXT", boolean = "CELL_LOGICAL", formula = { warn("Internal warning: Cell has formula as effectiveValue. I thought impossible!") "CELL_TEXT" }, gs4_abort("Unhandled effective_type: {.field {effective_type}}") )) } # only numeric cells remain nf_type <- pluck( cell, "effectiveFormat", "numberFormat", "type", ## in theory, should consult hosting spreadsheet for a default format ## if that's absent, should consult locale (of spreadsheet? user? unclear) ## for now, I punt on this .default = "NUMBER" ) .number_types[[nf_type]] } ## userEnteredValue and effectiveValue hold an instance of ExtendedValue ## https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets#ExtendedValue # { # // Union field value can be only one of the following: # "numberValue": number, # "stringValue": string, # "boolValue": boolean, # "formulaValue": string, # "errorValue": { # object(ErrorValue) # }, # // End of list of possible types for union field value. # } .extended_value <- c( numberValue = "number", stringValue = "string", boolValue = "boolean", formulaValue = "formula", # hypothesis: this is impossible in effectiveValue errorValue = "error" ) .number_types <- c( TEXT = "CELL_NUMERIC", NUMBER = "CELL_NUMERIC", PERCENT = "CELL_NUMERIC", CURRENCY = "CELL_NUMERIC", SCIENTIFIC = "CELL_NUMERIC", ## on the R side, all of the above are treated as numeric ## no current reason to distinguish them, for col type guessing or coercion DATE = "CELL_DATE", TIME = "CELL_TIME", DATE_TIME = "CELL_DATETIME" ) is_na_string <- function(x, na = "", trim_ws = TRUE) { if (length(na) == 0) { return(FALSE) } fv <- if (trim_ws) ws_trim(x) else x any(fv == na) } ## compares x[i] to y[i] and returns the last element where they are equal ## example: ## upper_bound(c("a", "b"), c("a", "b", "c")) is "b" upper_bound <- function(x, y) { nx <- length(x) ny <- length(y) ## these brackets make covr happy if (nx + ny == 0) {return()} if (nx == 0) {return(y[[ny]])} if (ny == 0) {return(x[[nx]])} comp <- seq_len(min(nx, ny)) ## TODO: if our DAG were more complicated, I think this would need to be ## based on a set operation res <- x[comp] == y[comp] if (!any(res)) return() x[[max(which(res))]] } googlesheets4/R/sheet_resize.R0000644000176200001440000000713114074074641016054 0ustar liggesusers#' Change the size of a (work)sheet #' #' Changes the number of rows and/or columns in a (work)sheet. #' #' @eval param_ss() #' @eval param_sheet(action = "resize") #' @param nrow,ncol Desired number of rows or columns, respectively. The default #' of `NULL` means to leave unchanged. #' @param exact Logical, indicating whether to impose `nrow` and `ncol` exactly #' or to treat them as lower bounds. If `exact = FALSE`, #' `sheet_resize()` can only add cells. If `exact = TRUE`, cells can be #' deleted and their contents are lost. #' #' @template ss-return #' @export #' @family worksheet functions #' @seealso Makes an `UpdateSheetPropertiesRequest`: #' * <# https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#UpdateSheetPropertiesRequest> #' #' @examples #' if (gs4_has_token()) { #' # create a Sheet with the default initial worksheet #' (ss <- gs4_create("sheet-resize-demo")) #' #' # see (work)sheet dims #' sheet_properties(ss) #' #' # no resize occurs #' sheet_resize(ss, nrow = 2, ncol = 6) #' #' # reduce sheet size #' sheet_resize(ss, nrow = 5, ncol = 7, exact = TRUE) #' #' # add rows #' sheet_resize(ss, nrow = 7) #' #' # add columns #' sheet_resize(ss, ncol = 10) #' #' # add rows and columns #' sheet_resize(ss, nrow = 9, ncol = 12) #' #' # re-inspect (work)sheet dims #' sheet_properties(ss) #' #' # clean up #' gs4_find("sheet-resize-demo") %>% #' googledrive::drive_trash() #' } sheet_resize <- function(ss, sheet = NULL, nrow = NULL, ncol = NULL, exact = FALSE) { ssid <- as_sheets_id(ss) maybe_sheet(sheet) maybe_non_negative_integer(nrow) maybe_non_negative_integer(ncol) check_bool(exact) x <- gs4_get(ssid) s <- lookup_sheet(sheet, sheets_df = x$sheets) gs4_bullets(c(v = "Resizing sheet {.w_sheet {s$name}} in {.s_sheet {x$name}}.")) bureq <- prepare_resize_request(s, nrow_needed = nrow, ncol_needed = ncol, exact = exact) if (is.null(bureq)) { gs4_bullets(c( i = "No need to change existing dims ({s$grid_rows} x {s$grid_columns})." )) return(invisible(ssid)) } new_grid_properties <- pluck(bureq, "updateSheetProperties", "properties", "gridProperties") new_nrow <- pluck(new_grid_properties, "rowCount") %||% s$grid_rows new_ncol <- pluck(new_grid_properties, "columnCount") %||% s$grid_columns gs4_bullets(c( v = "Changing dims: ({s$grid_rows} x {s$grid_columns}) --> \\ ({new_nrow} x {new_ncol}).")) req <- request_generate( "sheets.spreadsheets.batchUpdate", params = list( spreadsheetId = ssid, requests = list(bureq) ) ) resp_raw <- request_make(req) gargle::response_process(resp_raw) invisible(ssid) } prepare_resize_request <- function(sheet_info, nrow_needed, ncol_needed, exact = FALSE) { nrow_sheet <- sheet_info$grid_rows ncol_sheet <- sheet_info$grid_columns new_dims <- c( make_dim_patch(nrow_sheet, nrow_needed, "nrow", exact), make_dim_patch(ncol_sheet, ncol_needed, "ncol", exact) ) if (length(new_dims) == 0) { NULL } else { bureq_set_grid_properties( sheetId = sheet_info$id, nrow = new_dims$nrow, ncol = new_dims$ncol, frozenRowCount = NULL ) } } make_dim_patch <- function(current, target, nm, exact = FALSE) { out <- list() if (is.null(target)) { return(out) } patch_needed <- (isTRUE(exact) && current != target) || current < target if (patch_needed) { out[[nm]] <- target } out } googlesheets4/R/gs4_endpoints.R0000644000176200001440000000205414076034667016150 0ustar liggesusers#' List Sheets endpoints #' #' Returns a list of selected Sheets API v4 endpoints, as stored inside the #' googlesheets4 package. The names of this list (or the `id` sub-elements) are #' the nicknames that can be used to specify an endpoint in #' [request_generate()]. For each endpoint, we store its nickname or `id`, the #' associated HTTP `method`, the `path`, and details about the parameters. This #' list is derived programmatically from the Sheets API v4 Discovery #' Document (`https://www.googleapis.com/discovery/v1/apis/sheets/v4/rest`). #' #' @param i The name(s) or integer index(ices) of the endpoints to return. #' Optional. By default, the entire list is returned. #' #' @return A list containing some or all of the subset of the Sheets API v4 #' endpoints that are used internally by googlesheets4. #' @export #' #' @examples #' str(gs4_endpoints(), max.level = 2) #' gs4_endpoints("sheets.spreadsheets.values.get") #' gs4_endpoints(4) gs4_endpoints <- function(i = NULL) { if (is.null(i)) { i <- seq_along(.endpoints) } .endpoints[i] } googlesheets4/R/schema_GridRange.R0000644000176200001440000000356714074074641016556 0ustar liggesusers# https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/other#GridRange # # All indexes are zero-based. Indexes are half open, e.g the start index is # inclusive and the end index is exclusive -- [startIndex, endIndex). Missing # indexes indicate the range is unbounded on that side. #' @export as_tibble.googlesheets4_schema_GridRange <- function(x, ...) { tibble::tibble( # if there is only 1 sheet, sheetId might not be sent! # https://github.com/tidyverse/googlesheets4/issues/29 # don't be shocked if this is NA sheet_id = glean_int(x, "sheetId"), # API sends zero-based row and column # => we add one # API indices are half-open, i.e. [start, end) # => we substract one from end_[row|column] # net effect # => we add one to start_[row|column] but not to end_[row|column] start_row = glean_int(x, "startRowIndex") + 1L, end_row = glean_int(x, "endRowIndex"), start_column = glean_int(x, "startColumnIndex") + 1L, end_column = glean_int(x, "endColumnIndex") ) } as_GridRange <- function(x, ...) { UseMethod("as_GridRange") } #' @export as_GridRange.default <- function(x, ...) { abort_unsupported_conversion(x, to = "GridRange") } #' @export as_GridRange.range_spec <- function(x, ...) { if (!is.null(x$named_range)) { gs4_abort("This function does not accept a named range as {.arg range}.") } s <- lookup_sheet(x$sheet_name, sheets_df = x$sheets_df) out <- new("GridRange", sheetId = s$id) if (is.null(x$cell_limits)) { if (is.null(x$cell_range)) { return(out) } x$cell_limits <- limits_from_range(x$cell_range) } cl <- list( startRowIndex = x$cell_limits$ul[1] - 1, endRowIndex = x$cell_limits$lr[1], startColumnIndex = x$cell_limits$ul[2] - 1, endColumnIndex = x$cell_limits$lr[2] ) cl <- discard(cl, is.na) patch(out, !!!cl) } googlesheets4/R/sheets_id-class.R0000644000176200001440000001556114075373351016444 0ustar liggesusers#' `sheets_id` class #' #' @description #' `sheets_id` is an S3 class that marks a string as a Google Sheet's id, which #' the Sheets API docs refer to as `spreadsheetId`. #' #' Any object of class `sheets_id` also has the [`drive_id`][googledrive::as_id] #' class, which is used by [googledrive] for the same purpose. This means you #' can provide a `sheets_id` to [googledrive] functions, in order to do anything #' with your Sheet that has nothing to do with it being a spreadsheet. Examples: #' change the Sheet's name, parent folder, or permissions. Read more about using #' [googlesheets4] and [googledrive] together in `vignette("drive-and-sheets")`. #' Note that a `sheets_id` object is intended to hold **just one** id, while the #' parent class `drive_id` can be used for multiple ids. #' #' `as_sheets_id()` is a generic function that converts various inputs into an #' instance of `sheets_id`. See more below. #' #' When you print a `sheets_id`, we attempt to reveal the Sheet's current #' metadata, via [gs4_get()]. This can fail for a variety of reasons (e.g. if #' you're offline), but the input `sheets_id` is always revealed and returned, #' invisibly. #' @section `as_sheets_id()`: #' #' These inputs can be converted to a `sheets_id`: #' * Spreadsheet id, "a string containing letters, numbers, and some special #' characters", typically 44 characters long, in our experience. Example: #' `1qpyC0XzvTcKT6EISywvqESX3A0MwQoFDE8p-Bll4hps`. #' * A URL, from which we can excavate a spreadsheet or file id. Example: #' `"https://docs.google.com/spreadsheets/d/1BzfL0kZUz1TsI5zxJF1WNF01IxvC67FbOJUiiGMZ_mQ/edit#gid=1150108545"`. #' * A one-row [`dribble`][googledrive::dribble], a "Drive tibble" used by the #' [googledrive] package. In general, a `dribble` can represent several #' files, one row per file. Since googlesheets4 is not vectorized over #' spreadsheets, we are only prepared to accept a one-row `dribble`. #' - [`googledrive::drive_get("YOUR_SHEET_NAME")`][googledrive::drive_get()] #' is a great way to look up a Sheet via its name. #' - [`gs4_find("YOUR_SHEET_NAME")`][gs4_find()] is another good way #' to get your hands on a Sheet. #' * Spreadsheet meta data, as returned by, e.g., [gs4_get()]. Literally, #' this is an object of class `googlesheets4_spreadsheet`. #' #' @name sheets_id #' @seealso [googledrive::as_id] #' @param x Something that contains a Google Sheet id: an id string, a #' [`drive_id`][googledrive::as_id], a URL, a one-row #' [`dribble`][googledrive::dribble], or a `googlesheets4_spreadsheet`. #' @param ... Other arguments passed down to methods. (Not used.) #' @examplesIf gs4_has_token() #' mini_gap_id <- gs4_example("mini-gap") #' class(mini_gap_id) #' mini_gap_id #' #' as_sheets_id("abc") NULL # constructor and validator ---- new_sheets_id <- function(x = character()) { vec_assert(x, character()) new_vctr(x, class = c("sheets_id", "drive_id"), inherit_base_type = TRUE) } validate_sheets_id <- function(x) { if (length(x) > 1) { gs4_abort(c( "A {.cls sheets_id} object can't have length greater than 1.", x = "Actual input has length {length(x)}." )) } validate_drive_id(x) } new_drive_id <- function(x = character()) { utils::getFromNamespace("new_drive_id", "googledrive")(x) } validate_drive_id <- function(x) { utils::getFromNamespace("validate_drive_id", "googledrive")(x) } # vctrs methods ---- # sheets_id is intended to hold ONE id, so I want: # c(sheets_id, sheets_id) = drive_id # I'm willing to accept that this is not quite right / necessary if one or both # inputs has length 1 #' @export vec_ptype2.sheets_id.sheets_id <- function(x, y, ...) new_drive_id() #' @export vec_ptype2.sheets_id.character <- function(x, y, ...) character() #' @export vec_ptype2.character.sheets_id <- function(x, y, ...) character() #' @export vec_ptype2.sheets_id.drive_id <- function(x, y, ...) new_drive_id() #' @export vec_ptype2.drive_id.sheets_id <- function(x, y, ...) new_drive_id() #' @export vec_cast.sheets_id.sheets_id <- function(x, to, ...) x #' @export vec_cast.sheets_id.character <- function(x, to, ...) { validate_sheets_id(new_sheets_id(x)) } #' @export vec_cast.character.sheets_id <- function(x, to, ...) vec_data(x) #' @export vec_cast.sheets_id.drive_id <- function(x, to, ...) { validate_sheets_id(new_sheets_id(vec_data(x))) } #' @export vec_cast.drive_id.sheets_id <- function(x, to, ...) as_id(vec_data(x)) #' @export vec_ptype_abbr.sheets_id <- function(x) "sht_id" # googledrive ---- #' @export as_id.sheets_id <- function(x, ...) as_id(vec_data(x)) #' @export as_id.googlesheets4_spreadsheet <- function(x, ...) as_id(x$spreadsheet_id) # user-facing ---- #' @export #' @rdname sheets_id as_sheets_id <- function(x, ...) UseMethod("as_sheets_id") #' @export as_sheets_id.NULL <- function(x, ...) { abort_unsupported_conversion(x, to = "sheets_id") } #' @export as_sheets_id.default <- function(x, ...) { abort_unsupported_conversion(x, to = "sheets_id") } #' @export as_sheets_id.sheets_id <- function(x, ...) x #' @export as_sheets_id.drive_id <- function(x, ...) { validate_sheets_id(new_sheets_id(vec_data(x))) } #' @export as_sheets_id.dribble <- function(x, ...) { if (nrow(x) != 1) { gs4_abort(c( "{.cls dribble} input must have exactly 1 row.", x = "Actual input has {nrow(x)} rows." )) } # not worrying about whether we are authed as same user with Sheets and Drive # revealing the MIME type is local to the dribble, so this makes no API calls mime_type <- googledrive::drive_reveal(x, "mime_type")[["mime_type"]] target <- "application/vnd.google-apps.spreadsheet" if (!identical(mime_type, target)) { gs4_abort(c( "{.cls dribble} input must refer to a Google Sheet, i.e. a file with \\ MIME type {.field {target}}.", i = "File name: {.s_sheet {x$name}}", i = "File id: {.field {x$id}}", x = "MIME TYPE: {.field {mime_type}}" )) } as_sheets_id(x$id) } #' @export as_sheets_id.character <- function(x, ...) { # we're leaning on as_id() for URL detection and processing id <- as_id(x) validate_sheets_id(new_sheets_id(vec_data(id))) } #' @export as_sheets_id.googlesheets4_spreadsheet <- function(x, ...) { validate_sheets_id(new_sheets_id(x$spreadsheet_id)) } #' @export print.sheets_id <- function(x, ...) { cli::cat_line(sheets_id_print(x)) invisible(x) } sheets_id_print <- function(x) { meta <- tryCatch( gs4_get(x), # seen with a failed request gargle_error_request_failed = function(e) e, # seen when we can't get a token but auth is active googlesheets4_error = function(e) e ) if (inherits(meta, "googlesheets4_spreadsheet")) { return(format(meta)) } # meta is an error, i.e. gs4_get() failed out <- new_googlesheets4_spreadsheet(list(spreadsheetId = x)) c( format(out), "", "Unable to get metadata for this Sheet. Error details:", meta$message ) } googlesheets4/R/gs4_auth.R0000644000176200001440000002253114074130606015074 0ustar liggesusers## This file is the interface between googlesheets4 and the ## auth functionality in gargle. # Initialization happens in .onLoad .auth <- NULL ## The roxygen comments for these functions are mostly generated from data ## in this list and template text maintained in gargle. gargle_lookup_table <- list( PACKAGE = "googlesheets4", YOUR_STUFF = "your Google Sheets", PRODUCT = "Google Sheets", API = "Sheets API", PREFIX = "gs4" ) #' Authorize googlesheets4 #' #' @eval gargle:::PREFIX_auth_description(gargle_lookup_table) #' @eval gargle:::PREFIX_auth_details(gargle_lookup_table) #' @eval gargle:::PREFIX_auth_params() #' #' @family auth functions #' @export #' #' @examples #' if (interactive()) { #' # load/refresh existing credentials, if available #' # otherwise, go to browser for authentication and authorization #' gs4_auth() #' #' # force use of a token associated with a specific email #' gs4_auth(email = "jenny@example.com") #' #' # use a 'read only' scope, so it's impossible to edit or delete Sheets #' gs4_auth( #' scopes = "https://www.googleapis.com/auth/spreadsheets.readonly" #' ) #' #' # use a service account token #' gs4_auth(path = "foofy-83ee9e7c9c48.json") #' } gs4_auth <- function(email = gargle::gargle_oauth_email(), path = NULL, scopes = "https://www.googleapis.com/auth/spreadsheets", cache = gargle::gargle_oauth_cache(), use_oob = gargle::gargle_oob_default(), token = NULL) { # I have called `gs4_auth(token = drive_token())` multiple times now, # without attaching googledrive. Expose this error noisily, before it gets # muffled by the `tryCatch()` treatment of `token_fetch()`. force(token) cred <- gargle::token_fetch( scopes = scopes, app = gs4_oauth_app() %||% gargle::tidyverse_app(), email = email, path = path, package = "googlesheets4", cache = cache, use_oob = use_oob, token = token ) if (!inherits(cred, "Token2.0")) { gs4_abort(c( "Can't get Google credentials.", "i" = "Are you running {.pkg googlesheets4} in a non-interactive \\ session? Consider:", "*" = "Call {.fun gs4_deauth} to prevent the attempt to get credentials.", "*" = "Call {.fun gs4_auth} directly with all necessary specifics.", "i" = "See gargle's \"Non-interactive auth\" vignette for more details:", "i" = "{.url https://gargle.r-lib.org/articles/non-interactive-auth.html}" )) } .auth$set_cred(cred) .auth$set_auth_active(TRUE) invisible() } #' Suspend authorization #' #' @eval gargle:::PREFIX_deauth_description_with_api_key(gargle_lookup_table) #' #' @family auth functions #' @export #' @examples #' if (interactive()) { #' gs4_deauth() #' gs4_user() #' #' # get metadata on the public 'deaths' spreadsheet #' gs4_example("deaths") %>% #' gs4_get() #' } gs4_deauth <- function() { .auth$set_auth_active(FALSE) .auth$clear_cred() invisible() } #' Produce configured token #' #' @eval gargle:::PREFIX_token_description(gargle_lookup_table) #' @eval gargle:::PREFIX_token_return() #' #' @family low-level API functions #' @export #' @examples #' if (gs4_has_token()) { #' req <- request_generate( #' "sheets.spreadsheets.get", #' list(spreadsheetId = "abc"), #' token = gs4_token() #' ) #' req #' } gs4_token <- function() { if (isFALSE(.auth$auth_active)) { return(NULL) } if (!gs4_has_token()) { gs4_auth() } httr::config(token = .auth$cred) } #' Is there a token on hand? #' #' @eval gargle:::PREFIX_has_token_description(gargle_lookup_table) #' @eval gargle:::PREFIX_has_token_return() #' #' @family low-level API functions #' @export #' #' @examples #' gs4_has_token() gs4_has_token <- function() { inherits(.auth$cred, "Token2.0") } #' Edit and view auth configuration #' #' @eval gargle:::PREFIX_auth_configure_description(gargle_lookup_table) #' @eval gargle:::PREFIX_auth_configure_params() #' @eval gargle:::PREFIX_auth_configure_return(gargle_lookup_table) #' #' @family auth functions #' @export #' @examples #' # see and store the current user-configured OAuth app (probaby `NULL`) #' (original_app <- gs4_oauth_app()) #' #' # see and store the current user-configured API key (probaby `NULL`) #' (original_api_key <- gs4_api_key()) #' #' if (require(httr)) { #' # bring your own app via client id (aka key) and secret #' google_app <- httr::oauth_app( #' "my-awesome-google-api-wrapping-package", #' key = "YOUR_CLIENT_ID_GOES_HERE", #' secret = "YOUR_SECRET_GOES_HERE" #' ) #' google_key <- "YOUR_API_KEY" #' gs4_auth_configure(app = google_app, api_key = google_key) #' #' # confirm the changes #' gs4_oauth_app() #' gs4_api_key() #' #' # bring your own app via JSON downloaded from Google Developers Console #' # this file has the same structure as the JSON from Google #' app_path <- system.file( #' "extdata", "fake-oauth-client-id-and-secret.json", #' package = "googlesheets4" #' ) #' gs4_auth_configure(path = app_path) #' #' # confirm the changes #' gs4_oauth_app() #' } #' #' # restore original auth config #' gs4_auth_configure(app = original_app, api_key = original_api_key) gs4_auth_configure <- function(app, path, api_key) { if (!missing(app) && !missing(path)) { gs4_abort("Must supply exactly one of {.arg app} and {.arg path}, not both.") } stopifnot(missing(api_key) || is.null(api_key) || is_string(api_key)) if (!missing(path)) { stopifnot(is_string(path)) app <- gargle::oauth_app_from_json(path) } stopifnot(missing(app) || is.null(app) || inherits(app, "oauth_app")) if (!missing(app) || !missing(path)) { .auth$set_app(app) } if (!missing(api_key)) { .auth$set_api_key(api_key) } invisible(.auth) } #' @export #' @rdname gs4_auth_configure gs4_api_key <- function() .auth$api_key #' @export #' @rdname gs4_auth_configure gs4_oauth_app <- function() .auth$app #' Get info on current user #' #' @eval gargle:::PREFIX_user_description() #' @eval gargle:::PREFIX_user_seealso() #' @eval gargle:::PREFIX_user_return() #' #' @export #' @examples #' gs4_user() gs4_user <- function() { if (!gs4_has_token()) { gs4_bullets(c(i = "Not logged in as any specific Google user.")) return(invisible()) } email <- gargle::token_email(gs4_token()) gs4_bullets(c(i = "Logged in to {.pkg googlesheets4} as {.email {email}}.")) invisible(email) } # use this as a guard whenever a googlesheets4 function calls a # googledrive function that can make an API call # goal is to expose (most) cases of being auth'ed as 2 different users # which can lead to very puzzling failures check_gs4_email_is_drive_email <- function() { if (googledrive::drive_has_token() && gs4_has_token()) { drive_email <- googledrive::drive_user()[["emailAddress"]] gs4_email <- with_gs4_quiet(gs4_user()) if (drive_email != gs4_email) { gs4_bullets(c( "!" = "Authenticated as 2 different users with googledrive and \\ googlesheets4:", " " = "googledrive: {.email {drive_email}}", " " = "googlesheets4: {.email {gs4_email}}", " " = "If you get a puzzling result, this is probably why.", "i" = "See the article \"Using googlesheets4 with googledrive\" \\ for tips:", " " = "{.url https://googlesheets4.tidyverse.org/articles/articles/drive-and-sheets.html}" )) } } } # unexported helpers that are nice for internal use ---- gs4_auth_internal <- function(account = c("docs", "testing"), scopes = NULL, drive = TRUE) { account <- match.arg(account) can_decrypt <- gargle:::secret_can_decrypt("googlesheets4") online <- !is.null(curl::nslookup("sheets.googleapis.com", error = FALSE)) if (!can_decrypt || !online) { gs4_abort( message = c( "Auth unsuccessful:", if (!can_decrypt) { c("x" = "Can't decrypt the {.field {account}} service account token.") }, if (!online) { c("x" = "We don't appear to be online. Or maybe the Sheets API is down?") } ), class = "googlesheets4_auth_internal_error", can_decrypt = can_decrypt, online = online ) } if (!is_interactive()) local_gs4_quiet() filename <- glue("googlesheets4-{account}.json") # TODO: revisit when I do PKG_scopes() # https://github.com/r-lib/gargle/issues/103 scopes <- scopes %||% "https://www.googleapis.com/auth/drive" json <- gargle:::secret_read("googlesheets4", filename) gs4_auth(scopes = scopes, path = rawToChar(json)) gs4_user() if (drive) { googledrive::drive_auth(token = gs4_token()) gs4_bullets(c(i = "Authed also with {.pkg googledrive}.")) } invisible(TRUE) } gs4_auth_docs <- function(scopes = NULL, drive = TRUE) { gs4_auth_internal("docs", scopes = scopes, drive = drive) } gs4_auth_testing <- function(scopes = NULL, drive = TRUE) { gs4_auth_internal("testing", scopes = scopes, drive = drive) } local_deauth <- function(env = parent.frame()) { original_cred <- .auth$get_cred() original_auth_active <- .auth$auth_active gs4_bullets(c(i = "Going into deauthorized state.")) withr::defer( gs4_bullets(c("i" = "Restoring previous auth state.")), envir = env ) withr::defer({ .auth$set_cred(original_cred) .auth$set_auth_active(original_auth_active) }, envir = env) gs4_deauth() } googlesheets4/R/range_speedread.R0000644000176200001440000000756014074135011016466 0ustar liggesusers#' Read Sheet as CSV #' #' @description #' This function uses a quick-and-dirty method to read a Sheet that bypasses the #' Sheets API and, instead, parses a CSV representation of the data. This can be #' much faster than [range_read()] -- noticeably so for "large" spreadsheets. #' There are real downsides, though, so we recommend this approach only when the #' speed difference justifies it. Here are the limitations we must accept to get #' faster reading: #' * Only formatted cell values are available, not underlying values or details #' on the formats. #' * We can't target a named range as the `range`. #' * We have no access to the data type of a cell, i.e. we don't know that it's #' logical, numeric, or datetime. That must be re-discovered based on the #' CSV data (or specified by the user). #' * Auth and error handling have to be handled a bit differently internally, #' which may lead to behaviour that differs from other functions in #' googlesheets4. #' #' Note that the Sheets API is still used to retrieve metadata on the target #' Sheet, in order to support range specification. `range_speedread()` also #' sends an auth token with the request, unless a previous call to #' [gs4_deauth()] has put googlesheets4 into a de-authorized state. #' #' @inheritParams range_read_cells #' @param ... Passed along to the CSV parsing function (currently #' `readr::read_csv()`). #' #' @return A [tibble][tibble::tibble-package] #' @export #' #' @examples #' if (gs4_has_token()) { #' if (require("readr")) { #' # since cell type is not available, use readr's col type specification #' range_speedread( #' gs4_example("deaths"), #' sheet = "other", #' range = "A5:F15", #' col_types = cols( #' Age = col_integer(), #' `Date of birth` = col_date("%m/%d/%Y"), #' `Date of death` = col_date("%m/%d/%Y") #' ) #' ) #' } #' #' # write a Sheet that, by default, is NOT world-readable #' (ss <- sheet_write(chickwts)) #' #' # demo that range_speedread() sends a token, which is why we can read this #' range_speedread(ss) #' #' # clean up #' googledrive::drive_trash(ss) #' } range_speedread <- function(ss, sheet = NULL, range = NULL, skip = 0, ...) { check_installed("readr", "to use `range_speedread()`.") ssid <- as_sheets_id(ss) maybe_sheet(sheet) check_range(range) check_non_negative_integer(skip) x <- gs4_get(ssid) params <- list( spreadsheet_id = unclass(ssid), path = "export", format = "csv" ) sheet_msg <- "" range_msg <- "" range_spec <- as_range_spec( range, sheet = sheet, skip = skip, sheets_df = x$sheets, nr_df = x$named_ranges ) if (!is.null(range_spec$named_range)) { gs4_abort("{.fun range_speedread} cannot work with a named range.") } if (!is.null(range_spec$cell_limits)) { range_spec$cell_range <- as_sheets_range(range_spec$cell_limits) } if (!is.null(range_spec$cell_range)) { params[["range"]] <- range_spec$cell_range range_msg <- ", range {.range {range_spec$cell_range}}" } if (!is.null(range_spec$sheet_name)) { s <- lookup_sheet(range_spec$sheet_name, sheets_df = x$sheets) params[["gid"]] <- s$id sheet_msg <- ", sheet {.w_sheet {range_spec$sheet_name}}" } msg <- glue(" Reading from {.s_sheet {x$name}}<><>.", .open = "<<", .close = ">>") gs4_bullets(c(v = msg)) token <- gs4_token() %||% list() req <- gargle::request_build( path = "spreadsheets/d/{spreadsheet_id}/{path}", params = params, base_url = "https://docs.google.com" ) gs4_bullets(c(i = "Export URL: {.url {req$url}}")) response <- httr::GET(req$url, config = token) stopifnot(identical(httr::http_type(response), "text/csv")) readr::read_csv(httr::content(response, type = "raw"), ...) } googlesheets4/R/range_autofit.R0000644000176200001440000001130214074074641016205 0ustar liggesusers#' Auto-fit columns or rows to the data #' #' Applies automatic resizing to either columns or rows of a (work)sheet. The #' width or height of targeted columns or rows, respectively, is determined #' from the current cell contents. This only affects the appearance of a sheet #' in the browser and doesn't affect its values or dimensions in any way. #' #' @eval param_ss() #' @eval param_sheet( #' action = "modify", #' "Ignored if the sheet is specified via `range`. If neither argument", #' "specifies the sheet, defaults to the first visible sheet." #' ) #' @param range Which columns or rows to resize. Optional. If you want to resize #' all columns or all rows, use `dimension` instead. All the usual `range` #' specifications are accepted, but the targeted range must specify only #' columns (e.g. "B:F") or only rows (e.g. "2:7"). #' @param dimension Ignored if `range` is given. If consulted, `dimension` must #' be either `"columns"` (the default) or `"rows"`. This is the simplest way #' to request auto-resize for all columns or all rows. #' #' @template ss-return #' @export #' @family formatting functions #' @seealso Makes an `AutoResizeDimensionsRequest`: #' * #' #' @examples #' if (gs4_has_token()) { #' dat <- tibble::tibble( #' fruit = c("date", "lime", "pear", "plum") #' ) #' #' ss <- gs4_create("range-autofit-demo", sheets = dat) #' ss #' #' # open in the browser #' gs4_browse(ss) #' #' # shrink column A to fit the short fruit names #' range_autofit(ss) #' # in the browser, notice how the column width shrank #' #' # send some longer fruit names #' dat2 <- tibble::tibble( #' fruit = c("cucumber", "honeydew") #' ) #' ss %>% sheet_append(dat2) #' # in the browser, see that column A is now too narrow to show the data #' #' range_autofit(ss) #' # in the browser, see the column A reveals all the data now #' #' # clean up #' gs4_find("range-autofit-demo") %>% #' googledrive::drive_trash() #' } range_autofit <- function(ss, sheet = NULL, range = NULL, dimension = c("columns", "rows")) { ssid <- as_sheets_id(ss) maybe_sheet(sheet) check_range(range) x <- gs4_get(ssid) # determine targeted sheet --------------------------------------------------- range_spec <- as_range_spec( range, sheet = sheet, sheets_df = x$sheets, nr_df = x$named_ranges ) range_spec$sheet_name <- range_spec$sheet_name %||% first_visible_name(x$sheets) s <- lookup_sheet(range_spec$sheet_name, sheets_df = x$sheets) # form request --------------------------------------------------------------- if (is.null(range)) { dimension <- match.arg(dimension) resize_req <- list(bureq_auto_resize_dimensions( sheetId = s$id, dimension = toupper(dimension) )) } else { resize_req <- prepare_auto_resize_request(s$id, range_spec) } resize_dim <- pluck( resize_req, 1, "autoResizeDimensions", "dimensions", "dimension" ) gs4_bullets(c( v = "Editing {.s_sheet {x$name}}.", v = "Resizing one or more {tolower(resize_dim)} in \\ {.w_sheet {range_spec$sheet_name}}." )) # do it ---------------------------------------------------------------------- req <- request_generate( "sheets.spreadsheets.batchUpdate", params = list( spreadsheetId = ssid, requests = resize_req ) ) resp_raw <- request_make(req) gargle::response_process(resp_raw) invisible(ssid) } force_cell_limits <- function(x) { if (!is.null(x$cell_limits)) { return(x) } if (is.null(x$cell_range)) { x$cell_limits <- cell_limits() } else { x$cell_limits <- limits_from_range(x$cell_range) } x } check_only_one_dimension <- function(x) { limits <- x$cell_limits if (is.na(limits$ul[1]) && is.na(limits$lr[1])) { return(invisible(x)) } if (is.na(limits$ul[2]) && is.na(limits$lr[2])) { return(invisible(x)) } gs4_abort(" The {.arg range} must target only columns or only rows, but not both.") } determine_dimension <- function(x) { limits <- x$cell_limits if (notNA(limits$ul[1]) || notNA(limits$lr[1])) { "ROWS" } else { "COLUMNS" } } prepare_auto_resize_request <- function(sheet_id, range_spec) { range_spec <- force_cell_limits(range_spec) check_only_one_dimension(range_spec) dimension <- determine_dimension(range_spec) element <- if (dimension == "ROWS") 1L else 2L list(bureq_auto_resize_dimensions( sheetId = sheet_id, dimension = dimension, start = pluck(range_spec, "cell_limits", "ul", element), end = pluck(range_spec, "cell_limits", "lr", element) )) } googlesheets4/R/utils-sheet.R0000644000176200001440000000631414074074641015633 0ustar liggesuserslookup_sheet <- function(sheet = NULL, sheets_df, visible = NA) { maybe_sheet(sheet) if (is.null(sheets_df)) { gs4_abort("Can't look up, e.g., sheet name or id without sheet metadata.") } if (isTRUE(visible)) { sheets_df <- sheets_df[sheets_df$visible, ] } if (is.null(sheet)) { first_sheet <- which.min(sheets_df$index) return(as.list(sheets_df[first_sheet, ])) } # sheet is a string or an integer if (is.character(sheet)) { sheet <- sq_unescape(sheet) m <- match(sheet, sheets_df$name) if (is.na(m)) { gs4_abort( c("Can't find a sheet with this name:", x = "{.w_sheet {sheet}}"), sheet = sheet, # there is some usage where we throw this error, but it is OK # and we use tryCatch() # that's why we apply the sub-class class = "googlesheets4_error_sheet_not_found" ) } return(as.list(sheets_df[m, ])) } # sheet is an integer m <- as.integer(sheet) if (!(m %in% seq_len(nrow(sheets_df)))) { gs4_abort(c( "There {?is/are} {nrow(sheets_df)} sheet{?s}:", x = "Requested sheet number is out-of-bounds: {m}" )) } as.list(sheets_df[m, ]) } first_sheet <- function(sheets_df, visible = NA) { lookup_sheet(sheet = NULL, sheets_df = sheets_df, visible = visible) } first_visible <- function(sheets_df) first_sheet(sheets_df, visible = TRUE) first_visible_id <- function(sheets_df) { first_sheet(sheets_df, visible = TRUE)$id } first_visible_name <- function(sheets_df) { first_sheet(sheets_df, visible = TRUE)$name } lookup_sheet_name <- function(sheet, sheets_df) { s <- lookup_sheet(sheet = sheet, sheets_df = sheets_df) s$name } check_sheet <- function(sheet, nm = deparse(substitute(sheet))) { check_length_one(sheet, nm = nm) if (!is.character(sheet) && !is.numeric(sheet)) { gs4_abort(c( "{.arg {nm}} must be either {.cls character} (sheet name) or \\ {.cls numeric} (sheet number):", x = "{.arg {nm}} has class {.cls {class(sheet)}}." )) } sheet } maybe_sheet <- function(sheet = NULL, nm = deparse(substitute(sheet))) { if (is.null(sheet)) { sheet } else { check_sheet(sheet, nm = nm) } } #' Normalize user input re: (work)sheet names and/or data #' #' @param sheets_quo Quosure containing user input re: how to populate #' (work)sheets. #' #' @return A list with 2 equal-sized components, `name` and `value`. Size = #' number of (work)sheets. #' @keywords internal #' @noRd enlist_sheets <- function(sheets_quo) { sheets <- eval_tidy(sheets_quo) null_along <- function(x) vector(mode = "list", length = length(x)) if (is.null(sheets)) { return(NULL) } if (is.character(sheets)) { return(list(name = sheets, value = null_along(sheets))) } if (inherits(sheets, "data.frame")) { if (quo_is_symbol(sheets_quo)) { return(list(name = as_name(sheets_quo), value = list(sheets))) } else { return(list(name = list(NULL), value = list(sheets))) } } if (is_list(sheets)) { nms <- if (is_named(sheets)) names(sheets) else null_along(sheets) return(list(name = nms, value = unname(sheets))) } # we should never get here, so not a user-facing message gs4_abort("Invalid input for (work)sheet(s).") } googlesheets4/R/rectangle.R0000644000176200001440000000053413635427346015335 0ustar liggesusers# hack-y implementation of typed pluck with an NA default glean_lgl <- function(.x, ..., .default = NA) { map_lgl(list(.x), ..., .default = .default) } glean_chr <- function(.x, ..., .default = NA) { map_chr(list(.x), ..., .default = .default) } glean_int <- function(.x, ..., .default = NA) { map_int(list(.x), ..., .default = .default) } googlesheets4/R/sheet_delete.R0000644000176200001440000000364114074074641016017 0ustar liggesusers#' Delete one or more (work)sheets #' #' Deletes one or more (work)sheets from a (spread)Sheet. #' #' @eval param_ss() #' @eval param_sheet( #' action = "delete", #' "You can pass a vector to delete multiple sheets at once or even a list,", #' "if you need to mix names and positions." #' ) #' #' @return The input `ss`, as an instance of [`sheets_id`] #' @export #' @family worksheet functions #' @seealso Makes an `DeleteSheetsRequest`: #' * #' #' @examples #' if (gs4_has_token()) { #' ss <- gs4_create("delete-sheets-from-me") #' sheet_add(ss, c("alpha", "beta", "gamma", "delta")) #' #' # get an overview of the sheets #' sheet_properties(ss) #' #' # delete sheets #' sheet_delete(ss, 1) #' sheet_delete(ss, "gamma") #' sheet_delete(ss, list("alpha", 2)) #' #' # get an overview of the sheets #' sheet_properties(ss) #' #' # clean up #' gs4_find("delete-sheets-from-me") %>% #' googledrive::drive_trash() #' } sheet_delete <- function(ss, sheet) { ssid <- as_sheets_id(ss) walk(sheet, ~ check_sheet(.x, nm = "sheet")) # retrieve spreadsheet metadata ---------------------------------------------- x <- gs4_get(ssid) # capture sheet ids ---------------------------------------------------------- s <- map(sheet, ~ lookup_sheet(.x, sheets_df = x$sheets)) sheet_names <- map_chr(s, "name") n <- length(sheet_names) gs4_bullets(c( v = "Deleting {n} sheet{?s} from {.s_sheet {x$name}}:", bulletize(gargle_map_cli(sheet_names, template = "{.field <>}")) )) sid <- map(s, "id") requests <- map(sid, ~ list(deleteSheet = list(sheetId = .x))) req <- request_generate( "sheets.spreadsheets.batchUpdate", params = list( spreadsheetId = ssid, requests = requests ) ) resp_raw <- request_make(req) gargle::response_process(resp_raw) invisible(ssid) } googlesheets4/R/gs4_share.R0000644000176200001440000000216114074074641015240 0ustar liggesusers# currently just for development # I'm generally auth'd as: # * as a service acct (which means I can't look at anything in the browser) # * with Drive and Sheets scope # * with googlesheets4 and googledrive # so this is helpful for quickly granting anyone or myself specifically # permission to read or write a Sheet I'm fiddling with in the browser or the # API explorer # # Note defaults: role = "reader", type = "anyone" # --> "anyone with the link" can view # # examples: # gs4_share(ss) # gs4_share(ss, type = "user", emailAddress = "jane@example.com") # gs4_share(ss, type = "user", emailAddress = "jane@example.com", role = "writer") gs4_share <- function(ss, ..., role = c( "reader", "commenter", "writer", "owner", "organizer" ), type = c("anyone", "user", "group", "domain")) { check_gs4_email_is_drive_email() role <- match.arg(role) type <- match.arg(type) googledrive::drive_share( file = as_sheets_id(ss), role = role, type = type, ... ) } googlesheets4/R/sheet_rename.R0000644000176200001440000000324414074074641016023 0ustar liggesusers#' Rename a (work)sheet #' #' Changes the name of a (work)sheet. #' #' @eval param_ss() #' @eval param_sheet( #' action = "rename", #' "Defaults to the first visible sheet." #' ) #' @param new_name New name of the sheet, as a string. This is required. #' #' @template ss-return #' @export #' @family worksheet functions #' @seealso Makes an `UpdateSheetPropertiesRequest`: #' * #' #' @examples #' if (gs4_has_token()) { #' ss <- gs4_create( #' "sheet-rename-demo", #' sheets = list(cars = head(cars), chickwts = head(chickwts)) #' ) #' sheet_names(ss) #' #' ss %>% #' sheet_rename(1, new_name = "automobiles") %>% #' sheet_rename("chickwts", new_name = "poultry") #' #' # clean up #' gs4_find("sheet-rename-demo") %>% #' googledrive::drive_trash() #' } sheet_rename <- function(ss, sheet = NULL, new_name) { ssid <- as_sheets_id(ss) maybe_sheet(sheet) check_string(new_name) x <- gs4_get(ssid) s <- lookup_sheet(sheet, sheets_df = x$sheets) gs4_bullets(c( v = "Renaming sheet {.w_sheet {s$name}} to {.w_sheet {new_name}}." )) sp <- new("SheetProperties", sheetId = s$id, title = new_name) update_req <- new( "UpdateSheetPropertiesRequest", properties = sp, fields = gargle::field_mask(sp) ) req <- request_generate( "sheets.spreadsheets.batchUpdate", params = list( spreadsheetId = ssid, requests = list(updateSheetProperties = update_req) ) ) resp_raw <- request_make(req) gargle::response_process(resp_raw) invisible(ssid) } googlesheets4/NEWS.md0000644000176200001440000001643014076062455014141 0ustar liggesusers# googlesheets4 1.0.0 ## User interface The user interface has gotten more stylish, thanks to the cli package (). All informational messages, warnings, and errors are now emitted via cli, which uses rlang's condition functions under-the-hood. googlesheets4 now throws errors with class `"googlesheets4_error"` (#12). `googlesheets4_quiet` is a new option to suppress informational messages from googlesheets4 (#163). Unless it's explicitly set to `TRUE`, the default is to message. `local_gs4_quiet()` and `with_gs4_quiet()` are [withr-style](https://withr.r-lib.org) convenience helpers for setting `googlesheets4_quiet = TRUE`. ## Other changes The deprecated `sheets_*()` functions have now been removed, as promised in the warning they have been throwing for over a year. No functionality has been removed, this is just the result of the function (re-)naming scheme adopted in googlesheets4 >= 0.2.0. More details are in [this developer documentation](https://googlesheets4.tidyverse.org/articles/articles/function-class-names.html#previous-use-of-sheets-prefix). The `na` argument of `read_sheet()` has become more capable and more consistent with readr. Specifically, `na = character()` (or the general lack of `""` among the `na` strings) results in cells with no data appearing as the empty string `""` within a character vector, as opposed to `NA` (#174). Explicit `NULL`s are now written properly, i.e. as an empty cell (#203). `sheet_append()` no longer touches any aspect of cell formatting other than `numberFormat` (#204). `gs4_example()` and `gs4_examples()` now learn the example Sheet ids from a Google Sheet. This should not change anything for users, but it means there is an API call the first time either of these functions is called. ## Dependency changes * cli is new in Imports. * googlesheets4 Suggests testthat >= 3.0.0 and, specifically, uses third edition features. R 3.4 is now the oldest version that is explicitly supported and tested, as per the [tidyverse policy](https://www.tidyverse.org/blog/2019/04/r-version-support/). # googlesheets4 0.3.0 All requests are now made with retry capability. Specifically, when a request fails due to a `429 RESOURCE_EXHAUSTED` error, it is retried a few times, with suitable delays. Note that if it appears that you *personally* have exhausted your quota (more than 100 requests in 100 seconds), the initial waiting time is 100 seconds and this indicates you need to get your own OAuth app or service account. When googlesheets4 and googledrive are used together in the same session, we alert you if you're logged in to these package with different Google identities. `gs4_get()` retrieves information about protected ranges. # googlesheets4 0.2.0 googlesheets4 can now write and modify Sheets. Several new articles are available at [googlesheets4.tidyverse.org](https://googlesheets4.tidyverse.org/articles/index.html). ## Function naming scheme The universal `sheets_` prefix has been replaced by a scheme that conveys more information about the scope of the function. There are three prefixes: * `gs4_`: refers variously to the googlesheets4 package, v4 of the Google Sheets API, or to operations on one or more (spread)Sheets * `sheet_`: operations on one or more (work)sheets * `range_`: operations on a range of cells The addition of write/edit functionality resulted in many new functions and the original naming scheme proved to be problematic. The article [Function and class names](https://googlesheets4.tidyverse.org/articles/articles/function-class-names.html) contains more detail. Any function present in the previous CRAN release, v0.1.1, still works, but triggers a warning with strong encouragement to switch to the current name. ## Write Sheets googlesheets4 now has very broad capabilities around Sheet creation and modification. These functions are ready for general use but are still marked experimental, as they may see some refinement based on user feedback. * `gs4_create()` creates a new Google Sheet and, optionally, writes one or more data frames into it (#61). * `sheet_write()` (also available as `write_sheet()`) writes a data frame into a new or existing (work)sheet, inside an existing (or new) (spread)Sheet. * `sheet_append()` adds rows to an existing data table. * `range_write()` writes to a cell range. * `range_flood()` "floods" all cells in a range with the same content. `range_clear()` is a wrapper around `range_flood()` for the special case of clearing cell values. * `range_delete()` deletes a range of cells. ## (Work)sheet operations The `sheet_*()` family of functions operate on the (work)sheets inside an existing (spread)Sheet: * (`sheet_write()` and `sheet_append()` are described above.) * `sheet_properties()` returns a tibble of metadata with one row per sheet. * `sheet_names()` returns sheet names. * `sheet_add()` adds one or more sheets. * `sheet_copy()` copies a sheet. * `sheet_delete()` deletes one or more sheets. * `sheet_relocate()` moves sheets around. * `sheet_rename()` renames one sheet. * `sheet_resize()` changes the number of rows or columns in a sheet. ## Range operations `range_speedread()` reads from a Sheet using its "export=csv" URL and, therefore, uses readr-style column type specification. It still supports fairly general range syntax and auth. For very large Sheets, this can be substantially faster than `read_sheet()`. `range_read_cells()` (formerly known as `sheets_cells()`) gains two new arguments that make it possible to get more data on more cells. By default, we get only the fields needed to parse cells that contain values. But `range_read_cells(cell_data = "full", discard_empty = FALSE)` is now available if you want full cell data, including formatting, even for cells that have no value (#4). `range_autofit()` adjusts column width or row height to fit the data. This only affects the display of a sheet and does not change values or dimensions. ## Printing a Sheet ID The print method for `sheets_id` objects now attempts to reveal the current Sheet metadata available via `gs4_get()`, i.e. it makes an API call (but it should never error). ## Other changes and additions `gs_formula()` implements a vctrs S3 class for storing Sheets formulas. `gs4_fodder()` is a convenience function that creates a filler data frame you can use to make toy sheets you're using to practice on or for a reprex. ## Renamed classes The S3 class `sheets_Spreadsheet` is renamed to `googlesheets4_spreadsheet`, a consequence of rationalizing all internal and external classes (detailed in the article [Function and class names](https://googlesheets4.tidyverse.org/articles/articles/function-class-names.html)). `googlesheets4_spreadsheet` is the class that holds metadata for a Sheet and it is connected to the API's [`Spreadsheet`](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets#resource:-spreadsheet) schema. The return value of `gs4_get()` has this class. ## Bug fixes * `read_sheet()` passes its `na` argument down to the helpers that parse cells, so that `na` actually has the documented effect (#73). # googlesheets4 0.1.1 * Patch release to modify a test fixture, to be compatible with tibble v3.0. Related to tibble's increased type strictness. # googlesheets4 0.1.0 * Added a `NEWS.md` file to track changes to the package. googlesheets4/MD50000644000176200001440000002350414076066132013347 0ustar liggesusers84d5b44947e64715c0df1d4a6a3378bf *DESCRIPTION 06cdb575e58cbbab438ac40e6ce562dc *LICENSE 0ea40ddc0ce144ba9bbc2f569bcb6c40 *NAMESPACE 0a49684fd8a821759a73b5cf01067572 *NEWS.md 2a387950e2a2139af8f1306330d4b6ed *R/aaa.R 48c2d3579100302ec48b65a2a70d0775 *R/batch-update-requests.R 25a847708dffa0f3f5afc46a32652f1a *R/cell-specification.R 5bfffabd0ce163494284a9cc015b2ea0 *R/ctype.R 8e9ff8c9a561ad1e985cd38fb6fa9234 *R/get_cells.R 2e38916d7869e9828a4a385b99dc8522 *R/googlesheets4-package.R a28efd36deb7bf0d2dec1658c5e5530a *R/gs4_auth.R 1151a6ff18d1d176495fec3bfd88fd28 *R/gs4_browse.R 596661639492c92d4704c094cf8271d5 *R/gs4_create.R a5540f98d45c9d8eca7cefe24f3584ae *R/gs4_endpoints.R bc5992dfe15baade006694f4ad7cb496 *R/gs4_example.R 469da4b7357ed4826097e786a69be2f6 *R/gs4_find.R 0ec87192bdc19a7e494d96e0caa7daa1 *R/gs4_fodder.R 05028307db0c2bbda7cd6aa8d0da3e4c *R/gs4_formula.R 23bd1fda908922d8a66c39e6a712c28f *R/gs4_get.R 22db4e6be4ed08be59798fcd22506d2e *R/gs4_share.R eac723189f9bb7ee4ae19b9a22b06680 *R/make_column.R 2062b3ebb490b2a49a4677b9c4c9ba52 *R/range_add_named.R 27cdba6e8df117b5ede5bc4c841e22c4 *R/range_add_protection.R d1b64ae691be2c3b162490ba51d2a23c *R/range_add_validation.R c0a47bd3f3e040a8f198209fb0bdf20c *R/range_autofit.R 9861047953b89a418985e4986dbf2657 *R/range_delete.R b8f3fc8d8346386e8bf8970a330befc3 *R/range_flood.R 50a898927f59604ae671b87172f216ad *R/range_read.R b544eed22bbbb6f15b7926e624d1c174 *R/range_read_cells.R 9a7baa74bfd41b8887f15389e7851139 *R/range_spec.R ede2f92a490355d6ae47da91e23af94b *R/range_speedread.R eaecf787bc8d8040ebd8cdc468fe4601 *R/range_write.R 997ed9f901d3d1a49ad50abff7e5baf4 *R/rectangle.R 1175d49a8ed17fa08580e45f4e43ac28 *R/request_generate.R 82ec73ff7e62db7e6ca4a5752e021ebb *R/request_make.R efab76bbcf66c86ec914ffce570316f5 *R/roxygen.R 638fd593196d7ed216caf08bf38ea17c *R/schema_CellData.R 231d9d335f1f9eca0dba3b58da97a3e4 *R/schema_GridCoordinate.R ee6044b6e2392bb73d1248fd40feeef6 *R/schema_GridRange.R 0a40309938a077c98dbde7fe521334b0 *R/schema_NamedRange.R de5b7c15cd49c098aa922e66aeb153e7 *R/schema_ProtectedRange.R ec7edba9515a8da2ba9124550dc69cdc *R/schema_RowData.R 8ca72d01e1b6226e71116d8129a15665 *R/schema_Sheet.R a75bf24bcef76149467537395e37cf84 *R/schema_SheetProperties.R f0adc5ebc1a8d978d4aca45df425e8f7 *R/schema_Spreadsheet.R 6ffeb58d3653eabaa02d2669f105c60d *R/schemas.R 4b800ce4d8c83cd50f4ae61f4d9b55ef *R/sheet_add.R 51489df5019c718ac3f0b9402cbbe553 *R/sheet_append.R 519e057f212df7b9d14ce9bb9e882caa *R/sheet_copy.R 316e0f578453888b9b21ec83ffa71241 *R/sheet_delete.R 8c63323ceeb7c264bd47117f2c97e767 *R/sheet_freeze.R 54fe49007f85185b8dd74c78244dc294 *R/sheet_properties.R a346f191ffeef58e593cf7ef0bc1a7a2 *R/sheet_relocate.R ede1a8fb4931b1b0262284f6b79491e9 *R/sheet_rename.R 8f4106694e27799b4bbd21a19fa6b00f *R/sheet_resize.R f2f1d22b31fdd8e90eb90f764606bdf2 *R/sheet_write.R a5e095b7bcdbb5da1544a1e85bd8ada8 *R/sheets_id-class.R 232e3324b97cb90a14e025ed2f94ca23 *R/sysdata.rda 9290081970d401c061328325896a4e16 *R/utils-cell-ranges.R 8d2487dbf45064f33b08e5a80830b68e *R/utils-pipe.R e9ce913701031348f320453e9a26657b *R/utils-sheet.R ab9e36f32bfd8aa61e4c8fb59c5e9b79 *R/utils-ui.R 31350947071cdd5e13c39a68402e8207 *R/utils.R 4fee5c5808d5454463c53e3f9f21117e *R/zzz.R bd3845793a4fd3e48dde1990cbec38be *README.md f4110c47e8329095556013c716336588 *inst/WORDLIST b84aab04f6a9fad8d1cd1154aec47ed9 *inst/extdata/example_and_test_sheets.csv 30c5fe5cdc74e63bbf2241319931692a *inst/extdata/fake-oauth-client-id-and-secret.json d4740b50816ed8e474240ebbd56609a9 *inst/secret/googlesheets4-docs.json 713b8094b51df88812c285618f7330ac *inst/secret/googlesheets4-testing.json 2bed12c840e30120edceadd3636ac8b7 *man/cell-specification.Rd cb1e46f469cfbbbde29c8b5113e1d789 *man/figures/lifecycle-archived.svg c0d2e5a54f1fa4ff02bf9533079dd1f7 *man/figures/lifecycle-defunct.svg a1b8c987c676c16af790f563f96cbb1f *man/figures/lifecycle-deprecated.svg c3978703d8f40f2679795335715e98f4 *man/figures/lifecycle-experimental.svg 952b59dc07b171b97d5d982924244f61 *man/figures/lifecycle-maturing.svg 27b879bf3677ea76e3991d56ab324081 *man/figures/lifecycle-questioning.svg 46de21252239c5a23d400eae83ec6b2d *man/figures/lifecycle-retired.svg 6902bbfaf963fbc4ed98b86bda80caa2 *man/figures/lifecycle-soft-deprecated.svg 53b3f893324260b737b3c46ed2a0e643 *man/figures/lifecycle-stable.svg dd15f73ab864c5f5c5f9b9624cd9b6b1 *man/figures/logo.png 861a90ddf521912038826c12a96370aa *man/googlesheets4-configuration.Rd 19f91435363ad9059bf70040a902ad2e *man/googlesheets4-package.Rd b80efcb1f431e9103b0bd8fa0c8eab3b *man/googlesheets4-vctrs.Rd 1f9068fb60c12586b0c60549d2286636 *man/gs4_auth.Rd 1cdd7fee3de9c44678ff0b6efc32a5d2 *man/gs4_auth_configure.Rd 6e7684edb632aa6b9e7b0b22700fa08d *man/gs4_browse.Rd b0db2705ca2100c1073f8352c257bbc0 *man/gs4_create.Rd 9e075a73eb0f3c383776566fee39075a *man/gs4_deauth.Rd 78bb7373bcaa07a09c7114d4adbea159 *man/gs4_endpoints.Rd 11ec04206e98d13ff533b829f96bfa67 *man/gs4_examples.Rd 2011717b79f0f031598a8b273184b3c2 *man/gs4_find.Rd b82a4fba26b3f22891e7c477ee1d9668 *man/gs4_fodder.Rd d6e6ca4f5647212e007415d380463b8a *man/gs4_formula.Rd e519f81c66c156472610540d88f9031f *man/gs4_get.Rd 6ded4e6dd61f2886dc1fa1aa56316ec4 *man/gs4_has_token.Rd 38364fe81457a5d97832a6ac0b3286fa *man/gs4_random.Rd ec4e97a1d62f9794a65d8158ea7afce4 *man/gs4_token.Rd 7f2b6842f51ac39c6da6499a8a79bd51 *man/gs4_user.Rd 1f7896a1b866ff9ae89ba35be7c7b6f1 *man/pipe.Rd 1b5f10dff138f7c06d98236fd5bb6a38 *man/range_autofit.Rd b73af04fa7ab0236a6653b7d34a3c57e *man/range_delete.Rd c51ace762c0798f75f6fb3a605cca4b4 *man/range_flood.Rd b56ccde07f0671d1334c3aee4b413281 *man/range_read.Rd 742ddc9c62b44b9872e999341aef64dc *man/range_read_cells.Rd d4b4a424465a62316ea3c0d788da9bce *man/range_speedread.Rd 534fead731560f5566aaf81927c4ac11 *man/range_write.Rd b1dc9a4e8406e52a9e58ce2f1ae83dab *man/request_generate.Rd 9b05cebc17d0a0fe419cce4ec16a8e09 *man/request_make.Rd 9e47ebe753411024323600d690b3ae6b *man/roxygen/templates/n_max.R fd3fb170d447b88c96d0899d9e27e1df *man/roxygen/templates/range.R f20737eacbc83ea0b81c5b466f9f1324 *man/roxygen/templates/reformat.R b37e3e46623c0e9240403710d51dfe54 *man/roxygen/templates/skip-read.R b164ac0b86e9e3c97545e96e70992e81 *man/roxygen/templates/ss-return.R a0aee6285e186d0b105b51d84abee053 *man/sheet_add.Rd 6ea9eb88bfac9be4b6c0d1dfb2e3a30d *man/sheet_append.Rd 9d25ef4e81251329244f9be06c3119da *man/sheet_copy.Rd 132cad26e026f2f1bd7af5d34776cfa1 *man/sheet_delete.Rd 3c96eed2e768b22a0f8c1fe8a71ba0ee *man/sheet_properties.Rd a384398fbb81d76a487cace5d5891442 *man/sheet_relocate.Rd 814a3181c79de9264afb6dcca4f30890 *man/sheet_rename.Rd d2ae3fdcc19f3c5fc58b7debe02a7910 *man/sheet_resize.Rd d59c74558ee884a43102df04b2fa04a2 *man/sheet_write.Rd cf169bfef767770bee515c8a4782f1d8 *man/sheets_id.Rd c0ee0e0b464c06c979f1c3c01098c1d9 *man/spread_sheet.Rd 0622a97a2aaa3c342f09636052c2d7f5 *tests/spelling.R 5c0f1bdb3f9d7e2d4b43b7f1cd47e346 *tests/testthat.R 3d421d65b65011634356dff93820b624 *tests/testthat/_snaps/argument-checkers.md d7cfbf8efdc035a036f0568421ca1699 *tests/testthat/_snaps/schemas.md 9783edb8e2dd87b66df9fd0aac6556f9 *tests/testthat/_snaps/sheet_add.md 72ab389da12f4adda32222a6a4e7b8cc *tests/testthat/_snaps/sheets_id-class.md d744ecf2edbe8fac303998847974b155 *tests/testthat/_snaps/utils-ui.md a1b4843c5001e660ba3a72141af1e91d *tests/testthat/helper.R 48d9cb72be2567c9f8d059754dedd117 *tests/testthat/ref/dribble.rds 97f6644357c0c618b0c0e3fd2c237947 *tests/testthat/ref/googlesheets4-cell-tests.rds 5990afc31782d015ae93bb99826f26b2 *tests/testthat/test-aaa.R 8f06b8bf72a855af6e3cc62aeccb0912 *tests/testthat/test-argument-checkers.R ff4d645445c622acd1628d165b99f0bc *tests/testthat/test-ctype.R c6fb2c81423210f89dcfc8c3c02ffe07 *tests/testthat/test-gs4_endpoints.R 3b77e8ace4e314440e97d34a17961d5b *tests/testthat/test-gs4_example.R 9bc1f5eae19899b646b60bf5949d8cb7 *tests/testthat/test-gs4_find.R 42288e8a50ab88be5c6edfe48498f61f *tests/testthat/test-gs4_fodder.R efaebf56f60488ff99f10e4f9c5f55fd *tests/testthat/test-gs4_formula.R 1bf7c7e5d1064a8126a1feb04b3b6be6 *tests/testthat/test-make_column.R 97e913c330fe28df8e6d75ef2fdafc35 *tests/testthat/test-range_autofit.R efa0e250ee9ce655574e899d0aac7c9e *tests/testthat/test-range_delete.R 6c2402b61a7d7098aeb9a1abd51f0b39 *tests/testthat/test-range_flood.R 41fe4f262207567d6df7486690d11dff *tests/testthat/test-range_read.R 28568c081940c9e672b900d786ba3a01 *tests/testthat/test-range_read_cells.R b994c8d356476d98de54b50aa455a111 *tests/testthat/test-range_spec.R 0276e5a4ff5ee4949699052ea5d3601f *tests/testthat/test-range_speedread.R e7bb7fabd75a1ac90ecc8ce6242c1af5 *tests/testthat/test-range_write.R 7081afdebdd4aef6438643646d1e25fb *tests/testthat/test-rectangle.R feb7873405cf06be960f42c3d3fa1da5 *tests/testthat/test-request_generate.R 6cc2cf3bd73a3010be415efc0d51d0e2 *tests/testthat/test-schema_CellData.R 7ee882dbdf7035e2ed82de672beb059d *tests/testthat/test-schema_GridCoordinate.R 33a7d13f482c1b0e0e4a35d2ea9a068e *tests/testthat/test-schema_GridRange.R b33d0d152ab86d2dc87c8a354ce1b5fb *tests/testthat/test-schemas.R 62f7c6ea9a2232195ddda63123c0d156 *tests/testthat/test-sheet_add.R 3c4c673671a570badd6a55178c701df5 *tests/testthat/test-sheet_append.R 4398816f0901334ad26ae3054e0a6331 *tests/testthat/test-sheet_copy.R feebcc071ad2f7bc3f193fad8b1c96ff *tests/testthat/test-sheet_delete.R 876853d8124b65b58d5e3a0dc4c7683e *tests/testthat/test-sheet_relocate.R ce4459a681f8bdec678d8dfbe6ff3079 *tests/testthat/test-sheet_rename.R 9b74f459e4ec59b05ba3ff3fc4aa939f *tests/testthat/test-sheet_resize.R 9848ac61410bd1ddf8179936bab585ae *tests/testthat/test-sheet_write.R 573471480e561712e501f73f3df6c4c3 *tests/testthat/test-sheets_id-class.R 9b9924341467a274991c16b16ce4669c *tests/testthat/test-utils-cell-ranges.R d7723f92ac617a14f16db4f00db38fd2 *tests/testthat/test-utils-sheet-geometry.R cc4cefcbd0fbf8a1e3d0aeddce9d27f6 *tests/testthat/test-utils-sheet.R a93bddcd902072f7a3060d869f29e6ae *tests/testthat/test-utils-ui.R 973e3aa0c7507b6a5fe193f68e4d493f *tests/testthat/test-utils.R googlesheets4/inst/0000755000176200001440000000000014074074641014012 5ustar liggesusersgooglesheets4/inst/secret/0000755000176200001440000000000014074074641015277 5ustar liggesusersgooglesheets4/inst/secret/googlesheets4-testing.json0000644000176200001440000000450414074074641022424 0ustar liggesusers]Cr"u̬G%Q4l+m=q% ] !͌GLM&\0- au\CeWDd]4slwY8n26~g}·=3nZhn[Fz>T;? kwhpp@կ2ucھHBi DؽjX1jH3GC=z[jl(fC/wdDZ-B]thV?$ \5TEQ듾9d.9/ə&kqq[ܡY)gr?] t?mZd z|xU 8V%g$ `J՘I<=^[L2/YYm7BH}\n53%+a7>4c"$ E޸6!3LF'$]  ~ 't%^Ήlqb>#rF1 ac:sJO 4@>_?UXAymsTsAWu~:s*?U@]xW%/&B4[Ktpf5ud$ ݍgr"|I90/rjE{;ۏB eph4jH/mpLe̟p} ᪷/~#`f+:3$pv?K+%>" ZGL֧pZ:}t!VQ4ɥG8qkMy}a2K"ô%H qzYm?u)'W>6֏F<}D+9_ TcfTe7W3 ybu#6JWOs Xs`#8I `7M xbPcv~ra,cYܒ̫`܏l8 r1~{A 6L}Skd6EA/J%Z۲ʑ!CR v@,'<Ԍfl s((vQQ@-,M'oBi.4Tr­2/C]E MnʯΘ?U;K38o esu6qc˿ƺU6Jъ݊Uo*kifS(Vs#M ҹcSzl۾5h&rX䓬7EOF N䳏#=HI-8jXO6=1<\}J.}kVF\+5`Q.M2^^ `80=QHl|&hga5YJl.OUt}"u$ 3/%?s{O~}) ?nͱ?0 AJpi”Xqk?Aoky|>4/iT=8~EE2#pM+P$ܐ*pnn_rz4ˍT/(RNu-$(M{~Sy5 d1p8KP¢\x&mK3c )} >googlesheets4/inst/secret/googlesheets4-docs.json0000644000176200001440000000447614074074641021707 0ustar liggesusersn}'gD\<̬G%Q4l+m=q% ] !͌GLM&\0- au\CeWDd]4slwY8n`b.5ÿ́5>4S3 n[.kVQm5 kwhpp@կ2ucھHBi DؽjX1jH3GC=z[jl(fC/wdDZ-B]t< *Z-BA|*y3SCӢZkp3 ed}iZLZJH%ƑpkVQ]hV"2T5 }EJB2[VaZ)80a~!դj̅@דHwN>LTd,~Cy1R62DJ=7b"G}q>K#E;8))RB=`1 )vleEL <ӏn'ܧy7?5@iYϗ1tX[r,a$<]j aK;hR@[NFߝW5>G> AA9$ \5Bxj֔RkQ=֭W%`^c чgr;Gp6`ɑJYrQ0_eeaLTD{[RRΤg5TiFشhRGP* 2]ɼxy͂@%}نKlW xzemZ -z:z_N 5a$`[w\\)D{.6O2B5jE$8l1}v]-:(,52:MdӠv2:Q6!mvP>Ejo| 'Tl=׊'ό#ފF¸w0'uRϢ8:Idx_(K7us! e9{w׌5 <q7F!rFCSb(| v^f˔ѧʜgwgTR7˶(RiK@3M)qR#:( zbQu$tQ^OίC+z~mہMlIzիZHOVG0I.FѰ̬-"E>qpگz?SLq1K,mwDO;:K.)iXYVZ|vNdct .9ud #3!@ j"Re-i5$zCXм Sdj"NizbpLfͤ QU2 Gpx &*O wJoN/"] Sfj·߾ J<{J  >M::jf?,rX0W 0(m?v%#)(׶c] ۝A'8ze 6)3@{Twoю )7RV8wJ+Up^jCz9|]$37CM`O &$wod~v^\T<#H7).Z}VL&њzPB3֙cdTPq%.ݖv7 C >!zǹTůk4pJ!)tcx^_bA AaȆ p|R!őLCįQqNB;]Q2W5W}Iu[XĄǞ.S*pӌne*kifS(Vs#M ҹcSzl۾5h&rX䓬7EOF N䳏37XA.Wo&6>h[=}e ]O),,w5N#Hk205XLcAš)2Phǽ/o_+:6LHbf$5V'ւa=9Ұ.n&$4-Ej/:snB`}xR1cD+pxB23?r2-tBRNRҮivI~q$U^ID;LzfmuwչSso1KnNv' >Q*i02J a4fQEdq5_ $ 8hݞYgooglesheets4/inst/extdata/0000755000176200001440000000000014075126357015447 5ustar liggesusersgooglesheets4/inst/extdata/fake-oauth-client-id-and-secret.json0000644000176200001440000000070213564636604024261 0ustar liggesusers{ "installed": { "client_id": "YOUR_CLIENT_ID_GOES_HERE", "project_id": "YOUR_PROJECT_ID_GOES_HERE", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", "client_secret": "YOUR_SECRET_GOES_HERE", "redirect_uris": [ "urn:ietf:wg:oauth:2.0:oob", "http://localhost" ] } } googlesheets4/inst/extdata/example_and_test_sheets.csv0000644000176200001440000000116014075320761023044 0ustar liggesusersname,purpose,id cell-contents-and-formats,example,1peJXEeAp5Qt3ENoTvkhvenQ36N3kLyq6sq9Dh2ufQ6E chicken-sheet,example,1ct9t1Efv8pAGN9YO5gC2QfRq2wT4XjNoTMXpVeUghJU deaths,example,1VTJjWoP1nshbyxmL9JqXgdVsimaYty21LGxxs018H2Y formulas-and-formats,example,1wPLrWOxxEjp3T1nv2YBxn63FX70Mz5W5Tm4tGc-lRms gapminder,example,1U6Cf_qEOhiR9AZqTqS3mbMF3zt2db48ZP5v3rkrAEJY Getting started,example,0B0ft3MvMsr66c3RhcnRlcl9maWxl mini-gap,example,1k94ZVVl6sdj0AXfK9MQOuQ4rOhd1PULqpAu2_kr9MAU googlesheets4-cell-tests,test,1WRFIb11PJsNwx2tYBRn3uq8uHwWSI5ziSgbGjkOukmE googlesheets4-col-types,test,1q-iRi1L3JugqHTtcjQ3DQOmOTuDnUsWi2AiG2eNyQkU googlesheets4/inst/WORDLIST0000644000176200001440000000123614076047017015205 0ustar liggesusersAPI's Auth AuthState CLI CMD Codecov Computerphile's Datetime Datetimes Feuille Gapminder IDEs JSON OAuth ORCID POSIXct SheetN Shortcodes Timezones UI UpdateSheetPropertiesRequest UpperCamelCase api auth autogenerates backoff behaviour bigrquery cci cellranger chickwts cli cli's csv datetime datetimes de dev funder gamechanger gapminder gmailr googledrive googlesheets https httr httr's js lubridate lubridate's noninteractively ny oauth oob pkgdown pre programmatically readr readr's readxl reprex reprexes rlang's roundtrip shortcode shortcodes targetted testthat tibble tibble's tidyverse tsv unboundedness unformatted unskipped utc vctrs vectorized withr xls xlsx