Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding a data paste function to Google Sheets #1045

Merged
merged 9 commits into from
May 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 58 additions & 1 deletion parsons/google/google_sheets.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import logging

from parsons.etl.table import Table
from parsons.google.utitities import setup_google_application_credentials
from parsons.google.utitities import setup_google_application_credentials, hexavigesimal

import gspread
from google.oauth2.service_account import Credentials
Expand Down Expand Up @@ -293,6 +293,63 @@ def append_to_sheet(
sheet.update_cells(cells, value_input_option=value_input_option)
logger.info(f"Appended {table.num_rows} rows to worksheet.")

def paste_data_in_sheet(
self, spreadsheet_id, table, worksheet=0, header=True, startrow=0, startcol=0
):
"""
Pastes data from a Parsons table to a Google sheet. Note that this may overwrite
anzelpwj marked this conversation as resolved.
Show resolved Hide resolved
presently existing data. This function is useful for adding data to a subsection
if an existint sheet that will have other existing data - constrast to
`overwrite_sheet` (which will fully replace any existing data) and `append_to_sheet`
(whuch sticks the data only after all other existing data).

`Args:`
spreadsheet_id: str
The ID of the spreadsheet (Tip: Get this from the spreadsheet URL).
table: obj
Parsons table
worksheet: str or int
The index or the title of the worksheet. The index begins with 0.
header: bool
Whether or not the header row gets pasted with the data.
startrow: int
Starting row position of pasted data. Counts from 0.
startcol: int
Starting column position of pasted data. Counts from 0.
"""
sheet = self._get_worksheet(spreadsheet_id, worksheet)

number_of_columns = len(table.columns)
number_of_rows = table.num_rows + 1 if header else table.num_rows

if not number_of_rows or not number_of_columns: # No data to paste
logger.warning(
f"No data available to paste, table size "
f"({number_of_rows}, {number_of_columns}). Skipping."
)
return
anzelpwj marked this conversation as resolved.
Show resolved Hide resolved

# gspread uses ranges like "C3:J7", so we need to convert to this format
data_range = (
anzelpwj marked this conversation as resolved.
Show resolved Hide resolved
hexavigesimal(startcol + 1)
+ str(startrow + 1)
+ ":"
+ hexavigesimal(startcol + number_of_columns)
+ str(startrow + number_of_rows)
)

# Unpack data. Hopefully this is small enough for memory
data = [[]] * table.num_rows
for row_num, row in enumerate(table.data):
data[row_num] = list(row)

if header:
sheet.update(data_range, [table.columns] + data)
else:
sheet.update(data_range, data)

logger.info(f"Pasted data to {data_range} in worksheet.")

def overwrite_sheet(
self, spreadsheet_id, table, worksheet=0, user_entered_value=False, **kwargs
):
Expand Down
26 changes: 26 additions & 0 deletions parsons/google/utitities.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,29 @@ def setup_google_application_credentials(
creds_path = credentials

os.environ[env_var_name] = creds_path


def hexavigesimal(n: int) -> str:
"""
Converts an integer value to the type of strings you see on spreadsheets
(A, B,...,Z, AA, AB, ...).

Code based on
https://stackoverflow.com/questions/16190452/converting-from-number-to-hexavigesimal-letters

`Args:`
n: int
A positive valued integer.

`Returns:`
str
The hexavigeseimal representation of n
"""
if n < 1:
raise ValueError(f"This function only works for positive integers. Provided value {n}.")

chars = ""
while n != 0:
chars = chr((n - 1) % 26 + 65) + chars # 65 makes us start at A
n = (n - 1) // 26
return chars
76 changes: 76 additions & 0 deletions test/test_google/test_google_sheets.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,82 @@ def test_append_user_entered_to_spreadsheet(self):
self.assertEqual(formula_vals[0], "27")
self.assertEqual(formula_vals[1], "Budapest")

def test_paste_data_in_sheet(self):
# Testing if we can paste data to a spreadsheet
# TODO: there's probably a smarter way to test this code
self.google_sheets.add_sheet(self.spreadsheet_id, "PasteDataSheet")

paste_table1 = Table(
[
{"col1": 1, "col2": 2},
{"col1": 5, "col2": 6},
]
)
paste_table2 = Table(
[
{"col3": 3, "col4": 4},
{"col3": 7, "col4": 8},
]
)
paste_table3 = Table(
[
{"col1": 9, "col2": 10},
{"col1": 13, "col2": 14},
]
)
paste_table4 = Table(
[
{"col3": 11, "col4": 12},
{"col3": 15, "col4": 16},
]
)

# When we read the spreadsheet, it assumes data is all strings
expected_table = Table(
[
{"col1": "1", "col2": "2", "col3": "3", "col4": "4"},
{"col1": "5", "col2": "6", "col3": "7", "col4": "8"},
{"col1": "9", "col2": "10", "col3": "11", "col4": "12"},
{"col1": "13", "col2": "14", "col3": "15", "col4": "16"},
]
)

self.google_sheets.paste_data_in_sheet(
self.spreadsheet_id,
paste_table1,
worksheet="PasteDataSheet",
header=True,
startrow=0,
startcol=0,
)
self.google_sheets.paste_data_in_sheet(
self.spreadsheet_id,
paste_table2,
worksheet="PasteDataSheet",
header=True,
startrow=0,
startcol=2,
)
self.google_sheets.paste_data_in_sheet(
self.spreadsheet_id,
paste_table3,
worksheet="PasteDataSheet",
header=False,
startrow=3,
startcol=0,
)
self.google_sheets.paste_data_in_sheet(
self.spreadsheet_id,
paste_table4,
worksheet="PasteDataSheet",
header=False,
startrow=3,
startcol=2,
)

result_table = self.google_sheets.get_worksheet(self.spreadsheet_id, "PasteDataSheet")
self.assertEqual(result_table.to_dicts(), expected_table.to_dicts())

def test_overwrite_spreadsheet(self):
new_table = Table(
[
Expand Down
13 changes: 13 additions & 0 deletions test/test_google/test_utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,16 @@ def test_credentials_are_valid_after_double_call(self):
actual = fsnd.read()
self.assertEqual(self.cred_contents, json.loads(actual))
self.assertEqual(ffst.read(), actual)


class TestHexavigesimal(unittest.TestCase):

def test_returns_A_on_1(self):
self.assertEqual(util.hexavigesimal(1), "A")

def test_returns_AA_on_27(self):
self.assertEqual(util.hexavigesimal(27), "AA")

def test_returns_error_on_0(self):
with self.assertRaises(ValueError):
util.hexavigesimal(0)
Loading