googlesheets4/0000755000176200001440000000000014441243302013023 5ustar liggesusersgooglesheets4/NAMESPACE0000644000176200001440000001130114440453710014243 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(format,sheets_id) 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_oauth_client) export(gs4_random) export(gs4_scopes) 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(lifecycle,deprecated) 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/LICENSE0000644000176200001440000000006314406646454014047 0ustar liggesusersYEAR: 2023 COPYRIGHT HOLDER: googlesheets4 authors googlesheets4/README.md0000644000176200001440000002470614440460716014324 0ustar liggesusers # googlesheets4 [![CRAN status](https://www.r-pkg.org/badges/version/googlesheets4)](https://CRAN.R-project.org/package=googlesheets4) [![R-CMD-check](https://github.com/tidyverse/googlesheets4/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/tidyverse/googlesheets4/actions/workflows/R-CMD-check.yaml) [![Codecov test coverage](https://codecov.io/gh/tidyverse/googlesheets4/branch/main/graph/badge.svg)](https://app.codecov.io/gh/tidyverse/googlesheets4?branch=main) ## 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("pak") pak::pak("tidyverse/googlesheets4") ``` ## Cheatsheet You can see how to read data with googlesheets4 in the **data import cheatsheet**, which also covers similar functionality in the related packages readr and readxl. thumbnail of data import cheatsheet ## 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 × 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. #> # ℹ 619 more rows # Sheet ID read_sheet("1U6Cf_qEOhiR9AZqTqS3mbMF3zt2db48ZP5v3rkrAEJY") #> ✔ Reading from "gapminder". #> ✔ Range 'Africa'. #> # A tibble: 624 × 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. #> # ℹ 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 × 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. #> # ℹ 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: 1enILX4tYJeFEJ1RL8MsGgDRjb0NHTdm3ZD92R2RMWYI #> 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: 1enILX4tYJeFEJ1RL8MsGgDRjb0NHTdm3ZD92R2RMWYI #> 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://workspace.google.com/blog/product-announcements/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/0000755000176200001440000000000014440453710013603 5ustar liggesusersgooglesheets4/man/gs4_token.Rd0000644000176200001440000000233314275601106015770 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{ \dontshow{if (gs4_has_token()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} req <- request_generate( "sheets.spreadsheets.get", list(spreadsheetId = "abc"), token = gs4_token() ) req \dontshow{\}) # examplesIf} } \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.Rd0000644000176200001440000000232614440453710016124 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{ \dontshow{if (rlang::is_interactive()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} gs4_deauth() gs4_user() # get metadata on the public 'deaths' spreadsheet gs4_example("deaths") \%>\% gs4_get() \dontshow{\}) # examplesIf} } \seealso{ Other auth functions: \code{\link{gs4_auth_configure}()}, \code{\link{gs4_auth}()}, \code{\link{gs4_scopes}()} } \concept{auth functions} googlesheets4/man/sheet_properties.Rd0000644000176200001440000000275414406646454017501 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:drive_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{ \dontshow{if (gs4_has_token()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} ss <- gs4_example("gapminder") sheet_properties(ss) sheet_names(ss) \dontshow{\}) # examplesIf} } \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/gs4_oauth_app.Rd0000644000176200001440000000122214407071424016625 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/gs4_auth.R \name{gs4_oauth_app} \alias{gs4_oauth_app} \title{Get currently configured OAuth app (deprecated)} \usage{ gs4_oauth_app() } \description{ \ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} In light of the new \code{\link[gargle:gargle_oauth_client_from_json]{gargle::gargle_oauth_client()}} constructor and class of the same name, \code{gs4_oauth_app()} is being replaced by \code{\link[=gs4_oauth_client]{gs4_oauth_client()}}. } \keyword{internal} googlesheets4/man/sheet_append.Rd0000644000176200001440000000520614406646454016547 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:drive_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{ \dontshow{if (gs4_has_token()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} # 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() \dontshow{\}) # examplesIf} } \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.Rd0000644000176200001440000000234314406646454020254 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: style='float: right' alt='logo' width='120'}} Interact with Google Sheets through the Sheets API v4 \url{https://developers.google.com/sheets/api}. "API" is an acronym for "application programming interface"; the Sheets API allows users to interact with Google Sheets programmatically, instead of via a web browser. The "v4" refers to the fact that the Sheets API is currently at version 4. This package can read and write both the metadata and the cell data in a Sheet. } \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@posit.co} (\href{https://orcid.org/0000-0002-6983-2759}{ORCID}) Other contributors: \itemize{ \item Posit Software, PBC [copyright holder, funder] } } \keyword{internal} googlesheets4/man/gs4_auth_configure.Rd0000644000176200001440000000602614440453710017655 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_client} \title{Edit and view auth configuration} \usage{ gs4_auth_configure(client, path, api_key, app = deprecated()) gs4_api_key() gs4_oauth_client() } \arguments{ \item{client}{A Google OAuth client, presumably constructed via \code{\link[gargle:gargle_oauth_client_from_json]{gargle::gargle_oauth_client_from_json()}}. Note, however, that it is preferred to specify the client with JSON, using the \code{path} argument.} \item{path}{JSON downloaded from \href{https://console.cloud.google.com}{Google Cloud Console}, containing a client id 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.} \item{app}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} Replaced by the \code{client} argument.} } \value{ \itemize{ \item \code{gs4_auth_configure()}: An object of R6 class \link[gargle:AuthState-class]{gargle::AuthState}, invisibly. \item \code{gs4_oauth_client()}: the current user-configured OAuth client. \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 client, 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 \code{vignette("get-api-credentials", package = "gargle")} for more. If the user does not configure these settings, internal defaults are used. \code{gs4_oauth_client()} and \code{gs4_api_key()} retrieve the currently configured OAuth client and API key, respectively. } \examples{ # see and store the current user-configured OAuth client (probably `NULL`) (original_client <- gs4_oauth_client()) # see and store the current user-configured API key (probably `NULL`) (original_api_key <- gs4_api_key()) # the preferred way to configure your own client is via a JSON file # downloaded from Google Developers Console # this example JSON is indicative, but fake path_to_json <- system.file( "extdata", "client_secret_installed.googleusercontent.com.json", package = "gargle" ) gs4_auth_configure(path = path_to_json) # this is also obviously a fake API key gs4_auth_configure(api_key = "the_key_I_got_for_a_google_API") # confirm the changes gs4_oauth_client() gs4_api_key() # restore original auth config gs4_auth_configure(client = original_client, api_key = original_api_key) } \seealso{ Other auth functions: \code{\link{gs4_auth}()}, \code{\link{gs4_deauth}()}, \code{\link{gs4_scopes}()} } \concept{auth functions} googlesheets4/man/range_speedread.Rd0000644000176200001440000001005114406646454017212 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:drive_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{ \dontshow{if (gs4_has_token()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} 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) \dontshow{\}) # examplesIf} } 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.Rd0000644000176200001440000000645514406646454016535 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:drive_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{ \dontshow{if (gs4_has_token()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} # 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() \dontshow{\}) # examplesIf} } \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.Rd0000644000176200001440000000374614406646454016556 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:drive_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{ \dontshow{if (gs4_has_token()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} 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() \dontshow{\}) # examplesIf} } \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.Rd0000644000176200001440000001351514406646454016420 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:drive_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{ \dontshow{if (gs4_has_token()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} # 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() \dontshow{\}) # examplesIf} } \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.Rd0000644000176200001440000000356114275601106016321 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{ \dontshow{if (gs4_has_token()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} 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() \dontshow{\}) # examplesIf} } \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.Rd0000644000176200001440000000560114406646454016027 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:drive_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{ \dontshow{if (gs4_has_token()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} 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() \dontshow{\}) # examplesIf} } \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.Rd0000644000176200001440000000405614406646454016544 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:drive_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{ \dontshow{if (gs4_has_token()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} 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() \dontshow{\}) # examplesIf} } \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.Rd0000644000176200001440000000622614406646454016062 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:drive_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:drive_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:drive_id]{googledrive::as_id} } googlesheets4/man/sheet_write.Rd0000644000176200001440000001015714406646454016433 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:drive_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 equivalent and you can use either one. } \examples{ \dontshow{if (gs4_has_token()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} 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() \dontshow{\}) # examplesIf} } \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.Rd0000644000176200001440000000522514275601106021517 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{ \dontshow{if (gs4_has_token()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} # 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() \dontshow{\}) # examplesIf} } googlesheets4/man/gs4_create.Rd0000644000176200001440000000421314275601106016112 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{ \dontshow{if (gs4_has_token()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} 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() \dontshow{\}) # examplesIf} } \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_scopes.Rd0000644000176200001440000000331514440453710016145 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/gs4_auth.R \name{gs4_scopes} \alias{gs4_scopes} \title{Produce scopes specific to the Sheets API} \usage{ gs4_scopes(scopes = NULL) } \arguments{ \item{scopes}{One or more API scopes. Each scope can be specified in full or, for Sheets API-specific scopes, in an abbreviated form that is recognized by \code{\link[=gs4_scopes]{gs4_scopes()}}: \itemize{ \item "spreadsheets" = "https://www.googleapis.com/auth/spreadsheets" (the default) \item "spreadsheets.readonly" = "https://www.googleapis.com/auth/spreadsheets.readonly" \item "drive" = "https://www.googleapis.com/auth/drive" \item "drive.readonly" = "https://www.googleapis.com/auth/drive.readonly" \item "drive.file" = "https://www.googleapis.com/auth/drive.file" } See \url{https://developers.google.com/identity/protocols/oauth2/scopes#sheets} for details on the permissions for each scope.} } \value{ A character vector of scopes. } \description{ When called with no arguments, \code{gs4_scopes()} returns a named character vector of scopes associated with the Sheets API. If \code{gs4_scopes(scopes =)} is given, an abbreviated entry such as \code{"sheets.readonly"} is expanded to a full scope (\code{"https://www.googleapis.com/auth/sheets.readonly"} in this case). Unrecognized scopes are passed through unchanged. } \examples{ gs4_scopes("spreadsheets") gs4_scopes("spreadsheets.readonly") gs4_scopes("drive") gs4_scopes() } \seealso{ \url{https://developers.google.com/identity/protocols/oauth2/scopes#sheets} for details on the permissions for each scope. Other auth functions: \code{\link{gs4_auth_configure}()}, \code{\link{gs4_auth}()}, \code{\link{gs4_deauth}()} } \concept{auth functions} googlesheets4/man/gs4_examples.Rd0000644000176200001440000000230414406646454016477 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:drive_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/0000755000176200001440000000000014407102342015242 5ustar liggesusersgooglesheets4/man/figures/lifecycle-defunct.svg0000644000176200001440000000242414407076245021366 0ustar liggesusers lifecycle: defunct lifecycle defunct googlesheets4/man/figures/lifecycle-maturing.svg0000644000176200001440000000243014407076245021561 0ustar liggesusers lifecycle: maturing lifecycle maturing googlesheets4/man/figures/logo.png0000644000176200001440000011603114407102342016712 0ustar liggesusersPNG  IHDRޫhgAMA a cHRMz&u0`:pQ<bKGDtIME XIDATxu]չks2 @)Rw޾n[F m!&.c3sfb3:Gps*G`ȵܠOOKeo (A|.'4) @u[FgWy<'3<N oא>Z?$_<G8~>Lua?Y @H*@c@?tIJ-Pz$puF,bHM(Ip,`c A<^ScJn7j؎f3qCZ@t) Tsat a@^ @c:cDR{^#) Y2 |Pkm=L=z6sJB͜jmq7piFWxp > z%y >nX,aC!)A"Qe%XcJkC׏kafO[:#DL;)p]B-;lHء sFHSBh;vXI-y _a} 3PȦ#jhXC$a,׌%~^7m1Ag=":!p :d O[_3P4+MzT#UMk<{wLɏ*%W+VNAE)khiћ >_SP4yzTq#/G±+%p 8 )]$Sz$p$ DZiҏ%i~c0i~)z-}lo75pꕗiQN$ž:'qCۓ)ptXV }@n 8|mWםw;CHStEᥣp-񁡬8Eѕ$8֬cDPA|DpY(%yd G>L5R7aK=ml3)uu%e"#E#:I5#2K2c-NK;o`"5 @03w`|Q{Nme KlCl{-Xg+67Й|? 13MJn~^#6ᩃ~hbxs#L7_J;YIHՕ C1yl%\uT/#ܞ1Ǖ/ >೜3<X^鈉6vv\jF'I$ji~Uٱ5Iɨ<;Nn`<%-vr|&#"ȍ#SepP@~C$i~v )/bYBRFu$e)O`I}fȯGb+n23'_|#A?f`}Hl빯UKm7O$IuNm>zM̈́ygMe #w9[hCLMUA[n c29?RJԎb7.Fˈe!T EwI YHJ&!r-s"/y,8ĴŵSiܨN{3CI:(<UuYL-%|5:t8F:҈Yqks%x&#t'XYy|g&H(`8JL7 l9xΌ$i~KY9 !BFi1Z•(Ļho]u:/] :Ɵ.0y<3(VKQuN s,Ѓ\$84 XDȠ ;! 2kۀWl{~J[ZAk B\ G_zq;>ـ~&f_f[Ā;57 0pHha חHw؊܉  `|y\$i~eCVxlmT{N0 ,\<̟"++dm-ڹ} gL;og3boePe@H=`?Sۢ-FOO'lp*Cr5p_xq3z>Y+~j঒ua:XbjL(8cm<_O=q"!tj8UK|>,]_lԭeM;Gمt5rdگm#pA.Tg?B"mqxG/Bl=4Gx8ԇJ;%Iʵx{濿HN(8=Ti=@?nWggW^^ i3/<صȅtP6)"ܓ@rEpM1̍c'$?ܴQw2}r)73@&CU^o?AgU걠${O޺(+߷}ⳑMY˻mtGQ5@L?dUSbI7_si}Foɔ3r{HuxZ!fxqu{M|Wp=7ln<NǣN~OPnۊ#q;aoG{CYPadH]͔5>?tG_W㇊/.BM&9蝴4M4O~E|ߩzUWͣ[Cfs@ò,0ĨGsTP() ȑn6 5$ g`>|ȇ_-ؖ`s Tm#l0\8|3mYrM!z*,wDZ/`ꗨ4mN:͗Mw(pl,A%0q2PG*M-mp~_1I3֋oǶ݌:G),\<11[$z(t5&Dom߂?ʻ .Ewg e-G_\?w#yM \ = M9vb/%9u~KC<]c?WAsdHX. p&7Qa$M5Ɔz=7IBEi34suASMYogJGpR'/7W;t(Ϝ:Y?y*nr# bO2>"AxѶV_ ^mІ.7vrO/?N : xS\)aw;p'|luD13fIIz6󶫗lݶe Oo=ҝ^,S & R(귿3on$LTؙ W0^i{INg|w;liCAl3{$l:#ɑ)u`y)0cאI ԅ[au=DO0P4E#XO3;˲xo3ӯ2uKl$mj'V􀿨HF$*-[e1,t5a&p8mOWa߁HxUF&B$.V ;z18m nbN|:XW~1۸; l1ڑQ[{Kiffei4!r%8ྜut6jum1\^A*vBQōv8k;USTwvVÑsBJ¥$QugLDOj|屒좌*&o**z#~ƺNeY<椞u5$SR{5/:h {_m pd!pnO`%COĤ$ 6G0 #'C](n.iC ,;mal<$a_ekIF Knj!l{&1f)1+N^MZZ㬫 b i yQ#1ֽUκá 2݅qS`85j#9~ ޘEo$RcbEbc27YPRx}\] Ou[\͉߂]gͪFyڴ3/omʏH0BQ,3' i~|pyKF`q&ʶPAq´,UQS )x,(;2-<^[%?nNlK2/ρ={D~^}旴,'ċ{kao'-0'/4֏DJYm뭜\lba `='bqOI͑*Bw…jWWhH4ӤX шRbZqw`? ?^?H[L mLl#H %_L۴i`H;n~Wt;ݕ,GzB\/!CIcVA2Yz^2Mv8 YKl)M*}'*i27"ugƫfW1L 6=Mb͑+2z $,~ j\+R;๤max<7 ߊ]ǫ>ņ52KmGonnvϚ5K(BgG;af6 wH,#Neq{eǎʜ9s)5!'L1C? =}oO^L9'Z[7r;D l [(M [}7JvSC旰Bg}xDYle =feen lHT[L0( %*c{өLivTGz.XqlcAc|{ g@nmAoG4+ӰY복~aptKч-#QzDY~CڵK슫LE:ڤ>sRDBPTb-5=+7x`J7UpWBN19Y\6lUUU֤IrޢrY)AQ Vmo?Dgo.9o>~_q_LvJUToZ)ƫS齃>–j pY4Jll R?.ӰϑB/U\ª8ҞƫxXY#pXNr_n5GpA1Y}NC,`ikO0diȑ䤑b'P22xR.RT͵g^Pv٣<7;)]W}&rPT_Oq}QޯK/dشiwӦM3ge}%7"u w#THM;$.Ii ]c,ϙN `E~muvnYޡ:Ǚ>,ftI~^Zpux姄8'b(//'''ŋ2إd4^tXjHvMwh*R%>r獼-yoؾ}RQQ7 9t7tiii~~oqB-EC5Zq cNގJ;3E5igf[B<pf֠dQW#?smo碋."??]kbAOX;.@j-FQ ۍ'ŗ\eY=yi_Jl"dX\2 ކ=ۙx!_;襤 _#<‡>!*.Rwq_7bq ~iV,x4bF*株̛7 &ii4b-fwWYQqcJ9l;nԶй`ICXWHca;,Yrҏoذ;3;3|@ @aa!>('ko~µ9GAV,x\IꪫNzrCM$ "4'qQݶsI< (X/נɊBXSF@Vz~Ơwa*y{ؿ?_|1shISS>(nbܝ6B!T,^vwYUUUr*(u]?~{./+_ \rI GA dKKFcea(й5CCz [|w\y'Hkk+_җz ~znVbuuu)&Nhb1+)PWTu< LccU7J$m{G?J]]̞=[[hԌb4M~z`Uwe )c\*0c-CQ+/`Lu!V)Ƒ6-x;cO k׮ZWW[nZqqNyEU`GT=X|ؾ]:u}v?yضmb$CJmmمQIJ5T: }t?l%c'yӒP2sfNcԩݻb~xz8NtEH4<|0>O+))EEEb֬YJ J彿мo2G)n@zpk Nxч⣍(&<ũZ( Ir:|kܓ߿I4ڪ !%???;/Pl-Z5\Ô)Sz>Dx6<3W²MBqi/c'[(YY~_455 O=Ů];iP፟Pc^KY2{*: '>2#u)iȪ qL;~8^#??|Ʋ,eW"/oY_EZJ&zf,Gv |gd+>r\vEIL{E@Jg!a+Sѹ}LZWVJLDNhFjkkp8^ɤeawBb. ]r/K4MW{{{433ӌ/}םӭcY$ۉGxu{OEo h.7ð9iE=RLTN >S+WJFTUUqF6o,ouuueYt###lkkst_,NJG,E? 'LI 7nh/~1OUUrlI-#c\Rl?7} DiʼnSXt'Wop_~ij9sPUGsO#G>/!f̘̞=[rXthkkJXF+qZ+SRZ[=V]mhՄ"ûqnXjx轩0" 1ЇO\ٞzUZ7}ߢɭ*֭3ҬLpBU[[+M&W\.־wHr*UI!4A %%%rݺuFR |Ⱦ9dUA zo* F_>iJ]&guz)m!M]*.=ACCuhinb=ֈ{Ųx2eI=!O=ST˗AĂH˒" 1rd2`EJ8Eϔԇ 'zL\qdӭ6>׿۽eAD`a7-1spOB a馛By˗/%FZjoHw#˸s{*:3V4(@2fO~V2L\fZδb ҤVrss י/wFK>r YEek ݉9R֯_qE!p(fin@'{gv~bXHvSQ931P2[%%5qm5/_.bM{j7(v~w甕rd\XF\vk{F'_nipOʁSŸHɄQw[3ЀKP3ضmeEQ%F;*U(`RJ\.!fsq(UFG &t( Uࡷfx>4ÉѼu˜u5ӏpՔo>ZZZ,EYqHTʡCD<t! C&Kʳlcz!Y:G^{,I3upRbsc9@Nxg{P^^N[[Ǐ' b:ϸe 0iNf[ bvMp885ˈvRӳUUbf"Bo\|ɧbƒ6`ۉ-R'xq`1ҷMfڜJP],Zz~;w%!.qZr0 %OSkՋ-ZP)Œz8pg GC,dgTh ȸ{i'`rj#sXmht>\n%%%2ʢba[DJؓe04m;w{*'Vh4꒺Áۡ 3ճqS`1aTx?/mXpxT+N&9wx^iV3K=%So:)"&Qr&P7pBEaaRSS#VXٟ* lHP)-ؾl2? A5hH:?h{\[Sr)l2C`gd‹7!0aO}$VO}]MqƟޑԇ>6ZSVHEb$? /TI4b&}]/j2I/Rd)vqs?b!HFgS mL) 3'd2Xp$7[J&M 8X'2:H;h)EOنEsJSuYS=vFK̨ n X⫵b"+jin:;UcεfӃXRҖVaa 7@RDIeƫYp!~H8LmXVSJsAM3&^j<ı,t"ӤGn9DuGV']5hX ߿L)^tA(x2륱r'9U;ϽQS"3sH݅+-'~w[laB ,k ,,Šc ~!uuD\&l2ޤ׍ex1˚*RWWSbQԇOچEwxSk(Bd)Co?zh;VnJ͆"H6nH0N*R1́-96w/_~ݔR풖i5UF:;iKo"eGx5."Dk<{jJ' Gpl7nGjjjtFګ{O?󬣤$x]Q5% (hiƍozSֳk.%2"hȊ "ڥlݲ;???iXCGKhRo8T)ԏ62X']MƟH-za;D>(<-foذ!#33T"---~a5IMPAhBQ[W>3bҤI@ M5)PiI5VO 6 -7&|eP>JG^%o3Iת7l"s .lnJh"ԇG?|gVɩ:ꢋ.2n-Q cNSTѸ3PzdxJS*`hM7޽{ͶVSeH=Iu$Αʭ&]MNƇHptҥClu MMޣGRJ#$9I4PH4T__/躎Ê(ј)jSYL6+Z>< p:EnF@:eR2P1N-[eeejsW0'ъ)z︩kTc$]c6Y%ias;O߳gwv#//Oxv4du!w{{;a5' 0X)4w"( 2"#qڵkGDsy+&1biࢴEf?jdd@X9JK#gs綴tz<23HW*mf՚ #'>%AӒR{8T(Hl۶md¼\kk1@HǎY[IKO-]*]HVL]@uI|XpiVB4)}7*MTg]ѕ+WJEQtrR#nؕt[+$H.2Rw6e%I^IزLed%\wXl4Mzj >:CPN&2&U='p IcjJá(1eԲcǎeڵ+(tvxr,BWff:l5p4EZeeVQQòT͵2JD'MR"͓eJ:y[T5 ?}jJ}3ʜ|5kD'NK tZ@s.V3z(Ņ VkJϵfϞ'%]AChn?2qD,Kh远UvX{+ތޛ"`C1YMcjJÝ#IO/QV ]}Q߯L\v"AR=b666\cBit`0 'MX^x(TU2Z\j(fϚ%u]󉌌Lta8 $W#7RM2 f}w hp1.7LBQR}Lc9 (ؼy3W_}5O<,It5XSuhKcccܹsQ/'42#7ڀI&"8%]},ݓp*f5)p`Jp^^MD*rؾm+/, (p58Ͱ0K4joG v4Bp<3nZvbfVVU^Z j(Sxnmő@ *蚪 Oo^ 1v}Ef m|u3N=(92zqJQtc7]n6mKeAw- LR^^EݮÇ1M\*35G>&N8I`wwػw1|&RRUg"@gAQ6mZ֑#GRJ+.|2v)I7>Fzo(8n4J:4~ }s5XQ"^|[LXx=L-qҴ/Ř,GKss^ʹhKEu&4KdO=ep k5k;''[7˚9{NQ?pS8 q *Y=Um8qdauH?OcjJ6N}X(zUDzݺ[n+";;>R/TR{Ky}k.u!&OtӺi" GQMb?q' +<(cyyy@ 5K,$ b"nI;no%H) @u(P/,0jzoO6,\|&:|z!}S+8X< OV6m$F鄊7HͯV&vkJ rs)Z"yWwS1m9is.Ñagit}g"ϧx43,)q>~xR\Qy2g"qΣ&κhҹ%B]M)R'CЇW!WϞ ރUU>O.B]Q䲤w6gʛ[vs gdd(xNv Kg_9nu6x3nCsY̙=KEQ8ȸݙܗEuC[`ÆRqkB 8pD^{Gv_YH)0 "SQӋ T=):a}Kb;wq:X[[yyy8p%)] ՖP2k1޴tQQc߾}>|XjΒsIˤʭ; jXOE_{KʬǬm-( Ξ3'СC瞓B(RȄJ#x oڴ):UA넩 }j^%lm3"ƙLtnVhEC'94?̅}n.W"L&H/roذzΜ9r߾}L4S'xᔹlȵ5<^ 5؎F~,ph>Tpg,{`, o{,ytimܸuRA<W, 5o}pIIIq*Nh2'H H}]DU5: cjJNNn;>inXvdi'-M){!k֬-"--R1E1Uӂe\|gsrrbPHL:t֚jFQGc;GX8 [s{QH/~XYYYɓ@84 ehED: 9sfeM}CU \,Qh.[9i4]M''ku6a忘m_I^ BJ h~;wHKϠد]mI.:Q]Px<\vrXD*Il9/YPXHբtEOUsH12B@"Nt] gCi<""ܟFWS> .`Vsi[I *ٓy+)**,5::b Wp QZPZU"`kJԎoSy8( ⊏|F3orH^{ |=//]ө>\őM+ryӳYȟ_DW4cڄ (..M}W)ğG:֚âlmǩy}#NbOEjZqӕX$Jzg]An+==]ǚիu7y<ݥ(n\zS?)h̜ʼdιnpWʊc+oB#%Ѿ"Z eɟD92pzd!Np5D޻isdT蹕\-29RoYV!?{j)%v1咛 njg~:g?yGc;7jvH mMS>'4 mT˴ r@v*2SQ*+'Y:-_^lG3ߓXsGҹKo |Dq2SF,fιPwxӝzOq(GaluCeoď}nDw5%|1\ vuS\h 3O?bŊQƓ&MR5J$,teWɜixרgy|ȯL/vVko Sz[[[e̟ȄT]Ӻ"yi+neKϙVӑSǠWOO&_q`&4\8e|L)ء[drG MNfX+ew|\\sӛϠ ,p"sp*DXʺ{.WJf-nzre;@ -iC{"B7=7gUƍ}v:7| dʋ[liPn_g_PmޚCʖ>+s29\^НÇ#P^>@ZP[)]+VߌHP$x|Q.8~nd6=-Ȉj{Cԝ`Z͞1ו7q~KQ5#[;#>}ͼ cEcDxnkUA!ۗ<N]:ih0C,hS-G6Qw CnY*-ȭ,6? ^} !7szjL_qxE䞟㭭x67=s&]jṄy!\| R6Q:k7\(l>EU#%3v辖 .L wuۣES:ZF5[ma{vMYm +d1VyiѢ7dԿ&uy w@{VX7)P4,g`6]ٕ  oyI4~.۷oh9M.¿~@noyN;??xҗ=W]ulkku|%bYzsnwOBkk+۷mck\5TrdII""?7@^r|&MǗ_pӻ=1]f(kl^f\o9xgG_.0nffyvxgt޼-&U]v+p6ރ'IGC1벛_J[/R|FOIJ嶪 ÿ?xN*=6]?᪋,׹|5k֠;鵵alڴcǎxQHND T@v;te߾},y`{ YTa%m 0m&\W'-NKN@t U'߽pʩI4bfx=U\aF:x$9KvI8ťA>x>KN?>tOz,:OIDՆUUw0e5D),*S`ߚ'9czѝw`o)M!ŵiƎ ] vGZ^N]v%H *-Ljşxbn:H,ٰa[K/eY<쳼jʎ4 )TwIҟ|Q߇>!RQZ̆_DfDZ2YoVw<:"]B 8;.VLM8amNjlҌx$UQBESni ޱ>X4exh+KI$3#Q||9E$I0 ڽSָ+b4r˧ōhJF4%KQT}[ea=a*y?S` W'Sh~4td[5"PDieW1|?--;;;X`k֬1/^l~:U䗿]x[_?9^g_ d2yBcnjȼy BUuְ+ l}+9 UsNr}ZKK\:W2%~~aU.4 U9M[ZZx饗Bo~e Mь۾XnmtdfD]Ruv=e?I 2+OZR3Q||d_ŗwmVb܅sKESvJlz/ďʹ$x3Dke]Z]^ hLxt1yhN7BQNGO hX8HVD& ׂbeUo]-_FvO5*˴KxyûۿG)jfe..dnTr*:IˤIy'B3grx<=#=G42 JWX l`Ĥ8kSqz~k#cr H+҂mLM/EJ\ٻYϟ8Hw'& &ͤd|_|p s,l9ׁeE#YO\e<x)f .k/%drCK np{KJf.FQUt_VbD##G8/ÇZsMFaQ1]šw.ݮޝ'=/i_(I;;']2 :{ϩ7.^ܐRJ+d}[8BfEMQO#bƠ(>E0w-dg(KOP4uT|5%A IÏQLg<^( :ԇ?%˖rǿI9Rj3u!PT{3) ˃t ;g93eoۺ-~!_=Nji]ߵL' O6pWP`8ÁeWpy}[ 4)A s]<2|Pğ] (2= dlCuhAk\3Iac&D/Bws:}bİ˙SƊ"VhN9Рn1Bs"}mu?Lw/+#33 ҿ:^pXt:Okky !4t7g;|9X?5M#N<E W~kQX.Lm啀\8d-kMV򵅝 t$~GZp-P}+NϰYG"qZ8=_; 7Mj:):X!EQymoq;*8uj3ݡZlTEXDCA܁ Y2cJ&W9*"ݝ2s@C1ccN9^Llճ Al'G:"Nyr&6Uz9s<~MU n|_{W3gʞ;w.EܰbՏv/A'Oޒ R,KSfDISi>—#yg~6$Fh=B z IU3/e8ӑR*gNa #E}[ϙMŢ 3.:G)YpYiKs4s7tmZk#BaX%-E(jϪIi7 4UwOs8)_iO9 : cA@CZN߈ԋ 6&}iyFhtcȉt#L .̂7+vソhZvc223+{kTJ2  eWWW4Mbqî'DZLJ?EN;ۿۃ$M5G-|9{L'ɌQ#T]OnD'UwbO|Ӧ;q9}{- O)&)CZRj^~e.@)ulqaF}%[-2):1!%A[>xw>mrDff`!-b`ɳ\OIaAtUUuv,g ~klk&َ/;ˮ]eЦ8Ttc)r7Eoz;s]MӦMPwJnc+FN~ʼ999Yqd9GMt! Αk 5$h}TX*(J tvlMRM}8 z;={DqG9"?iMka(@,1ƃ;bQI 8PUE'"P{3^Ӳ,MbBJ.9(jd'M T%;U_HroJ |݆asqCRw>#Rst4]{{9=LddN?6 Eh!BIkQ Jq5xx'%܍itN_>^TOюI ŷeGCUDŋ2A|7VW=(Nl`ڧUQQ^QP_ǁV2{bǜ/e9s#Ía SY=S{ M~_|󋪪}y@!%ӲN>u pShf}ÎGqT%@gdHYF;CYx;?Gng`Lg 4jL1vx0[KxL{]JS^N:ϴKӧOwnC!2?u PUMd@[Zн 9W\R|?S1"84$)H:kz5 sBd8 h'l= _8G0yk{<ݙ3ahV-muGm?~|ҹ'B"`6^0-*,形tmw4M(e]_=_-XsT e?Ru7pm[RwQ2c/c785)6Φ:i&^I.l r22O[3kƥ}(^pEkpw[[J;K.Sr'<ί(vIQH[u'> tHXcl'm3S^~;O5MQ:kسskUz׻ݮ 6X.2qB~_!V(]- TѲkEPHB!iYW[>iF i>vEQed vaz(Lp5W Ͽffx=L\t uo((v68tfuMx7=;8w,gT𤍜8-2<9T'b̺`9}֗;(Lٽc{ƣ6###֦{^4Me)|+7bˏ[V??HƮOo;vM6-[NWݡGKuQXs~ᒖe)PgԈԎ`,P;6eվ[^ѲK&Y%CIJK+lTvÉ=U< #VjoY#Yk7ݐVŠ9*W2aE7.:^^?WdL,}?*Pudeew^nz32272w\?8XL:ymǿ#96ZJ(,4ICs:f?+djZPTҲ qdd,_\4W:=֜;6msܹyyqI+AjTajV]SF"U/0Nzݽl ?Ƃ-k!JX՜,N}IK?y ͷhhh`ӦMʅ^hmܸѺꪫ~!N_~InټIh'/W~{OM;5 4Xٳ s1a2sm/;ݺ;tw#f^zSZWsZ{mW)V4}9qnuk[BN7{  /~B|Jg73XC}VXy,&:F"K.cܘNG:Jg-oQ3y~Q+'3;mX+ҳgv>6m|]~;ߵB¥̿?wq ޓMAi}sO3 W;3^>bPWEPntx= 9c6 $x-^2s"澝hPG #.B"ӟSwO֭3"M+\$,?o~ӗ_{96nx>{eVL{}vqZyש]KVIEOes m#'Qs0Jf,UM5e:c`(lj'f.t ԲģMv U᦯CģaZZsUӅ4M]t*vvO^v?U/X%w @c=Q^ {3sXx[{Jw[XKw{3S 7/3y8=]r:M5 eB v!MCTv]D|j-9Gvna`W|N<{CJ,2ANK9XIwK-UVLfϖ`wKӓ^Y~VuјRRG4 M '>ܦV0aEM/s+]J^ r@Qˀ `t7x|(m\Rb #hN_9SR/},i_7` R۹t$%(JGM5H8iݛmĢ!X Vp*%ǙQo){B tiaP'*,\9˝&|Q4ufI'p  -XXx]%YBFZ~'| yjo! σ< t2)X=He)^rKwF(h@3U4p2xB\ހ?Oid-VɼNr):5GX{+,4n'E%%u ɮ-y:O%!RBj9[qzL]`>tBg͕x/Fh^uh&Epeb5$֛LEe0,y< !pz²,K*e"-g7q[8ف̬=cL#'.{3a~JI O[(M#q&<$;uSQU'7@#_N^,py]c wAag#Yqx2y+ g,шS6IFR\wXS4loFJp:BQi90w~|`ٜ]ǫ ?hHHO [wURZjKUDbZW2y|+IB}iģ4HyU2QD]Β,PgkاR}n > Wt:u/IL!zjќ`KAGߎf,mtFaY )V,i,iY A$4fiY!L#HP[)UM;-eHi%DJ)'W w\( i)f E(RJED"",E(jBxG"E29V (D7O)-KZ -XBKtU"iBRS$RQ5)NSWHUw' J(TuU!,0UCs:EQ>B&TT5yRQTBB(%ҲR}**E]{4eykBhB(²LE) ]N;d+{ ztk&CS uF}n:%C*C L˥J ]2%aBzjX]CJY {1$-iIǒe$s*DYg`vӐHSEJ ,@A(7` !1`Pba.0c 'JI~|6QUކlD%Faz-%BXB($ BA(}$EHV<&@JaYEBQ)ZL<}edd[iV\ U .89>E8ӇE門hGy.VY6n!t 8Xİp4-{~˾S(^ˑRyJb'ӡN>B)<̒a{'|pJ=QbBwhڙP4gڒàď q,$1]ԞUD1M|֑=܇8ӋbeaU iJ9Sh TߵHvٓV9ojD&mTTĉBia:!OmPDnޭǡas@NddL3ǚɒ8"BM`/U$N0Tt"H8v(@V9w8HP?7$'')Cܽ+̆wM4>6، L?م3fr*O XⷕT'N'k)Y(A,˄|g=#!^wJk,LkOi]ﺍ-@{ޙ\;1 Up^ZhZܻb&R'{גod[)ߗ,6O>?ۜH|4qT=y,ہ3φ=5SUv{v_H:?.y/r?_m+_5i">$9nXk5uT` NEJ, jms2|i.TN?ǃو~8L#{lP'7V~pyN\M5#NT}@ w =1up7O-홻'߻}*V0N"rWC]iowLOGCƭg!?1{ۀLl_=B3 ~vlWHZh'h}XgחP=+I_[|})ܽ\El^D|žj_t-ǴpW/yy%KUSh|m/'6ڧ"3f?CpE1\VhnIXSoև:4~x'6mۥWwN xT _m`saOU 3)վ>1:YG |WY8U(5H%vjQ r+arRk㞑mI:AwQ6ln tJgFTO ]1^g ZZMb_^pzlA>Cȿs[J+RU ^ ky6uIQK aǐo.0`suA>vqRxkC/O@4?ƻr9!ͭU QQk2 p{> _kF8\ i{: ?(oS^ڇ'"2xbiImÁ33 <5Up׫X= \~@ /6AA8O nгPm3ےɍp>x0LJ-p{VNj=vim_]YoS2aQO mK ϭ ᇗµds4̧t(i??bs5Umi; {<C^$rC( [[A52wHis׵g:q wL>)Ut0u|o"3;oKGCHTwW Vt\=Pkl2B1F%sS miN[=e? ELhlc~~-qi?nP-!6K3jn(epC td;{Ǻ%1Vmp*ssQ6mH(L-ITW{pY1x4XQO9Qj.\b~-8;Q7"+EM%Kw%Zal)Gm#s?+K,ԉ^:XZ^1]?#Qy;aJmi~B"ɑVx347 = a}(J?[!2o9f'd/.^7`Jӝ2#3KI6ih2 Qmv6l%)Eq(6MHȰE/&>p66丠&8TWKlp;dٙi")p۵bH ذsRyL]b?o~ݖde8=B(/U Rd玧ZmQܟX󤄂99}ae2\Qڻf)s/rٙ誰CK;{֠;jҠg*}YðB~OBu˲cJT[x.Ca Q5khWadi5{u*l]H'\W 3 21'1l), ޿}4mKucq huư-!T$8!87T,LX[~Ƈ*˙"^999ԇ૯Ï/ dq9 ͓0_&p 1{>[' 2!a𤬥>cSB|ϓOEy0Tl_л^xTc-s lnŚLM E5SO}3MM?tZq=(5}\k!x5&yf7bh/ ۊ]0?ff#W,mon)m_G9e$ߞ8 ~?X~lqVliqYoond6CMV0-{Ijb8uxmi~-z%C h#ҟl-ME%x[e8>' n-omNג(] .>֑;@w)eLhLs<]y;ߦ9|8*3}ߣٺHwrC\?#>;>1>dUUl7ͱnhI.e,ZmQׯCA\^l5&$Iߑ%ll}BاҼ &9YPd< u7}PUޜ:$c=% {3"υE=h?۱dl-eƱ,e[lk \^׫WO AΊ{|/bZ] 8<4H%U2]0;6rKGD*"m+$th7}mͰ:^ˍށ/.{yU_K^R/í'BDgΜUrcF5M/!.i E)2K.no%^C֐ʐa{6&5Ec7.K"a?wN]GնIDATqxFʶ2tV+HɪYڠ͝16i \?XC-'j{J{-mLMG%8C©;Tަe;mPg8x.27n!뇯iO7EXQ\|zumtn{ah U[%z$[VΖ9 Ɉ1l㊼Θm:J;Aw YJ7Mo{4H{"XsB7\m)SK~Lp6%Cn(JlĒspenyl5'S ȉ|~١#.l]m˷> |c!ʰl{!} !E>~f.ŶVh-a;H٪`S= 4EK.@RA;HY5u wR8*JǎJ]0VܲixR1?+u'yBHqhv_og"CE) 5 vbnHn_\b `v`I`@+/[bB؀Mn)as݈T{e I-UpǍxfF`&Q/͕,;.7CYrn&!X(YgWrYE{ŘaŹ0/f;QAbol֦fIhO1:;1!etu)L&uD]9YpMmQ8lVxy{,V=`̄M vTrn 鉱n'!01~h'ܿ+aՀ,XYD"#JDօ`K֪V"bE,ɲ}ikG`O+<6M30X㣓Gc:0TIdö_^ln/Y!~k +`A}8)鰣+0vzQU q[^Q@m1.$kwt=Eu] ]iz~ѷ{L\xK%βA4ІbUlދ+vVoyO[e( x8]jG:.8\owT½U FJ:oL㨮<^}$χ{^ p:"ez? zS0AI̠413v>*s%tf6> b=j{q@%D%<YUg\k0REq" *wWB 8ڻBRR,IҰJᨿM}@mjXƌىa\)LW}::{51ԩ""1l}71"Űt>d"9:'<f1!t0nDZpv2 tF"fG=YUpT L䄲wPbs!`p7$p t" Ã3[Ar0X$g-QJ0o|dU$$5%P۰M9{WW!ɘ~);/sX0'ox%t2yW/8Zsp(+v@iF{`I(1M\5 RqѶub϶w\4nLyuN`u;tGv4,i!k߿nk d*INY'1e! Ufؕ?=;eֈԺE\{삚kQ$2_|"bE7G&< dXBM^gddYii۶mےmj/ `zH(`<8`mMl xuiIHg72^O2_2>hz9q@Sљ$TU3I2ʑ~v3u27+3yӭ \dQ#I3^ocŠE+y%Gt*$$ R%wSk/*+ʢVVmifӸasѷj1WMœ1*ӏs-^ e&ͧ= CM[ܨiDnkC68\KLjeSVdwnBsTHLJH?s5Ie_'ݱLRo'/졒ZB`}n 4#q"gMN&qŲFw^l*Fm.\xx xײ*K&!5"OWs)$6b#rj3[m[U"ח?>Wd$\-q!g[J%Ӊ& \XfeBBZxXէ/ln$a1RǶRocd)촶/elŷYnBqO~LJ]ܸ@V8o,51H0lŹ.޺G O^5Aa#|qd߲*ǶnbelOg&f wMI!3¨sJvv; {=XF)H.Ǽ@Ueہ$>UD\.X 4xD27T;~Ƿ^C89;W #GE%PzNj}|aב\1#i7,>ʬ+BWc#,Ts}pVi_E|ԜLZcg8:xl89EEՐcX* x4E\&՗&;6JSB䝭`*57wngҟ'qPJj vfMI.Qr.~ RtC^ J*${FZ|| scxn EKx0V1: wNre_,k&gԈ@[H+ xэ7'IE` cN% k:TEPIjPNUaU"M^G)mnX!{lcC-D_܁(Up_ԍȓ2UᄉwUa!3qA̅H7o}ZA Y2 &8Cw$Oٚ&IvSL#Ko_/_} Fgr-_3J<V́E%qO#)G Y5FpR I8B66:ّnu*㜴kreD`:TniuǕ֌rM.M~q*bBedד6DۈM&G̭f/]R?|LbXx 7G=,4ԏMvY%o泹o  8-B&(IrlgPv/~HCrO8I~~8A^׽Xcnu$FOE`U] _ ob~:L[_JXq:M+sIsHO_U&[\,;} Ŗ $^Gս[T?HR&<-H]%m(}:&T 2nqc[H;N\G6JtgI$г}M62e!ol(\3[q)f̩NI\]3D;My6k![|R۸; N$D_zgJ~?^ \+I-M7{< 7LM"\au0U 4؈|9"X&A'rGs˗vcq66O!ڤ- 7# q)Nǰº]Tc3ZV:m7'I{TBo]rΨH^(R$6~VɇGa.ۢ׻IA&~W׵fg1cnx 5";+q{vlūMVV o5ˍp4m}DnZBG=U_̴֭wτfd~ UTH,5hD.A֍nƅlFx 'WgٌU+1ouqK`{_"\nUw&[qlT^R\ {"&[d;|!ibq 9}w4"W-^-cQolZ~w5 !Fz,A2ϑBkd~Ý #.-Ɉl񛸑-bu[<%g!85ecྙpc-~t;#W ohc-G`~8nEbtElUS˦Sa\k6 2# #5d"ŤQ]W`AA.btw'%ub:[?.e~B7dDWa޹>d#yNǰ}VLd͵-pDss'o#"{F6y@؈<x xu:]ygVx&uo:<F5#5!5@ +`-^+rcػ-q6c絑Ӂݜ).Wz&Φ=R!& klN$> q?}X^jP`k2ES0WM*KV2De~~``sǓ-r:_Xe9d @}yXPMrLr‥LHNeFwNǰeL2>ߓe3y.-W\9sqb]ϐV  lRc&|h (_ri=eԔ@YV2?P hD.Gڥ>OM[hMmRQ`/ª.)\834(r\V ̿ ;^I3'eC  kk.! e"8W7nx_%ܚYWItK W)*yElF>Wd}hBe)Kgн|E]7ĹdBC#h`Rey,]kwx %]1BY%rJO؀@1d~~`,VOSt ݭ%H||v`|~iRKzpu) fOpCY,s"Ķ4]6kLZDW&+K(Π}e}Mx7EN'r#Q|e|d~%nFtlBu& n~O#27PЈ<xx\;:b54їvaq$qd1:R Vl8'1~cW %@\:9c摼pi 9CpC9F"[|Ŷ.bo a7VD[SQD"vLǖsU7)rs ȥR&臠 ͯ 5 jLcضIl?cA Av6HZys#؈ lifecycle: archived lifecycle archived googlesheets4/man/figures/lifecycle-soft-deprecated.svg0000644000176200001440000000246614407076245023015 0ustar liggesusers lifecycle: soft-deprecated lifecycle soft-deprecated googlesheets4/man/figures/lifecycle-questioning.svg0000644000176200001440000000244414407076245022305 0ustar liggesusers lifecycle: questioning lifecycle questioning googlesheets4/man/figures/lifecycle-superseded.svg0000644000176200001440000000244014407076245022077 0ustar liggesusers lifecycle: superseded lifecycle superseded googlesheets4/man/figures/lifecycle-stable.svg0000644000176200001440000000247214407076245021213 0ustar liggesusers lifecycle: stable lifecycle stable googlesheets4/man/figures/lifecycle-experimental.svg0000644000176200001440000000245014407076245022432 0ustar liggesusers lifecycle: experimental lifecycle experimental googlesheets4/man/figures/lifecycle-deprecated.svg0000644000176200001440000000244014407076245022034 0ustar liggesusers lifecycle: deprecated lifecycle deprecated googlesheets4/man/figures/lifecycle-retired.svg0000644000176200001440000000170513635427346021401 0ustar liggesusers lifecyclelifecycleretiredretired googlesheets4/man/range_autofit.Rd0000644000176200001440000000555314406646454016744 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:drive_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{ \dontshow{if (gs4_has_token()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} 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() \dontshow{\}) # examplesIf} } \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.Rd0000644000176200001440000001731014440457641015621 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, subject = NULL, scopes = "spreadsheets", cache = gargle::gargle_oauth_cache(), use_oob = gargle::gargle_oob_default(), token = NULL ) } \arguments{ \item{email}{Optional. If specified, \code{email} can take several different forms: \itemize{ \item \code{"jane@gmail.com"}, i.e. an actual email address. This allows the 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 targeted 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). \item \code{"*@example.com"}, i.e. a domain-only glob pattern. This can be helpful if you need code that "just works" for both \code{alice@example.com} and \code{bob@example.com}. \item \code{TRUE} means that you are approving email auto-discovery. If exactly one matching token is found in the cache, it will be used. \item \code{FALSE} or \code{NA} mean that you want to ignore the token cache and force a new OAuth dance in the browser. } Defaults to the option named \code{"gargle_oauth_email"}, retrieved by \code{\link[gargle:gargle_oauth_email]{gargle_oauth_email()}} (unless a wrapper package implements different default behavior).} \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{subject}{An optional subject claim. Specify this if you wish to use the service account represented by \code{path} to impersonate the \code{subject}, who is a normal user. Before this can work, an administrator must grant the service account domain-wide authority. Identify the user to impersonate via their email, e.g. \code{subject = "user@example.com"}. Note that gargle automatically adds the non-sensitive \code{"https://www.googleapis.com/auth/userinfo.email"} scope, so this scope must be enabled for the service account, along with any other \code{scopes} being requested.} \item{scopes}{One or more API scopes. Each scope can be specified in full or, for Sheets API-specific scopes, in an abbreviated form that is recognized by \code{\link[=gs4_scopes]{gs4_scopes()}}: \itemize{ \item "spreadsheets" = "https://www.googleapis.com/auth/spreadsheets" (the default) \item "spreadsheets.readonly" = "https://www.googleapis.com/auth/spreadsheets.readonly" \item "drive" = "https://www.googleapis.com/auth/drive" \item "drive.readonly" = "https://www.googleapis.com/auth/drive.readonly" \item "drive.file" = "https://www.googleapis.com/auth/drive.file" } See \url{https://developers.google.com/identity/protocols/oauth2/scopes#sheets} for details on the permissions for each scope.} \item{cache}{Specifies the OAuth token cache. Defaults to the option named \code{"gargle_oauth_cache"}, retrieved via \code{\link[gargle:gargle_oauth_cache]{gargle_oauth_cache()}}.} \item{use_oob}{Whether to use out-of-band authentication (or, perhaps, a variant implemented by gargle and known as "pseudo-OOB") when first acquiring the token. Defaults to the value returned by \code{\link[gargle:gargle_oob_default]{gargle_oob_default()}}. Note that (pseudo-)OOB auth only affects the initial OAuth dance. If we retrieve (and possibly refresh) a cached token, \code{use_oob} has no effect. If the OAuth client is provided implicitly by a wrapper package, its type probably defaults to the value returned by \code{\link[gargle:gargle_oauth_client_type]{gargle_oauth_client_type()}}. You can take control of the client type by setting \code{options(gargle_oauth_client_type = "web")} or \code{options(gargle_oauth_client_type = "installed")}.} \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. } \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, \code{gs4_auth()} allows the user to explicitly: \itemize{ \item Declare which Google identity to use, via an \code{email} specification. \item Use a service account token or workload identity federation via \code{path}. \item Bring your own \code{token}. \item Customize \code{scopes}. \item Use a non-default \code{cache} folder or turn caching off. \item Explicitly request out-of-bound (OOB) auth via \code{use_oob}. } If you are interacting with R within a browser (applies to RStudio Server, Posit Workbench, Posit Cloud, and Google Colaboratory), you need OOB auth or the pseudo-OOB variant. If this does not happen automatically, you can request it explicitly with \code{use_oob = TRUE} or, more persistently, by setting an option via \code{options(gargle_oob_default = TRUE)}. The choice between conventional OOB or pseudo-OOB auth is determined by the type of OAuth client. If the client is of the "installed" type, \code{use_oob = TRUE} results in conventional OOB auth. If the client is of the "web" type, \code{use_oob = TRUE} results in pseudo-OOB auth. Packages that provide a built-in OAuth client can usually detect which type of client to use. But if you need to set this explicitly, use the \code{"gargle_oauth_client_type"} option: \if{html}{\out{
}}\preformatted{options(gargle_oauth_client_type = "web") # pseudo-OOB # or, alternatively options(gargle_oauth_client_type = "installed") # conventional OOB }\if{html}{\out{
}} 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 client or API key. To learn more about gargle options, see \link[gargle:gargle_options]{gargle::gargle_options}. } \examples{ \dontshow{if (rlang::is_interactive()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} # load/refresh existing credentials, if available # otherwise, go to browser for authentication and authorization gs4_auth() # indicate the specific identity you want to auth as gs4_auth(email = "jenny@example.com") # force a new browser dance, i.e. don't even try to use existing user # credentials gs4_auth(email = NA) # use a 'read only' scope, so it's impossible to edit or delete Sheets gs4_auth(scopes = "spreadsheets.readonly") # use a service account token gs4_auth(path = "foofy-83ee9e7c9c48.json") \dontshow{\}) # examplesIf} } \seealso{ Other auth functions: \code{\link{gs4_auth_configure}()}, \code{\link{gs4_deauth}()}, \code{\link{gs4_scopes}()} } \concept{auth functions} googlesheets4/man/range_flood.Rd0000644000176200001440000000776714406646454016405 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:drive_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{ \dontshow{if (gs4_has_token()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} # 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() \dontshow{\}) # examplesIf} } \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.Rd0000644000176200001440000000154514406646454016170 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:drive_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.Rd0000644000176200001440000000305714275601106015574 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{ \dontshow{if (gs4_has_token()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} # 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)) \dontshow{\}) # examplesIf} } googlesheets4/man/range_read_cells.Rd0000644000176200001440000001052214406646454017356 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:drive_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{ \dontshow{if (gs4_has_token()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} 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 ) \dontshow{\}) # examplesIf} } \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.Rd0000644000176200001440000000753214406646454016256 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:drive_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{ \dontshow{if (gs4_has_token()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} 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() \dontshow{\}) # examplesIf} } \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.Rd0000644000176200001440000001544014406646454016200 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:drive_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{ \dontshow{if (gs4_has_token()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} 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" ) \dontshow{\}) # examplesIf} } googlesheets4/man/spread_sheet.Rd0000644000176200001440000000511714275601106016544 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{ \dontshow{if (gs4_has_token()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} df <- gs4_example("mini-gap") \%>\% range_read_cells() spread_sheet(df) # ^^ gets same result as ... read_sheet(gs4_example("mini-gap")) \dontshow{\}) # examplesIf} } googlesheets4/man/request_make.Rd0000644000176200001440000000574614407071424016574 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/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 client. 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 client 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.Rd0000644000176200001440000000641714406646454017103 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:drive_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{ \dontshow{if (gs4_has_token()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} 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() \dontshow{\}) # examplesIf} } \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.Rd0000644000176200001440000000602114207753213017435 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 "Credentials, access, security, and identity" (\verb{https://support.google.com/googleapi/answer/6158857?hl=en&ref_topic=7013279}). 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.Rd0000644000176200001440000000504714407155113016570 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:drive_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{ \dontshow{if (gs4_has_token()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} # 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() \dontshow{\}) # examplesIf} } \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_rename}()}, \code{\link{sheet_write}()} } \concept{worksheet functions} googlesheets4/man/cell-specification.Rd0000644000176200001440000000350014275601106017625 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{ \dontshow{if (gs4_has_token() && rlang::is_interactive()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} 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))) \dontshow{\}) # examplesIf} } googlesheets4/man/gs4_get.Rd0000644000176200001440000000252214406646454015442 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:drive_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{ \dontshow{if (gs4_has_token()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} gs4_get(gs4_example("mini-gap")) \dontshow{\}) # examplesIf} } \seealso{ Wraps the \code{spreadsheets.get} endpoint: \itemize{ \item \url{https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/get} } } googlesheets4/DESCRIPTION0000644000176200001440000000324114441243302014531 0ustar liggesusersPackage: googlesheets4 Title: Access Google Sheets using the Sheets API V4 Version: 1.1.1 Authors@R: c( person("Jennifer", "Bryan", , "jenny@posit.co", role = c("cre", "aut"), comment = c(ORCID = "0000-0002-6983-2759")), person("Posit Software, PBC", 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.6) Imports: cellranger, cli (>= 3.0.0), curl, gargle (>= 1.5.0), glue (>= 1.3.0), googledrive (>= 2.1.0), httr, ids, lifecycle, magrittr, methods, purrr, rematch2, rlang (>= 1.0.2), tibble (>= 2.1.1), utils, vctrs (>= 0.2.3), withr Suggests: readr, rmarkdown, spelling, testthat (>= 3.1.7) ByteCompile: true Config/Needs/website: tidyverse, tidyverse/tidytemplate Config/testthat/edition: 3 Encoding: UTF-8 Language: en-US RoxygenNote: 7.2.3 NeedsCompilation: no Packaged: 2023-06-11 01:11:05 UTC; jenny Author: Jennifer Bryan [cre, aut] (), Posit Software, PBC [cph, fnd] Maintainer: Jennifer Bryan Repository: CRAN Date/Publication: 2023-06-11 04:00:02 UTC googlesheets4/tests/0000755000176200001440000000000014207753213014174 5ustar liggesusersgooglesheets4/tests/spelling.R0000644000176200001440000000022514207753213016133 0ustar liggesusersif (requireNamespace("spelling", quietly = TRUE)) { spelling::spell_check_test( vignettes = TRUE, error = FALSE, skip_on_cran = TRUE ) } googlesheets4/tests/testthat/0000755000176200001440000000000014441243302016025 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.R0000644000176200001440000000536714207730741021566 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-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.R0000644000176200001440000000464714207753213021776 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.R0000644000176200001440000000151414207753213021403 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.R0000644000176200001440000000116614406646454021603 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_no_error( sheet_delete(ss, 1) ) expect_no_error( sheet_delete(ss, "gamma") ) expect_no_error( 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.R0000644000176200001440000000170714207753213021105 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.R0000644000176200001440000000742614406646454022230 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_no_error(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_no_error(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_no_error( out <- new_sheets_id(character()) ) expect_length(out, 0) expect_s3_class(out, "sheets_id") expect_no_error( 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.R0000644000176200001440000001370514275730406021235 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)) }) # helpers to check arguments ---- test_that("col_names must be logical or character and have length", { wrapper_fun <- function(...) check_col_names(...) expect_snapshot(wrapper_fun(1:3), error = TRUE) expect_snapshot(wrapper_fun(factor("a")), error = TRUE) expect_snapshot(wrapper_fun(character()), error = TRUE) }) test_that("logical col_names must be TRUE or FALSE", { wrapper_fun <- function(...) check_col_names(...) expect_snapshot(wrapper_fun(NA), error = TRUE) expect_snapshot(wrapper_fun(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-gs4_auth.R0000644000176200001440000000476014440453670020664 0ustar liggesuserstest_that("gs4_auth_configure works", { old_client <- gs4_oauth_client() old_api_key <- gs4_api_key() withr::defer( gs4_auth_configure(client = old_client, api_key = old_api_key) ) expect_no_error(gs4_oauth_client()) expect_no_error(gs4_api_key()) expect_snapshot( gs4_auth_configure(client = gargle::gargle_client(), path = "PATH"), error = TRUE ) gs4_auth_configure(client = gargle::gargle_client()) expect_s3_class(gs4_oauth_client(), "gargle_oauth_client") path_to_json <- system.file( "extdata", "client_secret_installed.googleusercontent.com.json", package = "gargle" ) gs4_auth_configure(path = path_to_json) expect_s3_class(gs4_oauth_client(), "gargle_oauth_client") gs4_auth_configure(client = NULL) expect_null(gs4_oauth_client()) gs4_auth_configure(api_key = "API_KEY") expect_identical(gs4_api_key(), "API_KEY") gs4_auth_configure(api_key = NULL) expect_null(gs4_api_key()) }) test_that("gs4_oauth_app() is deprecated", { withr::local_options(lifecycle_verbosity = "warning") expect_snapshot(absorb <- gs4_oauth_app()) }) test_that("gs4_auth_configure(app =) is deprecated in favor of client", { withr::local_options(lifecycle_verbosity = "warning") (original_client <- gs4_oauth_client()) withr::defer(gs4_auth_configure(client = original_client)) client <- gargle::gargle_oauth_client_from_json( system.file( "extdata", "client_secret_installed.googleusercontent.com.json", package = "gargle" ), name = "test-client" ) expect_snapshot( gs4_auth_configure(app = client) ) expect_equal(gs4_oauth_client()$name, "test-client") expect_equal(gs4_oauth_client()$id, "abc.apps.googleusercontent.com") }) # gs4_scopes() ---- test_that("gs4_scopes() reveals Sheets scopes", { expect_snapshot(gs4_scopes()) }) test_that("gs4_scopes() substitutes actual scope for short form", { expect_equal( gs4_scopes(c( "spreadsheets", "drive", "drive.readonly" )), c( "https://www.googleapis.com/auth/spreadsheets", "https://www.googleapis.com/auth/drive", "https://www.googleapis.com/auth/drive.readonly" ) ) }) test_that("gs4_scopes() passes unrecognized scopes through", { expect_equal( gs4_scopes(c( "email", "spreadsheets.readonly", "https://www.googleapis.com/auth/cloud-platform" )), c( "email", "https://www.googleapis.com/auth/spreadsheets.readonly", "https://www.googleapis.com/auth/cloud-platform" ) ) }) googlesheets4/tests/testthat/test-range_read_cells.R0000644000176200001440000000475414406646454022430 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_no_error( cell <- out$cell[[which(out$loc == "C2")]] ) expect_true(!is.null(cell$effectiveFormat)) # C1 bears a note expect_no_error( 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-range_write.R0000644000176200001440000001423214207753213021444 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.R0000644000176200001440000001664314275477215022512 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(10000000, 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.R0000644000176200001440000000205514406646454017451 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_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/0000755000176200001440000000000014437177014017323 5ustar liggesusersgooglesheets4/tests/testthat/_snaps/gs4_auth.md0000644000176200001440000000263514440453746021374 0ustar liggesusers# gs4_auth_configure works Code gs4_auth_configure(client = gargle::gargle_client(), path = "PATH") Condition Error in `gs4_auth_configure()`: ! Must supply exactly one of `client` and `path`, not both. # gs4_oauth_app() is deprecated Code absorb <- gs4_oauth_app() Condition Warning: `gs4_oauth_app()` was deprecated in googlesheets4 1.1.0. i Please use `gs4_oauth_client()` instead. # gs4_auth_configure(app =) is deprecated in favor of client Code gs4_auth_configure(app = client) Condition Warning: The `app` argument of `gs4_auth_configure()` is deprecated as of googlesheets4 1.1.0. i Please use the `client` argument instead. # gs4_scopes() reveals Sheets scopes Code gs4_scopes() Output spreadsheets "https://www.googleapis.com/auth/spreadsheets" spreadsheets.readonly "https://www.googleapis.com/auth/spreadsheets.readonly" drive "https://www.googleapis.com/auth/drive" drive.readonly "https://www.googleapis.com/auth/drive.readonly" drive.file "https://www.googleapis.com/auth/drive.file" googlesheets4/tests/testthat/_snaps/range_read.md0000644000176200001440000000154014437176416021741 0ustar liggesusers# col_names must be logical or character and have length Code wrapper_fun(1:3) Condition Error in `wrapper_fun()`: ! `col_names` must be : x `col_names` has class . --- Code wrapper_fun(factor("a")) Condition Error in `wrapper_fun()`: ! `col_names` must be : x `col_names` has class . --- Code wrapper_fun(character()) Condition Error in `wrapper_fun()`: ! `col_names` must have length greater than zero. # logical col_names must be TRUE or FALSE Code wrapper_fun(NA) Condition Error in `wrapper_fun()`: ! `col_names` must be either `TRUE` or `FALSE`. --- Code wrapper_fun(c(TRUE, FALSE)) Condition Error in `wrapper_fun()`: ! `col_names` must be either `TRUE` or `FALSE`. googlesheets4/tests/testthat/_snaps/sheet_add.md0000644000176200001440000000036014437176464021574 0ustar liggesusers# sheet_add() rejects non-character `sheet` Code sheet_add(test_sheet("googlesheets4-cell-tests"), sheet = 3) Condition Error in `sheet_add()`: ! `sheet` must be : x `sheet` has class . googlesheets4/tests/testthat/_snaps/sheets_id-class.md0000644000176200001440000001075014437177046022727 0ustar liggesusers# string with invalid character is rejected Code as_sheets_id("abc&123") Condition Error in `validate_drive_id()`: ! 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]) Condition Error in `validate_sheets_id()`: ! 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) Condition Error in `as_sheets_id()`: ! input must have exactly 1 row. x Actual input has 2 rows. # dribble with non-Sheet file is rejected Code as_sheets_id(d) Condition Error in `as_sheets_id()`: ! 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.md0000644000176200001440000000020314437176625021422 0ustar liggesusers# abort_unsupported_conversion() works Don't know how to make an instance of from something of class . googlesheets4/tests/testthat/_snaps/schemas.md0000644000176200001440000000133514437176451021276 0ustar liggesusers# new() rejects data not expected for schema Code new("Spreadsheet", foofy = "blah") Condition Error in `check_against_schema()`: ! Properties not recognized for the 'Spreadsheet' schema: x 'foofy' --- Code new("Spreadsheet", foofy = "blah", foo = "bar") Condition Error in `check_against_schema()`: ! 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) Condition Error in `check_against_schema()`: ! 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-get_cells.R0000644000176200001440000000371714207753213021105 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-sheet_copy.R0000644000176200001440000000144614207753213021303 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.R0000644000176200001440000000322514406646454021357 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_no_error( 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.R0000644000176200001440000000343014406646454020305 0ustar liggesuserstest_that("check_length_one() works", { expect_no_error(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_no_error(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.R0000644000176200001440000000603414207753213020562 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.R0000644000176200001440000000211414406646454021063 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_no_error( sheet_add(ss) ) expect_no_error( sheet_add(ss, "apple", .after = 1) ) expect_no_error( sheet_add(ss, "banana", .after = "apple") ) expect_no_error( sheet_add(ss, c("coconut", "dragonfruit")) ) expect_no_error( 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/0000755000176200001440000000000014437204004013225 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.R0000644000176200001440000000111014407133760015426 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.R0000644000176200001440000000404414275601106016026 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 #' #' @examplesIf 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.R0000644000176200001440000000551014407071424016043 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/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 client. 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 client 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.R0000644000176200001440000001142314275745276016014 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`: #' * #' #' @examplesIf 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, call = caller_env()) { 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." ), call = call ) } googlesheets4/R/utils-cell-ranges.R0000644000176200001440000001712614275736326016732 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,8}" 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. # https://workspaceupdates.googleblog.com/2022/03/ten-million-cells-google-sheets.html # Rows: Max number of cells is 10 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 %||% 10000000L 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, call = caller_env()) { 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.", call = call ) } ## 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.R0000644000176200001440000000746114275601106015643 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`: #' * #' #' @examplesIf 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.R0000644000176200001440000000561414275601106016633 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 #' #' @examplesIf 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.rda0000644000176200001440000014563114407102300015370 0ustar liggesusersBZh91AY&SYl 6{/+_}=;w) (;b;QyT>y=ӹ}}ǣq`]0OnJپ}*(:h@oQt:rX>FP t= (P(pQmI@:{`s҆Ir4(h6elCP@_`׮=r{}K9 (.䈈IrS΢p| qg<;uhy{hlA~/.|5nVoYzj^/VR>כ~ l׍GZޞ8N1re_>u8uQٮ:E/WTf9Ig厵zR抎;Lg8cS'80uuum^wFfxw8UN+q}N88^&Q}a~ģyE0BF"~*% #jRB 8S# _ w$?!򽸃T\$waEN:`aIwFo#HTJ1hul˧_O~8zo׺`"!ztrBv@Tݧ.9wд,_L"?S&{& ;zy:I ʟ-_F> pC:,?ֿDpyBy bp|Ɨ/珶߉Fz0¢=|JUb0珈T*` vʨ5G<^9C?7\/7;2|~s q.4( Qe%ޫ5Ŕ!X~Lt/;crI8̕LeE>1iCmF,.dK.&ew0 =p Ș8I)"#b0trMYR06% Y)HDjoIX..풑V)h7.ִE X(i.+Cguͅ4@ɺ+۬ެrJf4Á$8 @n dZ2HbiD"Bºj1"*jsIt#H̓L/ ̒ՉC&ytb ecpL'3<`S)ee+b@)!eth#C)XhۍHĭͮ3V2]h!ҕhG[Չ8!APa, Ņys܊tC* O[!dhCoUG/|Hk xѧP+`jlSKBZe kWH Q"0biZa*0WBt2iʗ  ߋ4}NM<\{lÉ{fU0=DD(c8ιI*XHLab vY?dH@ `fDV/`yfrh6(Ƞ,ؑ-`IV1^ 45Db 7YVZDScʼni.?3$>TܪbHP8ø"29`mߦ!5"2 3 hA.KL$ϭOMS5 4Q͜}`NL]i˯iC}#"i}w'a;mb=~~WqE˙ 7Gېݐm57$ ˺ʓfr. I?C|}Uh |-5o!O~qbgD$tn?j1gF#c brbD*BwA*(bQx;n ǯ# SƐ!| ‚Ţ *";|7f_1~D^pNq`?[Ce?r&x O2svf0ˠTc$\j f1Y_/8q2ƅ<, Ǖ(iT"PTH_ei$;\ >#|Qy4WuE>~wM6zx:mEgLF՘N FeS>TEVCk&XAy*)f"&d:N8 6 ob V4'X::X3ṅ,.bصH:P06Q: 0`u&L&ۏg/?IÚ\*=NIwW0 5j"&}L,#3K $uZI@5Cˋ%6UaӰ,Yvv.UԾ+`ˣ~o 3^IS,!d }* xPAT> %@R!TQ2 e ,*U @\1DQ}B{QM 0TC !_c\43NX1М$2@H7=컈m 'd TT$d-: {>/ܟb %KO\ Y>ӚAE81TmwJN}>{k HAA7n1X* qqYFTȉ@J!.׈w|h &K5lsDhMD)ZPK1[6\ёQ"rKc#R&y (nr+@A4I/\Rg>NC\d1ed-4qL{iie/=h~F2譱[TUEQ5أ|mY:!TMEH/ry$dA:py SM4R4ZAjR}v$ svWu0"&n??XÏ J0g\Nx 6 TM}mG8K1)l+F9y;; "].XWf@np)((+ 8q*k~ x!a @Ρ$͌B9=1s^?ǔƧ Eow [hm0U^"?u`Y[@+ 8p|=y3h[De&(lT)Ll(6H$fIMf &1i0PIWZA!a/Ļeg(,#C]'}ܯ ŧn- %>{qٲ-(RXw *|9B 9>6z@q$bTD [9oloH=M5.2BF;(k97*I5 X4MFhy$DkkFܬ3 T2!-]F7qFs$wH99}:½r7nrlU-=^ (m(R1ҭ %d-D` W $D;,1'(SA`زvdN:!ēF7!`B( h K`1 8:BG{UK? $|_cͳ^rgUJ"{ rI X1Pz-G,q@bm e 6ewr%6q{]4%u:3Wξ|g I{#-s %wtʼZKIkB\g@XSCBG T6 ;Ùnsa& mF?7Q_g9(\_)qP)?<"Z(yR3 Muio&l M1N0=׵?ݠ<+#ŚOQZwb֫++ŝQoӺ`mI8n)IK4 h;YfW>W6?iD, U2NBzpڛcks)rU^3 Ɩ#o M5v)jmt;=U}׋VL~׹,T2Y-ݙ?aYU(aiur'|\TzP΃ ]A 8T Cv y8dup[!k"FLbU߻r%lUt7+`11M) XjC0獷~|11rͰ/^PS30aY 2׺0T4–(b' ˣ)IK@ޘJ#b Y]f̢?t)cgѐRO gM=A?0f`%'1鋧oB0eJAڶ^`p`XZ,*x^. Xzc| I?.=88/"׈FgҖ[V2,  M#-"ȁ.Z#2׼A/F117r9nZ+: 5׈>K\A)+`hl}XILmG"X+e28h=Xk24dF$䮖db[^\u6djAD1fF~?{ɤjU:VXkr;,w9r)OM/@ A mcq`{U(&SkD m !A2ŋ &2)Ob aaG/!F &`*H-)(ꀮ&(""Ph"JJR p Dab  B SLKA ɉFU` 8ba#ʑLJpP/" ("lLTVI!$fURFTTI QXT쨊#@.22eGH@@Šʼ^(" (RU*MPH)H!Q(e@@ 2 TTNJD eQR B$$"r, RUU * [X41b=ٛ(NJN -6mn<=]Qu_ھt:G<|i{q `˩=u}Td4!` VBaAQV1}8ȪSK^ WGⷺ#@F#"DDLCo)-94ɲuBttc"*&*2g1 W\"sF7m4?g(qJyu '92dٻa6~ ə k¼0(0-T##")iw0~z5w<1K|y\0meŋg'*R7Gklݍ,ۘ}օa;q*V"pZoxTk +ģXҚS %_(WAY\{7,6Y`.&2vbK:Ό> <'0F].dlD4MD 9x+e1 l?7Z43\|hZ D/4fse2gtAcI%o P`gȰh0#:ț|EIlia*tl$sOR|,+"Y;a ":rh( Eo`=Pިw|s%" %,:IՕm?XqpQU18<29)nzdKu{!uiix͠&մ`2A"!auJS)`-hĶ2)i@ecE )E0B`{P;7x^a}3C 8^sF4!'0 bmdS\>|Y-J϶V^&<@zZX1/Zs[]rS>U *| cLs8˝MJFU0l{@ϳ"Ze)ni̸a []#2)ȫ(/s`0dy|YPD2u 26(|?2sgH4[c,Y:$g~= <0 `!c>UÏ޲lpUhAO;0\'@s>'GY=F B)*0{U<@*^ZTm! gPu)Èe}W),}fMnv`%hhwzze]:ue,XlqfB@ٻ>l5 5 v1 bhG|u2Ҧe*Uy7EΤ^8qLb7|5Tʒ"B.JJ&F_8̀\Q|{9fIl|D.$@Μ06nGDBx"Q3)/ @@! Sr8Cw q1Q2ᦳ,iY {r ja3RmiS D8"c*}A9h Vҡ;cCrMCKM E 8Mp|\;9g8d8mѪvGq/ibOaй#0ܮiھ.z'*C: ͐" $i"A*Ϻ}m9OȆ|0x^7jPVlۺ'd:~{ap?ǥ0)PFÌ3f/džlr3cl0x-UcbmϒUyzBhPJ?F0EhnQǯarV*=6 >sp+v_GC`A흩-Cdv_H>O:M Qňjb^ϖBlޥFŵ"3P`CH `Ȁ&#GH,M cbF`p:+<affn A ܹPWq2! A-)b7a8-a q~>!`(σ1O1ˌO:-w{&#koY#7#pصCG-8qAʖu>g }5}$̱3C2*LHrD/&Z.G(l^ud(z48B$P*.`mJ, _8l$ !` iO.cki0YH ,}fg1c ^ir^P kOzd>cfSS sưr̚eq- Z\&fx4ŷxX_.s<U)'?GFsۇE=|˞i4L,5trCMH\шjFN~*P{X1Aq I!Y^zrI"JQ c3|AAG.L6qO䬵1_]c|7TوuVdp`AXnWňf4Щ@}?ьklRh.DDWuim-&7%b a}j+9 tDh?::7㒑kj% \}tG^"}d1$ޛii5c&yE!d :y3zXq7n#kNj}0fhxדŇz*S&C'(>}~~FL(?5>b_+?MVaQEDY -͝82{9BaN K>Q;.%&J:3MV"NhAʉ28cMXP61 G,*T+=Iu#wg~&x;kkQN߿^2}# 9lA匩n>4NPm7ߥq8bGŘ(eGFja>}! m#X8QP[.lLhB4&Ypg& ;]t')j]2^$,9= 鈓x= Ӿ8p2zuzr)5{ 0x blTQ $ݗRP3D3_GGhZO.qha>q>[ ABҼs7U;Mݧ04hEgu7ٵgDW$#dTDdGH$lF&Dys$ DCT`bhFiV1\7#sː-7j{+riM>Z'yY$wrxJl2^C%,jxFՁ//0ޠY Lj2 g("-4ڔН9eNϵ>^e^FbqA0d0Me0RzD S&U R a'ѥy *ks5E&ZfrҀjFUx~`i3wrhZh0Y!Q]v8c ۍ3t̺R]P !V"kLb8aTpӾN0IƦ%?7Dnƛk?q$D^ _7MQ7D G=@x:@P <\ RT>Ze%iGUyiF "h?V,zAτA{4Õ ԐTCܣ>`f@dkdʥE%!85@7;ݩ@X^s2hs9a@u p<-̐bN=*![q<{4L1UUMSz?l2>:n˃X L0KD{z*HhRDC%0Bl(9+r2]w 'xllmڴ9|971LN`S nCjy MRid=v| wۜyń!/- 1_7BrT gHb:Ȏ4ߎHS{o'FLukSL‚er$FG Sm(tIjFP r< (o/S!a$|40$5"''s#X\p.N}Uq #wYx#Bssl!DǼ3H9c~S֎!Gn8q失u/u (VZ59>9g1=JB'xP|6Tu4 ޹+14ůTvS^̾C\tjjB6gE1V˶x:cv?1_^c!I"J+QӇLS ,%q$QgM,mc|1{Ir>'eOH_VBH~[EIDƠ_|OX+k|v\g5QRb ߓXk/Ʈ u# U^'b0/|q7Zs F,3 k@L"H2UC9'i'{q1T0lMo ;1Ϸ(Z]ygR@ cKH ~W.`Oߞ3=ʹÂk+XfKY'TFh_oY2 RCӥ|?|6]H:liFȩqOJӐ]xfb}sa/۩xYm&]w]Hie Fht\o zV:(f̈́=ǜJO4X3m[/ $eEN_XQ6 }2C%pn2rhЛ|@fKE1fnAkR]nuc,, `Aˆ\_V>D¸ `9<]AkvV8G15ow|ԕ|%|F4:0̪pj0]dsڹ꭛3٘&n:gYzS;gm{獐ˢO>},ry6## s̓p5 %ku\q35\J'8`O6skoJՔ0 ZB,b(*2u3#7[AGLM%c}-D!ѩ'Q4up1DH1qpaOYVQ:W_^ ^pW&d.K: pDK[L4iEX@{叉`DDtz^sf8.!GV+7J^'C[W5,⫌O1|/e_cȤ>?_Fײe"_P\of `6tb6/>ksAt4y8KZb!%&=p^x*I'w_&\s hk@;NAɾݞvlGuhoL`MACbIL!Ĵ0 M S^yE%{]'} }ۙ!Sq(5dyd+`t˙52[K2|+k>dZ ^=XzyOO_oIPhlkP(rAV֬#.O1!K+3Kp"9Xq0sxnl䣎S-ETvJڇXM X)zt̐=A3 :@,c鉥{)\ dnY"u 5s-7 Y=ξ_،cQ^=mpі|,7y,:SPBItIZxC"i{s']qe} 3P)Ÿ,jFSD!m'gh S0C-GQhF(ҨY`,( N9`D3٠ף쀀QES?TH}i 1$qzg*Wt=fQyDc~l*Q&AiM !! /jpθ&n_j|:sy/CrAsGTpó yk@|YЫ!!1(K)s6'(%l \{-sf-(/ @|/F=& j#9(ccCm q8`?pDm0MRϳqE?^clFe &Bdp}va4 V,gGaV##8-{E9hD39эvedbRr 5 fwXyb1n$k$"LBYGqEXմAƾpq 5bVOԱ@:=(@G U;$Jf\x/^c~e _*4[Cs94.V m. ;Ř` OZBR;RLh ǃg /ާ$n.G3wKD3N+vH tbηf޵1 {gSN=IT P:VѼ@_@nrb+d 4Nl.qσ|q[euwQ%Pv 0xڥVLIPzo | <%+̃58lR4!3Kt=='<-w5==Z9RF`/QsRnu0[c8wZ@R jޖ|MFWlI9#SeoV1C30|[wvݮgfi{'Sd_gɲܼ%&F&7_ ǘ,G,~l[6Z>SжK[ QQw}I Ƌ@#B,6% N7 c>,~siAM;\㘔A@%G.Uugplje)tis =&XZю~.4*Yd"!cȅ쁉Zi6A 6QXc@a9 z:>V/\SY(,f,1Mu&nNՏgc‹u0тDz6,$bvx$SA"( Yi01I˵䄠\e9}z9V BP(F22!@ISXN?M}&2*/rduᛓh@ :0AZ>SF:M9MN:EwcNOQv o//oxÑ19\`Gt['>cJ0Ǿ`uIf?F˲|wNpc+W d$0P@LaF-Dh*qLr;kѪq7Нor$ֳ,0#tq/3^d6A~ c߿ۑ'ɗ͠Xǎg߆\/w_()1w,6lk':aQ(Ti5 ,SGƦ~{5xw\q7^w^sLȍI lp22 AJFݸ):i,TpKvUV~ <s%jVm0T@|`pٳ9$Vg(TzȨ46f~Sx'B9)QUnsݺU*awHd3 !C:a~1:656sY)='t3c;5!/I(;rcGո?f?Qf~Qc3NÈm|?Β79_OW pd{b83,uݲ\b2"Nmߜ SLh?vx_w]=.sņKJw2􎡚3rPf? ǭ-LߍfO~|(qxH2r1!s=~3y$<2ђ췏OF>a&d?kX8_>~r9- >u8?f#rUhMJUUVݺv6wvmtnTM%JÛuoj`:K ;"8vb3W Xp)՞MU~ЋaG*¶ Kc3R`M 0}M~y75~5kWd~|e>)XtQk$xp}ſgq.8lh8+V]ez zSk>!7zg.[b*b<5). ijC)0G.>WRgأoV; FH2{~#Ӌ4m*SF40uD"?pKqo&I082 E Aa4Ѽ{.&4uYoV Y5<ƞk@at"%g% !.SG7yhAh6$jQTingOϬL]aN 2JRr5-Xq[@q@L0ޒ REDUVvP#,j?Z@F8*̰DEMѵɘ2KQ8뒞%¯b* 6˺IA/l;)Ja!@odW>%O P2B<)!( ;k@#>;о;'lFwp9v*NH-,m`92'I=W2tVQxT ,H>}}rrvMq1nܽu%2;Ë{O|N àXoeFkn1~SGx_9po${ ]s?v $.:7'BL x1'!wddK%}#ޑِ^$@ g"dJ#ئҠ<2韠t,#ԣ~gjr'PqqӔ#TfMg/uWppU+z]u /4asPqk!Sw>PjgK'x(zyL LSƝg뢾@t2,Cۨk"Dh!]tn)[=F/{\Đ"EFYdؠ&k$s8rH sj 6ʖ{F= yDҢ*Y+Ȕ"@dt9Lc~7ޯꝀI>V*]"sGbr4f J9|!:Ț (vZߌEY]kq{4WE2 ,33{lxI&]jB#]چo|D^|<6?|JI.תH*Ht~\TA,շ2$C!}R6h&K~ޗcov"|'D> x+{q̢;eZhv tVRdlBwUUχ3]̙2:eb( ',R}:+v8r}~^6ďG1T2Q:!u\YKl% /(NaٻB2_?8I]μE%| DCCޓf(,ߕ O=lSd`g8G;9wweE4kNG;>]kn\6%@~gJJlcƈ]L;Ȕf+yNc5YOky &h+ 35//( Yq*9c #z0B- BLKx 9T׬稧#8 }ǧo%_sb=1!-@a η/= (A4~I l|Jt6'Wm4U}<%&=kD0N>o;'p G\#qZcڈ=g^:RP#ZFdDkY61\sG7#=w{W2?F2Zʅi_@ i$#1&döv)LhNU,<fo"l@'O{z;/@qB%;G.}=\tVcݘIadLYנ*j .\Hp>+T!Gϗa pGg6(Al% !`py*,'Y E-5X3$ff^2ʹ?on}5Th`i{BN|*9$E݁QOgzM܇8- )@: h}7HOpԁ[ #Z$ c DRnhFZ\fT~v>{ȈebG>U@Ue$uW5nu/j^(335@gLpp/lzSƣnՂ8yi)3hlT/ ci)NYǿ.K* QGM\0zi 1LF}~IyX;ͣpgL`MeԖtukWQl=\K8ǹ.@Slr?_å ^"d `dəB;fg JْU9?|{·GOӦwL|U X PĮ9d: ݀`Y7`цEp欆_a3dxgdR0UDI(*A%OEtS͏DŽ$AIiv{~8@*'lOpߩ јV4 .vX 8p/$yg}-#T2k^Lfv|0srNr=D= Vܣwx1ˀ ᪨a&MƩ1X㝑ӌrbb16^U<*k%SW`6̣Y0v#t4ܒIX`=K`ar᠗f!jTN]^ɒ^qIM;H"4ӊU;-1ÔmZ S[K)nS\/?fmtٿ<*DCEj33 z #|^9>M*?n~pHͽ6&/P(]-\[SqH"q H:Yn e.S|.Io};x;\!BfElƃ!] aŽ3}D;B %}v4F:VmJƔZˑ:#s_' 7,o5v?-Y4swx0!#@ #X;.!2m<'<D.cfcY] `Ɍ#񌩝}?_o  Lv{h+DO͟L .P :4\xC4ټFw#jHi!:bLVk#&iI (m!pKL^hb_$x#P@iVZ7&Z`^͚a_H/fY2TZ `\!|`2lL d2<._3Є SW*1 a0RW~䪬聃rʼ 2ŝ^D*6u BF+p-qzLN,#0Nw=]mY9:;:H\ccԢ/cxP^X*J+G(Z?gڐfX"V5]3^] -ULp[rܨa[au^BR gj'jQ(2ٙOfvcW _? ͈Lw[Dh 1yAޅJE 32Ll9`XaB)}iQ~4οyPxu!ZQ +Hd^qG8 V#(WJ g4lW".; 1WbMӒ!@k˱T̒|n+x_$E2iD,azOݭ&@D'T?Oߔ@̞&14a .g&( k%nO*yh4j&gA4C10a ߋz-L;RJ.D kծ+TB(;HhҎ>o{G}?.3X"㗆t1K|׾39QjXaI$a4V4e4@eOacc-{̦L'co:(F|8 Tp fs<(Q :sK,L|/;&\?C}wWX/\O?q{/sV׈~??xO py_IPO?<8X?`>?j.Ϯ}hoN9tk>۳oOWs}g{>}]7xdZf6d>.7U+ѷϽn[zC+{|~I3x(y`gVs%;G&9}w~?>[x~7bз{~`yý2p?S35f^G8_;z9߿~cgL?/}Gk쪿l_n4{м-nk}ob|⾧txަol`x|oާ$+eG?[-37_?\#Ϗ5nn lmh-N?ϿNwO>?~ p#9I7'0>3'ۈzuQnGbʧf`sWgꟳz9 |#wfecwͯܝC!`SrNMr׋𗒽u߫}E{|ns8{R~D-螿GZ`Tsm7x\@47:Rd#!zlv|OIz 3m{]`HjD힠 G`bD_̐t)|h#,lP5BW^jxj疩Hjԉ +g``]?ϵ]&>/{@s]?Ȁ%>O(f_F$0ը矸=_)gPNˎghkUǵ?kyk]VZ?] U "Pc?MlX/)/ Bv\ ?GDVk5fvfБE:ξv9 h@+ǟ?7⁌qo<ӁF`LѶ^{KMwH6~w)Rlip,4OݎK8zk \.gi<ʟ!`\u2wv ]I~3J>Yȱ9X|UB<Y~H5yWK=HVu1.t5j%#iicK|C/f&#-5h``39ިөMt$q * Q/AΘ@gBGK&X,ߡК}$oxa#lsykimw$WdwAϤڧX(CYF3&9p䇉&3Xw7o3gy~Tp~Dr{)V +^"Kbk~+[3CokNq[-` {s*ͦlɿ5KnhL< b.Hcs~S Ħ@^oz^pP@p%'\! h聁]bu,^Lv ?QA4QDcR6(h`ۼl$Ql &߉(zGPe@䠎~IS?? nWzkIL'L; 9AA%KY}rݢx4<!vr|޷ --$2A4,L=S5&>}͋oQLRA,}lZZ!ŚZi4*6Z2aaQ:âJjw'wאIK 2;*h!\͟8=&aP/;d#GPO6EuPr5f[G3ˀc8(xe<l.=b8v Z]c"H keOLzIj-h R&"!"iR  )%a!X"%FD)@~I(}IWBLF@LTTXyoz* pnY'cCqW.%FN)i8e==nH*U"R8Y#8\KJGy;r|ݒ2-QSTUDA,APS5,CM%ԉ@ & Hi!pqMPƴ0&BŃ>O}0:N"Y.߾4w!BPBLA$LD @?f &,D^ײO/1# Lhɥ1} 7T+Έ΂JGy"rƞ;Q ЋWg2Z;'u~ :R zvC(D3M4e%C 0IUH`R e( $dhdRZDw;ތ4?)OR"Rt!8sb 巖cwl1c0>0y=H'``5)@0PLI19Q Xm xgFy Vǀ{x"cSv ?={L*.tT$2ep-:9 A.J^Q&b!2XN RfcQnqz)o1EPRDQI4PE0QDCIDUJEB4 +H B@ H4)(J1!@IJD A,@(JM!%0C@D4 HH!2DUP0Ԍ$I冨aHJ$Iahu@~̤{cp.1e4->WNjh+DL )tӡJo1QU*r!`$M>A<ȎBu qb'7%FT Aũ`E!KD2, [n yA) uqf9wakk)hrF`^c#&B0@q¤w^zr%S]4rp f2@dDHL%8#-sm97E)Ox܀F¢rhD`T$Cd p AGmFpF{m  *;瀴P_Ø@ WSq0!:ةiXSB|"'H 71\ntPu@'sRo4ǗxfW=[2:}_y;PSJc|j*N$(}?v<QE15UD@&2~_=j2 $pVzyg6r$ %&HФC~1F$;&@g E%)k$22[}^LT˵Ӽ7U1lA<5!|ȟ%OpL\`H (45Z!x$f!De:j JiaJ\@v.~ Ͼ6ge_sr97ҝaOڭɈ;e@θe00 )"I  &(h΢ʁJ@H m(& ";s@ zDGIY" ;:E-,@M!4M4L0*M}N"zoC~<`8rb>|c[!/|_ H8Y:%20p)d&B #D6'Hn!axTy Y8q8E{d/EHtHR"NUsVs.w}> ׾T!@LSWBdL  $--*)Uպh(ڪ}ſK}Nx)=Za?L0_w/=(P*XWt:U:~1d`q /vCTrdU3@o¯Ω;N|3tzpUiC@9&EE # 4@B@@SӫE O%_޾1Xy; J$BGDA$u+ Cg(a9pacS IR!N6Vb>R52xO#zׄq- A wkՑ9$gԫIGu6v( ny p` T9e&8+y<:ZW!!:dӝHgw'- H.T7~xZɋ[عL%* `C`K6*qy/z>S9}l@&Bh[[l  y\pv b&e!#5H{̡$JpT; x#(bN2Kfp饰ɓSv;~; !jnLcD9[@AQ? _l !$suNRcāb>Aw-L+ХĚ"dKR 4q9n2%%%!By_$@),K`=>ӃQ}t;r &E;`Ϝ;TK'l'"4d.ؓE(Up @\#Ox$prD,IJ$7ԄلwM@?,I@PK=Ga;ПǤGz7(!_#ּGC+Ȇ-6[d ˦G>m†19 #KtK-u$fh]Є+ C* MYLccl`Ϧ-@ n&2w*!K5P$19 4\;xAalj#RE$'2SJɂ"a82RkwP5Md C <ͽYHF I J% V4C ?r#"Bx))7[fP9kXR+:E5M58!KN5k0 '\ Lb3*ܵE CmD3c {5ͼe2`7QxzpNn"I5T00,6LۦnrX3 V)HEG@jQpS"'2".\} K@@ C1j$B `9.Y#7% FH|4Ig&i nX4qK =jߩX8*>8^$f x-:hWFG&x%p`}0D n6a{WRrCgppz4Θ@h0iMoA 25 Tz"2 H쁍 e Q$ Yq wd6.etØ%Sl" EB(i!%J Zf'@сX#aPuP.FT& p}h!':г#N`D$h4sjʆd ^*i"@'cE}!}@CF>}(F$<8@@31:A9n<7Xt46&\ DAI1, $k0.a%[[;76TH`"Kxr&۲5I9Fꈆ6 9r5L0(Y,O!ƌڢ%`IC)ԁKai!tx!/<|p=mj8'w-D &6!#c@pіH.$x`lLs>Y#[:rs5Wp"-oS:m_e.s^1P#Yrx-Dr{\U(`mдf0RƵ3gbtK^ )-&ҩ+]sS0+Į@ў Ijxdhd|DNVHKS%ħC6Y3 0Gc[  rK-E e2l nҲDporBg˕ow'$n- 39 ~vNeIF{46"4YsɝPZ>FFn* 9'.UZ0:Z'Vd 8κւ*X1:@fj#9() M93O5#ķ11H1͓RLcn`Drs<7zw:9b:u5xf1 EJhOpNvg%HQ c 01Iƈ&8<ިMU}_6"A yfd1&\+ 7v ,ˆOq7"ko렐_ ջNӍCnpE /j00 Fŭ6 kϾnr7lL_#t'J [m>$asM'.OQ#";EK  Zr㘊_Z&3JkW+FVAӶ6V|=)=)A%)`=葍E$P TRe #djF!7ctDNhXε u=®#C* <@C?L%(8"&IޓmQkf7R0o0KIHP i\'趂{q'Έ= xHߔY/ZP|[&% 2 5zƚakWX9nM,qҡodķJyGC_n~">y0*&(`hbLˎŽPY S" #(rL($X @ݵ%L! 8/v-{r35 7sP\Jr':ÉJG0м笖vh&P)M@Ե\D39fRA(LXH.[X܏ʃ q3bFb)O.8kb{8I F$ {K@u<߱)A2г4(&@*26tA-CA}9s¡8CgN.Xzn,$D>3# !fXo5rd2YS*nuRd]{2. "f/J04M 0==* iCu$Ǖ|Aijkn0 fM_I֨Kd}a /OUjvJe1b "&b&-s0A\jnnIA`ơ J 퓘CEj `nĴQqDy R(@("J#4)+CE$,QC)؟@&ꟘtBA}T$ I cq:1 1 D4M%%0-,Z٬F ==Qd*&P"d[ X*}ֵF ̒MͿhL+؁dL"AB" ܣƨGd?Y%2sS70D+E"ĩ`լEI7tC؀q$&+/FON/ctd 3a}G1eRl)"1ffoq? O YȨvz"(LqKЗ$? c5nj"hAFn 4p;mp-n+*reVA;B+gLHV`=cHty!zݯDY-욟hJLuQ##;D܌4SD,J0 p=dmͽࡈ읚vɣG9PWP% ◃iih x8I'L" h.P&=‡`XTMC*R9qeID~ aNAP-0!CM-=t`9_j6,Q ExtK )BLM9fPVE0cw@b NhF6Cňi'&U "A4 t s.nEiDAhôT)$ŠzDviKN j{Imj{ehEoʞp a=҅}-t0͈ OLLJЅqSt ZI/`;TЛр]p 2La틁M%(Y򻶏qR6i@S@2XHRE6RjFe$4U4{DR4 >n_EjS1 6WUl-(5 E4C0osoPC7辥"ta˜&Aƕ75U/Z.6/R$LÛ2emB}d2@Ō'Y LxKVjxZGhPáJL # Fs6+:oi"ZF2V92.!DMr"&H!urD# xoVR$yR|\F68ēJd/)#@n0)AJf 6lh4%4 B{*-zQCQ3ތOeB G: ` 2q2m!o s3 Ez94D7"=}gw pxj*w T J4'cK)HD%-"@LҬQD@@v @-(d 0Ko .e#@B&5d_< /;1#(fA "a &T厂${ șA ة>` ='4cO˪jiBT[C!M< PTLQ (Ͻ6|!ruU!$$bJ!XR"XB\a@(F${JDCI C z`0|C!)5SP̌X:!Gj&Fl&-I{Py$;${iAL~d4Q ,BD!!)h1A(S**Q8YX!iO}*d0e_iE8dw /1`.-,\:@w_!>SH5EDTE5UUTA=>ƹCayeޓRxnQ2I+0d ? Rl9@*la9(e9 i("H!pQf ME)JZus(ch 8nn2~r;D/:)Aa {G:/I )01E4HР(P",PRIH$EPPQLT̀M,ARHЂB,C-L(th#&$Ӳb&h25~q=r3"-${e IFFވ5U>/ʲf~o::QKx˺) ApzT( Z()X %H %$)HGYa&̡"Ea4 6w%n\m*r^F˝pTƪe /]6p>`B*}H9 a K.~֓Ȋ!Mϫlel֣56AvI寫c(n#J Yfc8~@(M"4$4b|ۑ{ ]Gy$or១Hdر'l<[:'ߤfKNL/(WB:tuYXXH)"B1pȅH T2"'bS"ai ((P `L9I՗'1@DyGg sY iFA;RT BPWl(*Hj5[*aڦmA_$w0dBVx0!0H>WdCTN~;7?+ä!Q! BjCN2TQ"KU$DlUUY!~bOxR$HIgJ hRfdI8OHEh M# qw7]R=xĴJf KlՖ=V$a O/d=" L/K?2)ԚCb˫DHƮY54pc2+ RPk.;LFrq,-dxh|ifJ1\DۄsӐ3>v22D>4u>9] 8坢 `q Uleob[.7=C#h(T=O(Xk]`M * %TTU9(& =Q&0Eg!C4ѐj Aty:,{! Sr1_Y{%C!D>R5"?[x>$CjO!($EH4(K T(L RR T  `JE>(hhEK@0A02#PUET"P*hҴ*!JA M RSPRR{hҦ| a.RHv3aO_*POvT[!V0JLL0,,% l(] cA cbJHB0)@%b7pAV"` Re`Tz-ˇ?/Dw W:NIDڑ$cϥP; ̛Ll&$DZh6&ꉜd7|^!9CN{ ϔ TPSATDT`H$U΅B2 Op-VVIzNUQ)b.HigԞEd{0^GJ` <*S"Iɸ>ܢ! A2Q@nq"\@܁DbHHsIiGsTACPB`A 0d'"zqSGUgԤ!$j=9UHo09K@-#3@CD z9C9fBehk\84APma"B%&B!j%<21JLDx B ;$cE3SQMC#jWɴbȠQQnx$-s"^>bj5QNA\ɝW["d=I"sVXa_9rX^H]m"j B vĠTĖLjYޛ̫BjbtQ?p$;'IU2a5Aonnf`뤑АpaL5DDV^|!P@įxXd sïIoaS73rPVDg%!ꠘьѼjcIcDS(QKl&jMRkPi0ŀ'%6A%$ +zC(XaF4MVbWT"A\ӹDhsX9 =ܪS\ |YOt8}].7, @@s3}&ySuK$httI&< `Q9=++9ǥE$J|Q'uEENK|˜CCWSn()t<Ɗ#,CA. VD0td2X 0aaxq4' '8㍹h) )8p Fs8a""ļ ~ã) tє D־J7&dcD ъ@>TF^N<*Gt k>v ))Zg}~1+2MDv ҜX<{ .f=db쀦S#A>'|zQ2"ge'TRї\a$D7$g@I@FNH"i/ToYA=*U+)cTӿ=9 0sb.g2*s_|<(#L4)8Szޛdta9-?aj*n)T[n'Z[1 mab$۹o@%HD59#lXIVRD&y5D,ʈzE .Nb Tu.!!&.&HVʒf # ؄)2{K)MÂz1 lxr6th9 zя^]8-SpI,`jgztaPDőPx)&8*ZJBtØL'kC!EA 1Ba3*R%)9FaDP1Yʄ0%=vP2$D"A&X9d%p(5t-Thr2!jD(-( Ɣ]U,Sf8\Ң%=ny'SsTEi0ʹɠsāLr}8><@($Pr5NPL4` rGG;zM>S+$$s&iIfFu-RIF!(#ȦdM97~p40CqcIP]YnTZedD15Pk1c|# 5jIB,>bEDMa\{ Ezݱ;[-}d}15G{Va P#"Y#tiT X%QÀC/C@B)9 rK J  E&hz ##wFZL=H8yK ,I,7#+9L؄$^T>n\@lkk"\uOdni "J  B"0Q"@ @!TH©)C2@4((@'gOsh-DJ-"g?"v4"PzhH;=FOU\C$ K=2H2EnCE/y`_נP}('o^`!<-tAoęgCiixD^ɨ[bsvqt4mh*8YQ5GʪlYcj#2^r1bk$)zP{Ӯ LA .Jڀ(@VhN.^(A!Hw4<%1DD$4PSq:p:57=IZ 9Ǥ0!A=RPrݔ_sxЏ`J PZ+$H"3(%U R@5ML̃M# a|24k hZ))[p1ۀw wo")bRVh:-2G WqD"R|#lL"'y,ǐv0A.Pqw7{{ZjA﬿o0$JBTEe);YE|#it;rQO41DP@-DЁCB$C)$Щ 08|r;e NXq 1b ?*<# GAN`~a uwVOX"GOS1 4RRSJDBAR” hP41XD+튝i P*1D+H߭`2dwS 2 l0H9IRrpȍ}?rϭ[c;VBcRJJz2^(1Fݛ;v)lbFFa!+z޾"zaOYS$'cI2 ^d܇wQ}+d&DR8_G| !@Yն"9X"_}*Ѐ{YbR~CceP{I ~qA~H>.i)OjzlYR|kx @@,4P0%1Q8ͼB0$deX[qΏ_XKMxbBSc̀ d >T>TM4P5 M{lry*" *'6 CȚf#E4$̭rq3 ))m;b ${\x/ EqÆH0D%IXsӚ ZNpUkB@&NpPb6؂ʼn*F*nJ.dC IY0F(Na:Wv–3)KIa j&*bJD0SM5,PA)PPE%b 8 *DY o!1W.(A8Rl!j & 1E1Q 5e dYd& d@pb]傄%_^-7"ˣc 946AS+g9ASeD >JH)h ^&% C~'$G9!hJ"kV0lTWUP)M(R yɠİ,HǝA1E@NF5`c\ݗnfl!%,v '!BF= (_6?h!&#eȜ9:c4~鄵=NNJ2 Ǔm"Mf`AL &*L!x Jq0S=a 7'K%>%yQ9\_sƒbMْxD{JQ*G{,V1V*$ $'f bU|0aflaHR^4W$>{KDE?xEoAg2 !P7d@B ڋi;gei(<:p=$^Dkϓ(6bCMPNS7@yU]FC<50R!%A%b"Kc@0eI#& ǬVa PgljW݄ Q1„4m  OGhj)!Rú,Xd4]f0N*Ql1{)$kͥTD2\ GD\0!x.ti-J!lAႍ,;u9XL10SEK%YPѣc8Hl&d$ !&e GD fjQZ\kZ $) =<):kK oN!vÉĶuUyX`:,C$wJPb%z ҝJ ^#,h8g 2 ,:7+s1ۺi'Bx=F_SBh3,έF]KAm 0 M1zqWgXE6d&F<{U-+4[rqʤ`lVW1D4cA(WXm>$tu_SPgl͓f`i>#l4O=&*TT4k1VKnX3YCi~  Re$ +iRth.+ DInERP+bڹ'H ,p]jrhPĘ q0uDlx(rAO/V.oXt10P S p̌46YC,-f̔6]LB&s;.D- rf$xNd"1 LX1l0aqDBBXÇ YC+$HEBV{us.sIqq*r"5O +8& 8H0;a ժep$Cx߿1ð۾$$wİ$XpF!c6(Ljt,;t@e x@j ۃIn9FVJ#pDUhYM1ת \2;Wh~ z8*)0Fb#p{bw3̺~&I[DT`fcju1 uֲaR5͗0 g6mJrB65P0&AeN$tf7t+p "T$0<.+a#[!I280 Cm! P!̈ĵSMT.~pB97k 9Q, `eQ$y5{ۂAmҩhv$>Ӊ"_6_$搂w|@!|XY6LVyhD)sv"P8Pa 䈡!ɜaJ'%Tn5!9XM1Hrܕ~/vqP{42ISgIS #)6 0,\ΣOC y9*JiMF <\Ɛvgv%gZTh.f~̸ 0}48>b=~n9RĕLA2 I DEA0UD4E 4 CLPL$UTLAQDDQPTDQ TJBJ"J**W瓔;hi#R'|c;g4+<.yIt0C y;*~EtaKz =R662 n }2|VG b> "C܂`l}$ p8OV K D}<gB|&OԐLYR)ZCi|C@vX&G5AKA@:w|"F҉=8yQ}i}@) $Ҵ(}pȁ#oo1gȑ8U*eiPHA% ]'@O#Ξsi)m$A/v"82ʶ4no[m:pkzB:` Sr4SshŤ(hj!ᒂ[`L5SXڠЃ2Dޥzȼm(ɓyH*KE.SE-AKN/ ,O !!pi / LI&7":!oPSr$()3yĶ:FJ3Rf$f` [Mm̿ }ւt EF)h`:8mDT&4RR0ey9:.ڒ4ɾbLdAeÀ]d`pTXZĮ)3ABlDS.H 𽧰ƖA먟;}nOrPPG ]N-!BTJ+ . 96JX JFeC*yTBRŅ[..<$p9cُS+vQGz`5>'5"DLU] 5TUMDQSERS;b*4"8e)6|u$jP۵&J.0jB@&/PN1-2i1&ʃ SQ(aj0XY-%[&4<@sp G3xIVon/1xgȣ&jdq) Q ؉HZq"cj,PCz8 %U80ly@򆏅NK#[xNX;e, MDY63l (@0HDڰds7'!ޛx`B_AВ2"s "( h9pq@R1 8HhҨH>&?/,owd:R;|7 @p@&T&s(R01AltaeF=JxB:oRp B'sj$w6~+"OpD)`yP_}(`~CI؃4"+nYwx)$IRO7|e2f]c%z6<6 Հ< A)G6h"Ghƌ/S9#R?&H!&WaiP^'<7[*)Z )h Bh"RSa 6LRĉ J0ЉQv6N+8"2l(l+<7@- `8q`np1a5kos"ty8LiܳHpLryLsh$"w9CJ wQ *a1@BB^`C+^]H"{ |Hvg Tsr9L=3&pj*ɵb/BPcgYm_/@>@PK)HT$(P  P- E!J4-SYd*pJMUe aLD)(&$kZ)dJ0" T!P2NEeme2X&Ʉ˅~d( L"LL\QQ *74Q t@_kࣾMe f@%3M<ꚺpƲlDR&9"Mul`)hvն_ -0Ƀȧ`z 2ʖ,ȃR%I6Qn0( ㄰|}\DGdq (VXCýIR!6ْ>,w>D}>)X*C$숢Ihsc"PX  |M`=jnuuZoGIDj̐iUi!{h;"Oe_v=P*NHiT ƁPqxg{X=`d)Bq#JL X(A8oy@Խy\e0EbaSG 0!kF/CDt S Hg O>-  GOtMrNyLH1NB>|<3y&{I?]|x/^^R%P4LI DSQ4CD!̀ Ҏg @jbd%"%dPlyXD yLj ޒ{h8,O~A %$S!8HO/z)tށHJd34p֑)@$ ՟>q k0!TDq( IA=Ѻ#T( 0ߩNnq8m 2k8[Nx55$: :8ESHRQB( (VYUad!hQ 2+K S'!> `#1ٟ4-)AT: :PB@RvhG2450?]hE 9C 耙  d _"hjedlM„ "DF>(ܑN$#googlesheets4/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.R0000644000176200001440000000704514275730406014530 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, arg = caller_arg(x), call = caller_env()) { if (!is.data.frame(x)) { gs4_abort( c( "{.arg {arg}} must be a {.cls data.frame}:", x = "{.arg {arg}} has class {.cls {class(x)}}." ), call = call ) } x } check_string <- function(x, arg = caller_arg(x), call = caller_env()) { check_character(x, arg = arg, call = call) check_length_one(x, arg = arg, call = call) x } maybe_string <- function(x, arg = caller_arg(x), call = caller_env()) { if (is.null(x)) { x } else { check_string(x, arg = arg, call = call) } } check_length_one <- function(x, arg = caller_arg(x), call = caller_env()) { if (length(x) != 1) { gs4_abort( "{.arg {arg}} must have length 1, not length {length(x)}.", call = call ) } x } check_has_length <- function(x, arg = caller_arg(x), call = caller_env()) { if (length(x) < 1) { gs4_abort( "{.arg {arg}} must have length greater than zero.", call = call ) } x } check_character <- function(x, arg = caller_arg(x), call = caller_env()) { if (!is.character(x)) { gs4_abort( c( "{.arg {arg}} must be {.cls character}:", x = "{.arg {arg}} has class {.cls {class(x)}}." ), call = call ) } x } maybe_character <- function(x, arg = caller_arg(x), call = caller_env()) { if (is.null(x)) { x } else { check_character(x, arg = arg, call = call) } } check_non_negative_integer <- function(i, arg = caller_arg(i), call = caller_env()) { if (length(i) != 1 || !is.numeric(i) || !is_integerish(i) || is.na(i) || i < 0) { gs4_abort( c( "{.arg {arg}} must be a positive integer:", x = "{.arg {arg}} has class {.cls {class(i)}}." ), call = call ) } i } maybe_non_negative_integer <- function(i, arg = caller_arg(i), call = caller_env()) { if (is.null(i)) { i } else { check_non_negative_integer(i, arg = arg, call = call) } } check_bool <- function(bool, arg = caller_arg(bool), call = caller_env()) { if (!is_bool(bool)) { gs4_abort( "{.arg {arg}} must be either {.code TRUE} or {.code FALSE}.", call = call ) } bool } maybe_bool <- function(bool, arg = caller_arg(bool), call = caller_env()) { if (is.null(bool)) { bool } else { check_bool(bool, arg = arg, call = call) } } 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.R0000644000176200001440000000710214275742211016345 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): #' * #' #' @examplesIf 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, call = quote(sheet_relocate()) ) ) 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, call = caller_env()) { s <- lookup_sheet(sheet, sheets_df = sheets_df, call = call) index <- resolve_index(sheets_df, .before, .after, call = call) 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.R0000644000176200001440000001223614275601106015702 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 equivalent 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 #' #' @examplesIf 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.R0000644000176200001440000000177214207753213017602 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.R0000644000176200001440000000645214207753213016727 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.R0000644000176200001440000000115614275601106016743 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 #' @examplesIf 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.R0000644000176200001440000001505614275736470015341 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, call = caller_env()) { ssid <- as_sheets_id(ss) maybe_sheet(sheet, call = call) check_range(range, call = call) check_bool(col_names_in_sheet, call = call) check_non_negative_integer(skip, call = call) check_non_negative_integer(n_max, call = call) detail_level <- match.arg(detail_level) check_bool(discard_empty, call = call) ## 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.R0000644000176200001440000000464214275601106016432 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 #' @examplesIf 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.R0000644000176200001440000001166414437176556015157 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 ".bullets .bullet-*" = 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}", .internal = TRUE) } fr <- function(x) { cli::ansi_align( as.character(x), align = "right", width = max(cli::ansi_nchar(x)) ) } fl <- function(x) { cli::ansi_align( as.character(x), align = "left", width = max(cli::ansi_nchar(x)) ) } gs4_quiet <- function() { getOption("googlesheets4_quiet", default = NA) } #' @export #' @rdname googlesheets4-configuration #' @param env The environment to use for scoping #' @examplesIf 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()) # TODO: fix this: when I switched from cli::cli_bullets() to # cli::cli_inform(), my custom bullet styling was lost (suppressing colour) cli::cli_bullets(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(), call = caller_env()) { cli::cli_div(theme = gs4_theme()) cli::cli_abort( message = message, ..., class = c(class, "googlesheets4_error"), .envir = .envir, call = call ) } # 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.R0000644000176200001440000001546214275601106017536 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 #' #' @examplesIf 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.R0000644000176200001440000000724214275601106015603 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 #' #' @examplesIf 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.R0000644000176200001440000000774514275733311015605 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, call = caller_env()) { out <- haystack if (!missing(needle)) { check_string(needle, call = call) sel <- grepl(needle, names(out), ignore.case = TRUE) if (!any(sel)) { gs4_abort( "Can't find {adjective} Sheet that matches {.q {needle}}.", call = call) } out <- as_id(out[sel]) } out } one_sheet <- function(needle, haystack, adjective, call = caller_env()) { check_string(needle, call = call) out <- many_sheets( needle = needle, haystack = haystack, adjective = adjective, call = call ) 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." ), call = call ) } 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.R0000644000176200001440000000270714275505345015065 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.R0000644000176200001440000000105214207730522020017 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.R0000644000176200001440000000432014275601106016012 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`: #' * #' #' @examplesIf 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.R0000644000176200001440000000377514275601106017125 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 #' #' @examplesIf gs4_has_token() && rlang::is_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.R0000644000176200001440000000746314406646454015321 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): #' * #' #' @examplesIf 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) { maybe_character(sheet) ssid <- as_sheets_id(ss) 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, call = caller_env()) { if (is.null(.before) && is.null(.after)) { return(NULL) } if (is.null(.after)) { s <- lookup_sheet(.before, sheets_df = sheets_df, call = call) return(s$index) } if (is.numeric(.after)) { .after <- min(.after, nrow(sheets_df)) } s <- lookup_sheet(.after, sheets_df = sheets_df, call = call) 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.R0000644000176200001440000001062414437203777016364 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.R0000644000176200001440000001411514275737675015545 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`: #' * #' #' @examplesIf 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, call = caller_env()) { maybe_string(to_sheet, call = call) x <- gs4_get(ssid) s <- lookup_sheet(from_sheet, sheets_df = x$sheets, call = call) gs4_bullets(c(v = "Duplicating sheet {.w_sheet {s$name}} in {.s_sheet {x$name}}.")) index <- resolve_index(x$sheets, .before, .after, call = call) 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, call = caller_env()) { from_x <- gs4_get(from_ssid) to_x <- gs4_get(to_ssid) maybe_string(to_sheet, "sheet_copy", call = call) from_s <- lookup_sheet(from_sheet, sheets_df = from_x$sheets, call = call) 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, call = call) 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.R0000644000176200001440000001107114275734745017512 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 #' #' @examplesIf 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}.", .internal = TRUE ) } patch(out, values = map(values, ~ list(userEnteredValue = as.character(.x)))) } googlesheets4/R/range_spec.R0000644000176200001440000001421514207753213015467 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.R0000644000176200001440000002031014275601106015656 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`: #' * #' #' @examplesIf 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.R0000644000176200001440000001027714437131541017153 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" ) # https://github.com/tidyverse/googlesheets4/issues/175 # dysfunctional named ranges are possible and should not prevent us from # dealing with a Sheet possibly_make_cell_range <- purrr::possibly( make_cell_range, otherwise = NA_character_ ) named_ranges$cell_range <- pmap_chr(named_ranges, possibly_make_cell_range) named_ranges$A1_range <- qualified_A1( named_ranges$sheet_name, named_ranges$cell_range ) named_ranges$A1_range[is.na(named_ranges$cell_range)] <- NA_character_ out$named_ranges <- named_ranges } structure(out, class = c("googlesheets4_spreadsheet", "list")) } #' @export format.googlesheets4_spreadsheet <- function(x, ...) { cli::cli_div(theme = gs4_theme()) meta <- list( `Spreadsheet name` = cli::format_inline("{.s_sheet {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 <- c(meta, `# of named ranges` = as.character(nrow(x$named_ranges))) } if (!is.null(x$protected_ranges)) { meta <- c(meta, `# of protected ranges` = as.character(nrow(x$protected_ranges))) } out <- c( cli::cli_format_method( cli::cli_h1("") ), glue("{fr(names(meta))}: {fl(meta)}") ) if (!is.null(x$sheets)) { col1 <- fr(c( "(Sheet name)", sapply( gargle::gargle_map_cli(x$sheets$name, template = "{.w_sheet <>}"), cli::format_inline ) )) col2 <- c( "(Nominal extent in rows x columns)", glue_data(x$sheets, "{grid_rows} x {grid_columns}") ) out <- c( out, cli::cli_format_method( cli::cli_h1("") ), glue_data(list(col1 = col1, col2 = col2), "{col1}: {col2}") ) } if (!is.null(x$named_ranges)) { col1 <- fr(c( "(Named range)", sapply( gargle::gargle_map_cli(x$named_ranges$name, template = "{.range <>}"), cli::format_inline ) )) col2 <- fl(c("(A1 range)", x$named_ranges$A1_range)) out <- c( out, cli::cli_format_method( cli::cli_h1("") ), glue_data(list(col1 = col1, col2 = col2), "{col1}: {col2}") ) } out } #' @export print.googlesheets4_spreadsheet <- function(x, ...) { cat(format(x), sep = "\n") invisible(x) } googlesheets4/R/make_column.R0000644000176200001440000001104414437211110015636 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 <- gs4_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() } gs4_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}}", .internal = TRUE ) ) 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, gs4_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.R0000644000176200001440000000234614275601106014715 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: #' * #' #' @examplesIf 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.R0000644000176200001440000000247614275601106015062 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 #' #' @examplesIf 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.R0000644000176200001440000000552314406646454017541 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 lifecycle deprecated #' @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.R0000644000176200001440000001062414275601106015377 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 #' #' @examplesIf 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.R0000644000176200001440000003016614275735411015460 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 #' #' @examplesIf 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, call = current_env()) 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 #' #' @examplesIf 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, call = current_env()) 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", call = caller_env()) { 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", call = call ) } # 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}." ), call = call ) } 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, call = caller_env()) { check_col_names(col_names, call = call) ctypes <- standardise_ctypes(col_types, call = call) if (is.character(col_names)) { ctypes <- rep_ctypes( length(col_names), ctypes, "column name{?s}", call = call ) 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, call = caller_env()) { if (is.logical(col_names)) { return(check_bool(col_names, call = call)) } check_character(col_names, call = call) check_has_length(col_names, call = call) } # input: a string of readr-style shortcodes or NULL # output: a vector of col types of length >= 1 standardise_ctypes <- function(col_types, call = caller_env()) { col_types <- col_types %||% "?" check_string(col_types, call = call) if (identical(col_types, "")) { gs4_abort(" {.arg col_types}, when provided, must be a string that contains at \\ least one readr-style shortcode.", call = call ) } 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") ), call = call ) } ctypes <- ctype(col_types_split) if (all(ctypes == "COL_SKIP")) { gs4_abort( "{.arg col_types} can't request that all columns be skipped.", call = call ) } ctypes } # makes sure there are n ctypes or n ctypes that are not COL_SKIP rep_ctypes <- function(n, ctypes, comparator = "n", call = caller_env()) { 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}." ), call = call ) } # 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.R0000644000176200001440000001731514275732377014526 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 = { cli::cli_warn(" Internal warning from googlesheets4: \\ Cell has formula as effectiveValue. \\ I thought this was impossible!") "CELL_TEXT" }, gs4_abort( "Unhandled effective_type: {.field {effective_type}}", .internal = TRUE ) )) } # 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.R0000644000176200001440000000704714407155113016054 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`: #' * #' #' @examplesIf 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.R0000644000176200001440000000356714207730511016547 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.R0000644000176200001440000001570314406646454016447 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(format(x)) invisible(x) } #' @export format.sheets_id <- 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, cli::cli_format_method( cli::cli_bullets(meta$body) ) ) } googlesheets4/R/gs4_auth.R0000644000176200001440000003036714440457633015113 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() #' #' @param scopes One or more API scopes. Each scope can be specified in full or, #' for Sheets API-specific scopes, in an abbreviated form that is recognized by #' [gs4_scopes()]: #' * "spreadsheets" = "https://www.googleapis.com/auth/spreadsheets" #' (the default) #' * "spreadsheets.readonly" = #' "https://www.googleapis.com/auth/spreadsheets.readonly" #' * "drive" = "https://www.googleapis.com/auth/drive" #' * "drive.readonly" = "https://www.googleapis.com/auth/drive.readonly" #' * "drive.file" = "https://www.googleapis.com/auth/drive.file" #' #' See #' for #' details on the permissions for each scope. #' #' @family auth functions #' @export #' #' @examplesIf rlang::is_interactive() #' # load/refresh existing credentials, if available #' # otherwise, go to browser for authentication and authorization #' gs4_auth() #' #' # indicate the specific identity you want to auth as #' gs4_auth(email = "jenny@example.com") #' #' # force a new browser dance, i.e. don't even try to use existing user #' # credentials #' gs4_auth(email = NA) #' #' # use a 'read only' scope, so it's impossible to edit or delete Sheets #' gs4_auth(scopes = "spreadsheets.readonly") #' #' # use a service account token #' gs4_auth(path = "foofy-83ee9e7c9c48.json") gs4_auth <- function(email = gargle::gargle_oauth_email(), path = NULL, subject = NULL, scopes = "spreadsheets", cache = gargle::gargle_oauth_cache(), use_oob = gargle::gargle_oob_default(), token = NULL) { gargle::check_is_service_account(path, hint = "gs4_auth_configure") scopes <- gs4_scopes(scopes) # 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, client = gs4_oauth_client() %||% gargle::tidyverse_client(), email = email, path = path, subject = subject, 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 #' @examplesIf rlang::is_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 #' @examplesIf 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 client (probably `NULL`) #' (original_client <- gs4_oauth_client()) #' #' # see and store the current user-configured API key (probably `NULL`) #' (original_api_key <- gs4_api_key()) #' #' # the preferred way to configure your own client is via a JSON file #' # downloaded from Google Developers Console #' # this example JSON is indicative, but fake #' path_to_json <- system.file( #' "extdata", "client_secret_installed.googleusercontent.com.json", #' package = "gargle" #' ) #' gs4_auth_configure(path = path_to_json) #' #' # this is also obviously a fake API key #' gs4_auth_configure(api_key = "the_key_I_got_for_a_google_API") #' #' # confirm the changes #' gs4_oauth_client() #' gs4_api_key() #' #' # restore original auth config #' gs4_auth_configure(client = original_client, api_key = original_api_key) gs4_auth_configure <- function(client, path, api_key, app = deprecated()) { if (lifecycle::is_present(app)) { lifecycle::deprecate_warn( "1.1.0", "gs4_auth_configure(app)", "gs4_auth_configure(client)" ) gs4_auth_configure(client = app, path = path, api_key = api_key) } if (!missing(client) && !missing(path)) { gs4_abort("Must supply exactly one of {.arg client} and {.arg path}, not both.") } stopifnot(missing(api_key) || is.null(api_key) || is_string(api_key)) if (!missing(path)) { stopifnot(is_string(path)) client <- gargle::gargle_oauth_client_from_json(path) } stopifnot(missing(client) || is.null(client) || inherits(client, "gargle_oauth_client")) if (!missing(client) || !missing(path)) { .auth$set_client(client) } 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_client <- function() { .auth$client } #' 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}" )) } } } #' Produce scopes specific to the Sheets API #' #' When called with no arguments, `gs4_scopes()` returns a named character #' vector of scopes associated with the Sheets API. If `gs4_scopes(scopes =)` is #' given, an abbreviated entry such as `"sheets.readonly"` is expanded to a full #' scope (`"https://www.googleapis.com/auth/sheets.readonly"` in this case). #' Unrecognized scopes are passed through unchanged. #' #' @inheritParams gs4_auth #' #' @seealso #' for #' details on the permissions for each scope. #' @returns A character vector of scopes. #' @family auth functions #' @export #' @examples #' gs4_scopes("spreadsheets") #' gs4_scopes("spreadsheets.readonly") #' gs4_scopes("drive") #' gs4_scopes() gs4_scopes <- function(scopes = NULL) { if (is.null(scopes)) { sheets_scopes } else { resolve_scopes(user_scopes = scopes, package_scopes = sheets_scopes) } } sheets_scopes <- c( spreadsheets = "https://www.googleapis.com/auth/spreadsheets", spreadsheets.readonly = "https://www.googleapis.com/auth/spreadsheets.readonly", drive = "https://www.googleapis.com/auth/drive", drive.readonly = "https://www.googleapis.com/auth/drive.readonly", drive.file = "https://www.googleapis.com/auth/drive.file" ) resolve_scopes <- function(user_scopes, package_scopes) { m <- match(user_scopes, names(package_scopes)) ifelse(is.na(m), user_scopes, package_scopes[m]) } # 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_has_key("GOOGLESHEETS4_KEY") 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" gs4_auth( scopes = scopes, path = gargle::secret_decrypt_json( system.file("secret", filename, package = "googlesheets4"), "GOOGLESHEETS4_KEY" ) ) 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() } # deprecated functions ---- #' Get currently configured OAuth app (deprecated) #' #' @description #' `r lifecycle::badge("deprecated")` #' #' In light of the new [gargle::gargle_oauth_client()] constructor and class of #' the same name, `gs4_oauth_app()` is being replaced by #' [gs4_oauth_client()]. #' @keywords internal #' @export gs4_oauth_app <- function() { lifecycle::deprecate_warn( "1.1.0", "gs4_oauth_app()", "gs4_oauth_client()" ) gs4_oauth_client() } googlesheets4/R/range_speedread.R0000644000176200001440000000750014275601106016466 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 #' #' @examplesIf 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.R0000644000176200001440000001126414275730406016215 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`: #' * #' #' @examplesIf 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, call = caller_env()) { 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.", call = call ) } 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.R0000644000176200001440000000704314275737123015637 0ustar liggesuserslookup_sheet <- function(sheet = NULL, sheets_df, visible = NA, call = caller_env()) { maybe_sheet(sheet, call = call) if (is.null(sheets_df)) { gs4_abort( "Can't look up, e.g., sheet name or id without sheet metadata.", call = call ) } 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", call = call ) } 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}" ), call = call ) } 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, arg = caller_arg(sheet), call = caller_env()) { check_length_one(sheet, arg = arg, call = call) if (!is.character(sheet) && !is.numeric(sheet)) { gs4_abort( c( "{.arg {arg}} must be either {.cls character} (sheet name) or \\ {.cls numeric} (sheet number):", x = "{.arg {arg}} has class {.cls {class(sheet)}}." ), call = call ) } sheet } maybe_sheet <- function(sheet = NULL, arg = caller_arg(sheet), call = caller_env()) { if (is.null(sheet)) { sheet } else { check_sheet(sheet, arg = arg, call = call) } } #' 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.R0000644000176200001440000000364514275742751016032 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`: #' * #' #' @examplesIf 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, arg = "sheet")) # retrieve spreadsheet metadata ---------------------------------------------- x <- gs4_get(ssid) # capture sheet ids ---------------------------------------------------------- s <- map( sheet, ~ lookup_sheet(.x, sheets_df = x$sheets, call = quote(sheet_delete())) ) 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.R0000644000176200001440000000213714207753213015240 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.R0000644000176200001440000000320114275601106016007 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`: #' * #' #' @examplesIf 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.md0000644000176200001440000002270714441217417014141 0ustar liggesusers# googlesheets4 1.1.1 * `gs4_auth(subject =)` is a new argument that can be used with `gs4_auth(path =)`, i.e. when using a service account. The `path` and `subject` arguments are ultimately processed by `gargle::credentials_service_account()` and support the use of a service account to impersonate a normal user. * `gs4_scopes()` is a new function to access scopes relevant to the Sheets and Drive APIs. When called without arguments, `gs4_scopes()` returns a named vector of scopes, where the names are the associated short aliases. `gs4_scopes()` can also be called with a character vector; any element that's recognized as a short alias is replaced with the associated full scope (#291). * Various internal changes to sync up with gargle v1.5.0. # googlesheets4 1.1.0 ## Syncing up with gargle Version 1.3.0 of gargle introduced some changes around OAuth and googlesheets4 is syncing with up that: * `gs4_oauth_client()` is a new function to replace the now-deprecated `gs4_oauth_app()`. * The new `client` argument of `gs4_auth_configure()` replaces the now-deprecated `app` argument. * The documentation of `gs4_auth_configure()` emphasizes that the preferred way to "bring your own OAuth client" is by providing the JSON downloaded from Google Developers Console. ## Other `gs4_auth()` now warns if the user specifies both `email` and `path`, because this is almost always an error. # googlesheets4 1.0.1 The mere existence of an invalid named range no longer prevents googlesheets4 from dealing with a Sheet (#175). googlesheets4 now understands that Google Sheets can have 10 million cells (up from 5 million) (#257). ## Internal matters Help files below `man/` have been re-generated, so that they give rise to valid HTML5. (This is the impetus for this release, to keep the package safely on CRAN.) Examples now use `@examplesIf` to express when a token or an interactive session is required for successful execution. Errors have been revised to (more often) reveal the most appropriate call, i.e. the high-level function called by the user as opposed to an internal helper (#255). Informative messages now route through `cli::cli_inform()`, instead of `cli::cli_bullets()`. # 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/MD50000644000176200001440000002403714441243302013341 0ustar liggesusers7f1862c8e9f0a79c1dab347e218ce890 *DESCRIPTION 348cfb97a704d8991d2ac8981507cc9e *LICENSE e99e1a77d213269546b7b8493bd89f28 *NAMESPACE 190cc366eea01e701e0d6db3bc712c1f *NEWS.md 2a387950e2a2139af8f1306330d4b6ed *R/aaa.R 48c2d3579100302ec48b65a2a70d0775 *R/batch-update-requests.R d1fc15ba8026412d2ec08ab4b6eef200 *R/cell-specification.R e6979c6afbfdc17d0d903f8ba72ba97c *R/ctype.R 2897826f8a40c6b5743e19f06addbab6 *R/get_cells.R ddfb363454d60ba14784d2896211fb2b *R/googlesheets4-package.R f48ba2e62bb0a2d98a1ab39380bd48c3 *R/gs4_auth.R 1151a6ff18d1d176495fec3bfd88fd28 *R/gs4_browse.R 2cbc9eac6e910c313bd5bbf7ff202668 *R/gs4_create.R a5540f98d45c9d8eca7cefe24f3584ae *R/gs4_endpoints.R ad42c59092119a18cdb291e73ba25cca *R/gs4_example.R 364ccc8e9ebec82f858c24f506f4e0bd *R/gs4_find.R 0ec87192bdc19a7e494d96e0caa7daa1 *R/gs4_fodder.R 5092f04d76b7b96fa209497cfdf36ffb *R/gs4_formula.R 29b4b653c303633793a7797512219692 *R/gs4_get.R 5b99f67d9bc028906cc9b30cbb4c3b15 *R/gs4_share.R 211d54c58d09d6f8100d4c907168277a *R/make_column.R e4fcfeb00e92ddb44e375c2f35fb83b0 *R/range_add_named.R 96d6d79ee3177c4a5f8388a5ef143cac *R/range_add_protection.R 4f178eedcf536e2fc5e03d40f4fcc2da *R/range_add_validation.R daa653895ba310cc35d28f7bdce1cefa *R/range_autofit.R 9ea7bd7811ccfb4a9c4d360428fde458 *R/range_delete.R 1d35e5afeee7244cd48fb7d8d976fd73 *R/range_flood.R a4b96e6b21ba94516cbc326bdcc8c052 *R/range_read.R 6d07e046498bdd362917124bb46bd43a *R/range_read_cells.R 19e30264ca85203246229bb2aad50c54 *R/range_spec.R 465b3aedd8da1e80352e4aad1797f377 *R/range_speedread.R d58f716ff98851118a34aa1f7cf07c62 *R/range_write.R 997ed9f901d3d1a49ad50abff7e5baf4 *R/rectangle.R e973830c746c65995ae8f186aed56f15 *R/request_generate.R be00f2564c2150616f3d08914382a43b *R/request_make.R efab76bbcf66c86ec914ffce570316f5 *R/roxygen.R 638fd593196d7ed216caf08bf38ea17c *R/schema_CellData.R d71c0d313306a0f6a3bb71baba1bf78c *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 be615ed81e058e7e32f7f195e87bf3f8 *R/schema_Spreadsheet.R 6ffeb58d3653eabaa02d2669f105c60d *R/schemas.R ea8e7f6f977b75f1e2dd3d09a1eff1da *R/sheet_add.R d8722c9fab5d0b50d34bdfaad9474a2d *R/sheet_append.R f3811c1f77c761f0d09b51a79133749e *R/sheet_copy.R 7dcb92ae5a1972525a9b4474a711e99b *R/sheet_delete.R c7d06bf059d665b833176fde5c0d67c2 *R/sheet_freeze.R 60d2d70e3ad89c11926602ed76ea1359 *R/sheet_properties.R 28f72df35dc48137a745a01040700460 *R/sheet_relocate.R b1ebca5e91f0259d4a375341fb2991a4 *R/sheet_rename.R aace3aa93fe52af04ec4e4da823d3132 *R/sheet_resize.R 65dd5387227de12a45f0c14398258c71 *R/sheet_write.R 57401ce37f63e6e81f94d7da16cdb096 *R/sheets_id-class.R 2c19812035d5949b9cdefceed4524c82 *R/sysdata.rda d38d27487daa674c95f532739f96c6ed *R/utils-cell-ranges.R 8d2487dbf45064f33b08e5a80830b68e *R/utils-pipe.R f046c5775382541665e80e432fde8b49 *R/utils-sheet.R aa42fc5b9cf9d0c051a768c2c0eef313 *R/utils-ui.R fda2d85049ff94c42680923e0e79d708 *R/utils.R 4fee5c5808d5454463c53e3f9f21117e *R/zzz.R 14c415b96aaa7111c3c1a80ef14f5d22 *README.md 77a76b1e7ca871fd61e411194db6347e *inst/WORDLIST b84aab04f6a9fad8d1cd1154aec47ed9 *inst/extdata/example_and_test_sheets.csv 30c5fe5cdc74e63bbf2241319931692a *inst/extdata/fake-oauth-client-id-and-secret.json 31faae01039821219c734902b8bf1f9c *inst/secret/googlesheets4-docs.json 213eb811f1e0f0fdaed6d1d0a6e6f9c4 *inst/secret/googlesheets4-testing.json dae7157f0a8d5929956311d29dbfe550 *man/cell-specification.Rd a1cbaf3f328e8d74e747faacf640c7fc *man/figures/lifecycle-archived.svg 6f521fb1819410630e279d1abf88685a *man/figures/lifecycle-defunct.svg 391f696f961e28914508628a7af31b74 *man/figures/lifecycle-deprecated.svg 691b1eb2aec9e1bec96b79d11ba5e631 *man/figures/lifecycle-experimental.svg 405e252e54a79b33522e9699e4e9051c *man/figures/lifecycle-maturing.svg f41ed996be135fb35afe00641621da61 *man/figures/lifecycle-questioning.svg 46de21252239c5a23d400eae83ec6b2d *man/figures/lifecycle-retired.svg 306bef67d1c636f209024cf2403846fd *man/figures/lifecycle-soft-deprecated.svg ed42e3fbd7cc30bc6ca8fa9b658e24a8 *man/figures/lifecycle-stable.svg bf2f1ad432ecccee3400afe533404113 *man/figures/lifecycle-superseded.svg 52af8c950a56418f61e2bf1213bf92e9 *man/figures/logo.png 95cb6cfedb3f0d5402d3113389a89b78 *man/googlesheets4-configuration.Rd e9b992c71a878e6b818df53b7bab86fb *man/googlesheets4-package.Rd b80efcb1f431e9103b0bd8fa0c8eab3b *man/googlesheets4-vctrs.Rd c53ac461d027f1458ba8e23f2a265906 *man/gs4_auth.Rd d4f8e6f9be9055316195f6f2febb161c *man/gs4_auth_configure.Rd f8b2b602cf5da6253e805178a2361ca2 *man/gs4_browse.Rd ac13dd30cc1a41c73ac6eabeebfe7d81 *man/gs4_create.Rd 47d1ee72e33c495a4992dd68e4b20b24 *man/gs4_deauth.Rd 78bb7373bcaa07a09c7114d4adbea159 *man/gs4_endpoints.Rd 98d460754b299422cc5a0ad197734287 *man/gs4_examples.Rd 7fa077790f146055c719f045c4271c91 *man/gs4_find.Rd b82a4fba26b3f22891e7c477ee1d9668 *man/gs4_fodder.Rd c1cf95fab32f3aa8b8c3cf51db495d54 *man/gs4_formula.Rd 059a2a75919a39f7c9387c5f1996bee1 *man/gs4_get.Rd 6ded4e6dd61f2886dc1fa1aa56316ec4 *man/gs4_has_token.Rd da148dd88a6286a12817597a5b8cd33d *man/gs4_oauth_app.Rd 38364fe81457a5d97832a6ac0b3286fa *man/gs4_random.Rd 79192b5965f401fee630d810adcce392 *man/gs4_scopes.Rd c6256395fc4dfd9a9239cab20bc25123 *man/gs4_token.Rd 7f2b6842f51ac39c6da6499a8a79bd51 *man/gs4_user.Rd 1f7896a1b866ff9ae89ba35be7c7b6f1 *man/pipe.Rd 08f9168b35f9135233d41bab299beb75 *man/range_autofit.Rd e327f5d22bff656cc07b9166e7a6e9c3 *man/range_delete.Rd b2905ec9a9d70461f5db35f859c5fc04 *man/range_flood.Rd 9338b9bbfdf11b7cf0098b066047bd8f *man/range_read.Rd 9dbaf139ba2be062ad10a86225eab6c3 *man/range_read_cells.Rd d29eee81f264420c373a2d09a93fdc1c *man/range_speedread.Rd a1a4ee7a0249a057569c1d845a256dfd *man/range_write.Rd fa11971720e0c56f389f03126f476182 *man/request_generate.Rd 9c61db410ddaa6bc1359c8e1bf18f139 *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 ab57f86be351ff0270a5dceffda3bc72 *man/sheet_add.Rd 82656e126219f851170217deb0b816b0 *man/sheet_append.Rd 43e82c06b8498ad1980e4c489fcf43b0 *man/sheet_copy.Rd f41dad396d3c4a547e31579e186ed7ee *man/sheet_delete.Rd 94ead58a6761d6f34a4ca910f3e0c5cb *man/sheet_properties.Rd 464d2f62961d1bb69741eaa56a4a61a8 *man/sheet_relocate.Rd a0666c73022f5e6a723856a50e3e9ccf *man/sheet_rename.Rd fed158686d5af29d00059275649c730e *man/sheet_resize.Rd ea4880d796d0ed7be6d1d740e220e0cf *man/sheet_write.Rd 33a33aad413a98c7baf206ac7cd01fe0 *man/sheets_id.Rd 02e24048faa272faf7ed7b54a3caff70 *man/spread_sheet.Rd 50f330eeca8db092d6807e04457bd06d *tests/spelling.R 5c0f1bdb3f9d7e2d4b43b7f1cd47e346 *tests/testthat.R 0d7c4e08a3d47e980fef4ef4d12e4243 *tests/testthat/_snaps/gs4_auth.md cd87d1bfecf4562b730d832a2fbe354f *tests/testthat/_snaps/range_read.md a47c957b9c337372ad99df93a9f4463e *tests/testthat/_snaps/schemas.md c2259f0cb6d4c60f456cd3eb47552034 *tests/testthat/_snaps/sheet_add.md e2f730b3a17815f37dd736f5dcedf9b5 *tests/testthat/_snaps/sheets_id-class.md e3f608ef5ff1e65123f414c00d43eb37 *tests/testthat/_snaps/utils-ui.md 40369e41053518a89904eb3e136cfa68 *tests/testthat/helper.R 48d9cb72be2567c9f8d059754dedd117 *tests/testthat/ref/dribble.rds 97f6644357c0c618b0c0e3fd2c237947 *tests/testthat/ref/googlesheets4-cell-tests.rds 5990afc31782d015ae93bb99826f26b2 *tests/testthat/test-aaa.R ff4d645445c622acd1628d165b99f0bc *tests/testthat/test-ctype.R d7723f92ac617a14f16db4f00db38fd2 *tests/testthat/test-get_cells.R 144f6782ba882ff2a0c790168fbd6078 *tests/testthat/test-gs4_auth.R c6fb2c81423210f89dcfc8c3c02ffe07 *tests/testthat/test-gs4_endpoints.R 551a470e6e25348a5e138603aa859ae3 *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 3b3affd7a1b16eddc784f1fa4786ab04 *tests/testthat/test-range_autofit.R efa0e250ee9ce655574e899d0aac7c9e *tests/testthat/test-range_delete.R 6c2402b61a7d7098aeb9a1abd51f0b39 *tests/testthat/test-range_flood.R 18a28e089a867215e2619390931140aa *tests/testthat/test-range_read.R 0b7783dedde4ac97905d9144d639d17c *tests/testthat/test-range_read_cells.R b994c8d356476d98de54b50aa455a111 *tests/testthat/test-range_spec.R 0276e5a4ff5ee4949699052ea5d3601f *tests/testthat/test-range_speedread.R d79eed826750a4863db1c84dd32d5038 *tests/testthat/test-range_write.R c1f10389f0a844e54432d1f6b070eaa4 *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 001bb45da0b0f882fa0350644a3a499f *tests/testthat/test-schemas.R 8177e8b7fc40c985c8ee1ebe87d60bcb *tests/testthat/test-sheet_add.R 3c4c673671a570badd6a55178c701df5 *tests/testthat/test-sheet_append.R ae5312dfcfca7a1fdbb0e46a8a31bd63 *tests/testthat/test-sheet_copy.R 0e132af1e32733a3d85390edc59798f0 *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 5a74ff5f7540258dac423a4878ecec78 *tests/testthat/test-sheets_id-class.R c5e2637ba8626e43281c0b912d1e0e63 *tests/testthat/test-utils-cell-ranges.R c2eccd10a06fe58dfd2639fec9b63296 *tests/testthat/test-utils-sheet.R a93bddcd902072f7a3060d869f29e6ae *tests/testthat/test-utils-ui.R 3755fcfca1a8f6f1761617e7b9118809 *tests/testthat/test-utils.R googlesheets4/inst/0000755000176200001440000000000014074074641014012 5ustar liggesusersgooglesheets4/inst/secret/0000755000176200001440000000000014074074641015277 5ustar liggesusersgooglesheets4/inst/secret/googlesheets4-testing.json0000644000176200001440000000613314436453367022434 0ustar liggesusersNYY-QxuREQbMnhYDuY8gdE4Gnq-fsnTX_RNc3io7H6V68XQxkG2O89AppEfacLPG3odEYAkZF3N_qMqLXrStVKKD-7oTJSjBq0BCas-9IBblJMdYvOJm1m_WkO_CASG3rbXktC_dNUM6BrdoQoXSURhtVzQpGIANXTAuQnZbPmlaYKb79s5Emz-0ASQaIGLfdx6YzsXhV17HojqbwN2lN6CrjdSIzYwKnm9GeKEUD8eSj5g8VnF4VbfMbuZw0dzKHPKB6c1K9bxkc4QULtlK_B_zl-vMKQZKpqb7EhIHSaw7_6zNDAeHyKms2JxFG7gSWErHNl6HbS8biLP-7w8yRGjQTqL-WcXK3jk57-fCjUbSzfjqEWbfI3p3wK6jk34hXy5z0-4zG8nGj8-OSXk1R8ZLAWP44NToQcMcPMnbBgjtkdehskLEDT9yIa1jJ26OIXzL0w-bfnDB3dlvhl3Km0be1mEUx-U9A0eDQzER38w1RILUgxYvf3giE14SFF_ohGx-zh3HJz8UpZR2OpMviFti5ZJjBqXn-gGyrPScvRTCjktqaadLcTZUmoCV3QoJeqn893MIUveuNpME4BNZT2q6NeTAKaj2yZBjxOQd2JTbO_Y_QCavsOBS_0FF7g8wBa0S73_KJtKRM02116Ut8g7ehtdK5RqcRx8zRLne9D5w-x3efsXpozCCKtZmKLi5jpngXShYU7JMyoRLHdoL7MbNlr5HQCY4R3PtVnjJLy1WU4u61FqzaoNDasrjQphZ7sMgQDvsS18ff0KY_dEbvLRkbXF304pqhs8S3N1St7afJ6AV2qvuEbx3Yt5F1RZzGfTf9r3N_ozl4RyhawFwBf1DC0hhQCbt8dhAjpxFa4PA6jsL6E5fmWbA-ehRj9ieK6nwH7v27qXpP792fpw3E8kACuEKSQK_Qg2ABHi2fbAjjf-jBebi_z8D92LTdbtJZ2oUAKreTT255tWNpgsHuafOQRQAvEHAiNB7K8cxwgbWsHAumhLTzRbGkwHHdtc3lcr9yxjlFnTJov6zWapv2Fy3K1SlUYcbvAO5Lp8Ch6J5oPmtzYggi0sLQ2rwbvB7IxHVk_-QrwYckpMftUpzthYEKAnwYnNrftyfHCzo1MjgL4iSyaa-3LnDReAffuiTwvZyejeGcsIrhlmj2hDqMSOond0VJSvp46PMwlXiuo3nzWh7C3J1pz8fNVu11SxQOFHih5tvVcS3ybj-1iexwqsMe0_wt2v03XC8IzKWpUnYSHiKMvcOPnwOxgENQaYs5rLl4jJGEc4h3F1lp6iqtbJ9q8_x4TQh5PhNWr6S4l2-ZL2bPbq6yyON9HeqCwIC2cF-xJdaGGSfUn1DQi32bhSFUXzvIFA8XDH35_z-fDgVGI2mZMvNLSBatNLncODutsXVYSJReS5QnjPXRzeoBvClAGVsH-KpD4pe2xrX1Wnuvbe_a6rdHXYJdwM39Q_8S7tcAcTC9PzuWF4hclXgegLBQuTHlcvRFrM_GJU18gQm73oxtae087nsZN70v1AKhHMvRLXxtz74y-Bkns_4qn6xgEVIRdDJAq8By3vzEcXwm4Kjr7gHJ1EFDzQ-IocS4UePBvo_DQU6Y43wffpRhhAwZQ7ANLx3J9h_R3uuk2dlSbl727Zt1Pgv4jiDcLDEpLZ4ThtYF3PyYbwmz-mpPHGe6SkSGDBcsGdPGrl36IsRSS8A8Dyi9i4ZlPCRGoYyX1T-1WUTIesr8AqbLwmOAjTtKup6QijUQoJY7_nkBB3vUQnSr_5AvpShsZWrt_XQg-whk72WHVIU8aFnlBN4bnZiIFfhP2C05-CsiB8QXloiW1Mad5JQJlwdz8Ld7Cn-uVKiOB_m2UvHoGc8ScbcWpBEwnb5T3CnrD9kztDg-yFk8fuojtv9WStgahULMmIlN3HTKIZlZwW-ZFhgaIVv4wt5ENEBzd5jWXf6czus5LyBOvix-nGekOFOkszfMBqCVkCtcDXQIr-cCt-PDuYM9itNdPr7OVewXY-vOFKWNFHVYfgfKRIpcgQHUgSgTAqfH5IIVklRx3IWse-3pdA7_RWw76f8Ey5xIsmtrFZ8C2WUw1XFCHG9MPe6n41S9Uy9k-6N1LUbJI2DWSZeEcvVFRw56FLUUCU9zRXLIDNjR9sA6gdHdnaDjb20sU34VnzSebZyty9OiGW3oiTk-iR1Y9_O8Lt8_nI4QKWRvit0T4EkYBmo92XQNSuMMcKe14NZ6EC710G0V3GNiEXDp6j0e92eTDjNA28uijnsqeQRs0goxoXU4Xn8qQ8RdiY-R0JqX3Uabv1d_yxDB1uVMs4W4zCw9-LDh_4On40lSGap8y0UMF96pzy4AkDnJqvScVkraWy1JH5OFdwUkL3qD2FLu8yv0oR5DcsBrM0tgRwgz-D3_B83wjbrXHj2v0GhANLKFoDnNMp1hoXcsdnlFKAlB6bh33yUsVMbMpBPrbqETi037RakvPHbSaRn13avlpWkywDWz7CmVCWgHlRK6Lp8pjTtuZIHzgv2b7_1faUjeHRnT0i473CWVriLpcGAVORLIOUqhv--YFE2MHo8o_tCzr3NDcTnEAQtBbhc2AUqvu1ivrV_HE-bAVxok-aJr4pLbpQDk_h3wn2-YIeZYA_yqdozQul4MwgXy_MjXExC563TKWkmn5cWhnfinhUAwqU6stQYD5EsIBHbvOB3Ue43B2-m63NX9eR1B3TTyWll37mqwVV8mXwulOYRlzBx5U0NulzsJVGrj_g1t-df0dKvv0Nw5wn2wPPi_JOTs40nHzGiLxg7aAHm4bVdvLGjaHuZO1VAgS91Psc15hcGLci6ogvSoyv_4ujiqvRh5A-Trd-Pz7ZPAbUh-XpDEKhpvk7KO9BwsFEPmiruWBTA7al8L__c3E83iBtP7UIVg7ZksulsXgWamkFp7FAenmXQ64-wxlamA9L2CuyTQN1_J7bFl_sq9xpPDkbU0VdwN-DBwXUXbHMXMNBnPqTuUjY07Aeq6p9YaXZeHQt8SoBj-E0yw1kMFGC1sdb5X3pG0IADvOHNR4mjpxRCXtzCHdehw7GFaw4Tmu01lXsW4gF2aKEn7TgvhI8-j376D5StTdieKtCYqqbP7BhiavLLJX1IkpKCZH1nEWgqa02_s0ERzeOMujrZPggooglesheets4/inst/secret/googlesheets4-docs.json0000644000176200001440000000612314436453411021674 0ustar liggesusersUwB0P_2uNxrvPSRfIRMi9LLseYrCR9rawfHNKaF0MPGHabvkUksG7YCqAM_GKew8JLohiAXWizEFKbpB8bA4NUjdS-tVK9912sxMNuZTVu9-bJvCbRdENxZCtxMzFkkWNr1VYeulCx3EkaoMuGu-8FQBRZpc599J5xX6teffD39AS6a1y-Lo-9Iaux3J0nX0Yw-dxrUR0Tvbz19U4Bm5nOfsDFnbURCrA9peT9IiEdqruX1brbJDGzFMg_q0w1fRTO3sDz0CL8M__Qj4opvCqWijybzcNyPfFJKs30OVNvdv0n6dvydGDnOONKy9hlDmtnVXcJqUMLPBiyV1T4FzWpDTj2EpIeqMHhXqx0hnsCm_Ua1SZke__lWFKpx2tlrvZ2UM7DOJHJvzS84ZEE5Ar_m5EukZx4MkgEzLSJYhbBvYaLfc5rBzfGOivvnSWtOZ2CVq0NRj8dPfUKLpYdqkRMAyJEzQ8oIUWRiBevrJz6h5bLpjMufIkGovsYKBa_hjBhI-tuaoqjUkqcepgsRxtfon5YYeXelmRTQZ8zy8rahDsCWugm1dKgSFkjlJZ_wzZiRfPwXWrbFjs0Zekad0G3bdwXIVTmCHAk4QRJnpWsN8k6OxCPfZh2PTZih7vZzqxSZUe-ajuxhnpzIfRiZuLbi3JeP8zX3alwfQWjCZH-uB--2rr7Z_z7wOQexH77quSXlm_FHY0jFOahZzEKpmpYTQAMjza-6LO96Ge0hYrFxidQMavi33zUH-0cCwX-dLtaFgAXCx4feP_G5sYiW9lLjCKiPhXGjs-pYYcSn89FAVcAaJwnfTxrRKkP-I8Wv4zL19MlNB8j7FzGPk2js4BzSel_MFkfBOr5UjDL_zDQpK0zwAzNRXbzRQmzc8nr5gSQDHTklDLSk0F10CgfnbUn1T3MfVRehhFocGOJRngL1rBvdwmSKzrpn9fe6-8ScLhFXvjHUsClUB4Xk9Dnr188a441VMRa8Ah1mGmCEQnF-uPs_pfIuSZ04Mwi611Zn2JF6neQtMkcYTVCuY3T0o_sksGMWre4pFM-qmy6W-KYrgj5yLcdwfVpzneRGUgDOGz2jhxyLTraagdjZz9Zmz2nvRw-T43US-5d8xLqE88sXQ2bnDU0mXL6gnb-Lu1FEjti006AYZTnyGgxV6ZOYhwTEBqINENBE_2cjBc12I9VHHOP_mVBsFUPp715dESugEaGpT2JvmRWjWDE8JqXQYulNvSjwx61HBwmrhzF0EmTZEfzqHGD1ZGknfjClgic0dVaY2LNO1wumnRHpr6P5avbc64B2QGBzmDnkywlGAPpTSQu99bGQYGR7w992jxdWpN1omQPR-G2mNsxON5Et-FEtAvM6sjikvlmRt6DgAGueXrKZW4ge8CiUO9b1AzHB_2lhn_lHoZ6wSTnL4M34CjUy6mDEDYaUv1WGDAv-Ht-H8Ylr1xqJbEiJ9_WenagQwD25dgdod2ThqACOPhbGLmPxkPX1Egp0VkMr3MGsbHvkfM__AyiPvEBSybq7nduzZNTbWZrdzUQpLl3jSmiGQLstirblWRI6to0hSYAqxRjs0KPTpkDgyl5EhNUJcxczRuMaYfCUaActWqVzFg_BLhZ-daytvxdHPSUdI5_GQUIzDD7FgQb67cCjRuKth2aMM5uiIIqPgsXigUy76jXgoYX3eid4Z6chtmZBrVQ4sm15N5CXE1jxVLW0RUmzDsOpJKnaLSmJNxQYU8cPPOqdyKV55VkR2Zkd59AMCwvLM6QdtDkjJ-YxAYHvcS8kQFCRDAf3cWfXRwz81QLxlAdp4gGoXlYUPR92cz_1J0hdYfwb6JePIWiTEsv2xBvjWtTVIQ9lH4r6RHDvqZtY1VOMiFIMoDGSs85kXP3EmY6QxYYltTMkF7NSopM_ZaoAXRRLqTFatK4Y6TsbpcugPUfO_GElC9sfzSPpyj6TUVrQFOMtK8HGR_ANme3u8WEpCj6QSVP2Y1kxeQsO9cHK4BcvpvdrrLfRbw-4tKSbo3t9SwIOzKsbx5VK37tWjaeVsINCfyTNOTZBGgMhVsdfoAp86tb0kCGv_rcbbVd81DIy0Ra4BHBCxk71tfG3BqtL0ZrQWuyv5t1-YiqodoZbMWkhFiI66ilIn_CTVCzL528DJ53Ygs9wBE1EADnqHAh1uibVcFxnAds6uU6F7ka3YReMqcbWkgPvE7XRbe3Agvxz7S034LM4ClPMrwuUwxOYCrJVr6Ph_0zn3AormLusoqkipuV8pCV3H28_uxlgcjtp7TueO_rAtK4fFN0r-oBddAl0pBo-j1rYu4nNWtUYkc53hp7nzmTI09usC4CopJwSZ4JAvq95abwXT7_E6eLeIYUssdIpQ4Mol6M0Fnka6nYiW8yQF-bTj2PT3QVgkJMp-kPtXWaDWoXEtnxHuXoJUxiL8wRooR5lGTSpFjbPEmtR6_z8rXx8UKOTmMrd-6YkQyUavzsztbdCAF_BF7DKrPfQsDJ9DyL2E79c8NuWG8OgvEaeLPD4cU0eLWYCJV4zLXFdp7Bhh-p1sWyrFCRWN5yghpjk9geuszPuXAvw9K6iB6V0bGO4esWNkAr4tuet-OpNVI3EZLBPb64DqZaN0JQR6U0pzS06Jve1MyGMnMDz4nR2fRMmyuMcMQ7bPs6sWyfVijq-GAJGywRz8Ppd7EMd1XJYiHUXZFNJFdTHedpPNlyrGkE0sGYSJR8oje3FD7aqw6PiDnpDYI6MHQc1xAJQ7yl91cERdMtAVobfcNAXgG6RrNil5n0g2w_ql3T1iIT59bhgaCCfcUzSP-VBEXen3Bs5mf_hatpSbEviW7du66jcKxGdJxdgMEyQjAqgQ1T5rJOwtrBYTGoJ09Y4vQx1HbTbdKyX_I0pDmdjbNjfvW1zToaoesbQjFo1XbJ1uvO1oc8NtEvuzIl5Zq0H3XBUrrp_p0HpOabcfrkmSZZgpnpRT9jP3ypgSqX7_0nLNY9ac8w05TaHPI1Kg27iqYjPHdf93yVdHAC5ZoG4tmupG3Wx1jx8GfBW1uY9PiRUrptnJMXT6jei14hOE34WeGj5qSuk23xvTlIZiaBRJE52I9XkufSReuUKZsj4yFhxvz70nFx6iTrbuyaywJSLOLe_NFr2J61jhDTp3cwbTmwgooglesheets4/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/WORDLIST0000644000176200001440000000127614440463142015205 0ustar liggesusersAPI's Auth AuthState CLI CMD Cheatsheet Codecov Colaboratory Computerphile's Datetime Datetimes Feuille Gapminder IDEs JSON OAuth OOB ORCID PBC POSIXct RStudio SheetN Shortcodes Timezones UI UpperCamelCase auth autogenerates backoff behaviour bigrquery cci cellranger cheatsheet chickwts cli cli's csv datetime datetimes de dev funder gamechanger gapminder gmailr googleapis googledrive googlesheets https httr httr's js lubridate lubridate's noninteractively ny pkgdown pre programmatically readonly 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 www xls xlsx