Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
51 changes: 50 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,55 @@ 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
presently 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
return

data_range = (
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
51 changes: 51 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,57 @@ 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, "Sheet4")

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},
]
)

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

result_table = self.google_sheets.read_worksheet(self.spreadsheet_id, 3)
results_dict = result_table.to_dicts()

self.assertEquals(results_dict[0]["col4"], 4)
self.assertEquals(results_dict[1]["col4"], 8)
self.assertEquals(results_dict[2]["col4"], 12)
self.assertEquals(results_dict[3]["col4"], 16)

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)