Skip to content

Commit 6c34012

Browse files
authored
Merge pull request #29 from TidierOrg/write_gsheet
`write_gsheet`
2 parents 525a550 + 30b6d04 commit 6c34012

File tree

4 files changed

+99
-5
lines changed

4 files changed

+99
-5
lines changed

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "TidierFiles"
22
uuid = "8ae5e7a9-bdd3-4c93-9cc3-9df4d5d947db"
33
authors = ["Daniel Rizk <[email protected]> and contributors"]
4-
version = "0.3.0"
4+
version = "0.3.1"
55

66
[deps]
77
Arrow = "69666777-d1a9-59fb-9406-91d4454c9d45"

src/TidierFiles.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ using Sockets
1919
export read_csv, write_csv, read_tsv, write_tsv, read_table, write_table, read_delim, read_xlsx, write_xlsx,
2020
read_fwf, write_fwf, fwf_empty, fwf_positions, fwf_positions, read_sav, read_sas, read_dta, write_sav, write_sas,
2121
write_dta, read_arrow, write_arrow, read_parquet, write_parquet, read_csv2, read_file, write_file, read_rdata, list_files,
22-
read_gsheet, connect_gsheet
22+
read_gsheet, connect_gsheet, write_gsheet
2323

2424

2525
include("docstrings.jl")

src/docstrings.jl

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ Write a DataFrame to a CSV (comma-separated values) file.
201201
- `missing_value`: = "": The string to represent missing values in the output file. Default is an empty string.
202202
- `append`: Whether to append to the file if it already exists. Default is false.
203203
- `col_names`: = true: Whether to write column names as the first line of the file. Default is true.
204-
- `eol`: = "\n": The end-of-line character to use in the output file. Default is the newline character.
204+
- `eol`: The end-of-line character to use in the output file. Default is the newline character.
205205
- `num_threads` = Threads.nthreads(): The number of threads to use for writing the file. Default is the number of available threads.
206206
207207
# Examples
@@ -223,7 +223,7 @@ Write a DataFrame to a TSV (tab-separated values) file.
223223
- `missing_value`: = "": The string to represent missing values in the output file. Default is an empty string.
224224
- `append`: Whether to append to the file if it already exists. Default is false.
225225
- `col_names`: = true: Whether to write column names as the first line of the file. Default is true.
226-
- `eol`: = "\n": The end-of-line character to use in the output file. Default is the newline character.
226+
- `eol`: The end-of-line character to use in the output file. Default is the newline character.
227227
- `num_threads` = Threads.nthreads(): The number of threads to use for writing the file. Default is the number of available threads.
228228
229229
# Examples
@@ -416,6 +416,7 @@ julia> read_sas("test.xpt")
416416
─────┼──────────────────
417417
1 │ sav 10.1
418418
2 │ por 10.2
419+
```
419420
"""
420421

421422
const docstring_read_sav =
@@ -766,4 +767,26 @@ julia> read_gsheet(public_sheet, sheet="Class Data", n_max=5)
766767
4 │ Becky Female 2. Sophomore SD Art Baseball
767768
5 │ Benjamin Male 4. Senior WI English Basketball
768769
```
770+
"""
771+
772+
const docstring_write_gsheet =
773+
"""
774+
write_gsheet(data::DataFrame, spreadsheet_id::String; sheet::String="Sheet1", range::String="", missing_value::String = "", append::Bool = true)
775+
776+
Writes the contents of a DataFrame to a specified Google Sheets spreadsheet.
777+
778+
# Arguments
779+
- `data::DataFrame`: The DataFrame containing the data to be written to Google Sheets.
780+
- `spreadsheet_id::String`: The ID of the Google Sheets spreadsheet or the full URL containing the ID.
781+
- `sheet::String`: The name of the sheet within the spreadsheet where the data will be written. Defaults to "Sheet1".
782+
- `range::String`: The range in the sheet where the data will be written. If empty, defaults to "A1".
783+
- `missing_value::String`: The value to replace missing entries in the DataFrame. Defaults to an empty string.
784+
- `append::Bool`: If true, appends the data to the existing data in the sheet. If false, overwrites the existing data. Defaults to true.
785+
786+
# Examples
787+
```
788+
julia> df = DataFrame(A=1:5, B=["a", missing, "c", "d", "e"], C=[1.1, 2.2, 3.3, 4.4, 5.5]);
789+
790+
julia> write_gsheet(df, full, sheet = "sheet2", append = false)
791+
```
769792
"""

src/gsheets.jl

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,4 +168,75 @@ function read_gsheet(spreadsheet_id::String;
168168

169169
return df
170170
end
171-
171+
172+
"""
173+
$docstring_write_gsheet
174+
"""
175+
function write_gsheet(data::DataFrame, spreadsheet_id::String; sheet::String="Sheet1", range::String="", missing_value::String = "", append::Bool = false)
176+
# URL-escape spreadsheet_id if necessary by extracting it from a full URL.
177+
if occursin("spreadsheets/d/", spreadsheet_id)
178+
m = match(r"spreadsheets/d/([^/]+)", spreadsheet_id)
179+
if m !== nothing
180+
spreadsheet_id = m.captures[1]
181+
end
182+
end
183+
184+
# Use a default range if none is provided.
185+
if isempty(range)
186+
range = "A1"
187+
end
188+
189+
# If appending, use only the sheet name; if not, use "sheet!range".
190+
loc = append ? sheet : sheet * "!" * range
191+
loc = HTTP.escapeuri(loc)
192+
193+
headers = ["Authorization" => "Bearer $(GSHEET_AUTH[].access_token)", "Content-Type" => "application/json"]
194+
195+
# Convert the DataFrame to a JSON object replacing missing values.
196+
col_names = [string(c) for c in names(data)]
197+
rows_data = [map(x -> ismissing(x) ? missing_value : x, collect(row)) for row in eachrow(data)]
198+
# If appending, do not include the header; otherwise, prepend the header.
199+
rows = append ? rows_data : vcat([col_names], rows_data)
200+
body = Dict("values" => rows)
201+
202+
if append
203+
# For appending data, use the append endpoint with POST.
204+
url = "https://sheets.googleapis.com/v4/spreadsheets/$spreadsheet_id/values/$loc:append?valueInputOption=USER_ENTERED&insertDataOption=INSERT_ROWS"
205+
response = HTTP.post(url, headers, JSON3.write(body))
206+
else
207+
# For updating (overwriting) data, use the update endpoint with PUT.
208+
url = "https://sheets.googleapis.com/v4/spreadsheets/$spreadsheet_id/values/$loc?valueInputOption=USER_ENTERED"
209+
response = HTTP.put(url, headers, JSON3.write(body))
210+
end
211+
212+
if response.status != 200
213+
error("Failed to write to Google Sheets: $(String(response.body))")
214+
end
215+
216+
# If not appending, clear out any cells below the new data.
217+
if !append
218+
# Determine how many rows were written (including header).
219+
new_N = length(rows)
220+
# Helper function: convert a 1-indexed column number to its corresponding letter.
221+
function col_letter(n::Int)
222+
s = ""
223+
while n > 0
224+
rem = (n - 1) % 26
225+
s = Char(rem + 'A') * s
226+
n = (n - 1) ÷ 26
227+
end
228+
return s
229+
end
230+
last_col = col_letter(length(col_names))
231+
# Build a clear range from the row after new data to a high row (here, row 1000).
232+
clear_range = "$(sheet)!A$(new_N+1):$(last_col)1000" # note the parentheses around sheet
233+
clear_range = HTTP.escapeuri(clear_range)
234+
clear_url = "https://sheets.googleapis.com/v4/spreadsheets/$spreadsheet_id/values/$clear_range:clear"
235+
clear_response = HTTP.post(clear_url, headers, "{}")
236+
if clear_response.status != 200
237+
error("Failed to clear remaining cells: $(String(clear_response.body))")
238+
end
239+
end
240+
241+
return response
242+
end

0 commit comments

Comments
 (0)