Skip to content

Commit c124ac9

Browse files
author
jpaten
committed
feat: basic functions to add charts to google sheets in analytics api (#4353)
1 parent efc0ff8 commit c124ac9

File tree

2 files changed

+141
-9
lines changed

2 files changed

+141
-9
lines changed

analytics/analytics_package/analytics/api.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@
3030
{},
3131
)
3232

33+
sheets_service_params = (
34+
["https://www.googleapis.com/auth/spreadsheets"],
35+
"sheets", "v4",
36+
{}
37+
)
38+
3339
next_port = None
3440
default_service_system = None
3541

@@ -291,7 +297,7 @@ def build_params(source, subs):
291297

292298

293299
def results_to_df(results):
294-
300+
295301
df = pd.DataFrame()
296302
for result in results:
297303
# Collect column nmes

analytics/analytics_package/analytics/sheets_api.py

Lines changed: 134 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1+
from dataclasses import dataclass
2+
import typing
13
import gspread
24
import gspread_formatting
35
from enum import Enum
4-
from googleapiclient.discovery import build
5-
import numpy as np
66

77
FONT_SIZE_PTS = 10
88
PTS_PIXELS_RATIO = 4/3
@@ -16,15 +16,21 @@ class FILE_OVERRIDE_BEHAVIORS(Enum):
1616
EXIT_IF_IN_SAME_PLACE = 2
1717
EXIT_ANYWHERE = 3
1818

19+
1920
class WORKSHEET_OVERRIDE_BEHAVIORS(Enum):
2021
OVERRIDE = 1
2122
EXIT = 2
2223

24+
2325
class COLUMN_FORMAT_OPTIONS(Enum):
2426
DEFAULT = 1
2527
PERCENT_UNCOLORED = 2
2628
PERCENT_COLORED = 3
2729

30+
31+
class CHART_TYPES(Enum):
32+
LINE = "LINE"
33+
2834
DEFAULT_SHEET_FORMATTING_OPTIONS = {
2935
"bold_header": True,
3036
"center_header": True,
@@ -41,7 +47,7 @@ def authenticate_gspread(authentication_response):
4147
gc = gspread.authorize(extract_credentials(authentication_response))
4248
return gc
4349

44-
def authenticate_drive_api(authentication_response):
50+
def authenticate_google_api(authentication_response):
4551
"""Authenticates the Drive API using the response from api.authenticate"""
4652
return authentication_response[0]
4753

@@ -107,21 +113,21 @@ def search_for_folder_id(drive_api, folder_name, allow_trashed = False, allow_du
107113
return [file["id"] for file in files_exact_match]
108114

109115

110-
def create_sheet_in_folder(authentication_response, sheet_name, parent_folder_name=None, override_behavior=FILE_OVERRIDE_BEHAVIORS.EXIT_ANYWHERE):
116+
def create_sheet_in_folder(drive_authentication_response, sheet_name, parent_folder_name=None, override_behavior=FILE_OVERRIDE_BEHAVIORS.EXIT_ANYWHERE):
111117
"""
112118
Create a new sheet in the project with the given name and parent folder.
113119
Returns the new sheet.
114120
115-
:param authentication_response: the service parameters tuple
121+
:param drive_authentication_response: the service parameters tuple
116122
:param sheet_name: the name of the new sheet
117123
:param parent_folder_name: the name of the parent folder for the new sheet
118124
:param override_behavior: the behavior to take if the sheet already exists
119125
:returns: the gspread.Spreadsheet object of the new sheet
120126
:rtype: gspread.Spreadsheet
121127
"""
122128
# Build Drive API
123-
gc = authenticate_gspread(authentication_response)
124-
drive_api = authenticate_drive_api(authentication_response)
129+
gc = authenticate_gspread(drive_authentication_response)
130+
drive_api = authenticate_google_api(drive_authentication_response)
125131
parent_folder_id = None if parent_folder_name is None else search_for_folder_id(drive_api, parent_folder_name)[0]
126132

127133
# Check if sheet already exists and handle based on input
@@ -309,4 +315,124 @@ def fill_spreadsheet_with_df_dict(sheet, df_dict, overlapBehavior, sheet_formatt
309315
sheet, df, worksheet_name, overlapBehavior,
310316
sheet_formatting_options=sheet_formatting_options.get(worksheet_name, DEFAULT_SHEET_FORMATTING_OPTIONS),
311317
column_formatting_options=column_formatting_options.get(worksheet_name, {})
312-
)
318+
)
319+
320+
def update_sheet_raw(sheets_authentication_response, sheet, *updates):
321+
"""
322+
Directly call the Google Sheets api to update the specified sheet with the optional arguments.
323+
"""
324+
assert len(updates) > 0
325+
sheets_api = authenticate_google_api(sheets_authentication_response)
326+
sheet_id = sheet.id
327+
body = {"requests": list(updates)}
328+
response = (
329+
sheets_api.spreadsheets()
330+
.batchUpdate(spreadsheetId=sheet_id, body=body)
331+
.execute()
332+
)
333+
return response
334+
335+
REQUIRED_CHART_ARGS = []
336+
337+
DEFAULT_CHART_ARGS = {
338+
"title": "",
339+
"x_axis_title": "",
340+
"y_axis_title": "",
341+
"chart_position": None # Means it will be created in a new sheet
342+
}
343+
344+
@dataclass
345+
class WorksheetRange:
346+
worksheet: gspread.worksheet.Worksheet
347+
top_left: gspread.cell.Cell
348+
bottom_right: gspread.cell.Cell
349+
350+
@property
351+
def range_dict(self):
352+
return {
353+
"sheetId": self.worksheet.id,
354+
"startRowIndex": self.top_left.row - 1,
355+
"endRowIndex": self.bottom_right.row - 1,
356+
"startColumnIndex": self.top_left.col - 1,
357+
"endColumnIndex": self.bottom_right.col - 1,
358+
}
359+
360+
def _cell_to_grid_coordinate(cell, worksheet):
361+
return {
362+
"sheetId": worksheet.id,
363+
"rowIndex": cell.row - 1,
364+
"columnIndex": cell.col - 1,
365+
}
366+
367+
def add_chart_to_sheet(sheets_authentication_response, sheet, worksheet, chart_type, domain, series, **chart_args):
368+
complete_chart_args = {**DEFAULT_CHART_ARGS, **chart_args}
369+
print(worksheet.id)
370+
if complete_chart_args["chart_position"] is not None:
371+
position_dict = {
372+
"overlayPosition": {
373+
"anchorCell": _cell_to_grid_coordinate(complete_chart_args["chart_position"], worksheet)
374+
}
375+
}
376+
else:
377+
position_dict = {"newSheet": True}
378+
formatted_domains = [
379+
{
380+
"domain": {
381+
#TODO: would be nice to also support column references https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/other#DataSourceColumnReference
382+
"sourceRange": {
383+
"sources": [
384+
domain.range_dict
385+
],
386+
},
387+
},
388+
},
389+
]
390+
formatted_series = [
391+
{
392+
"series": {
393+
"sourceRange": {
394+
"sources": [
395+
series_source.range_dict
396+
],
397+
},
398+
},
399+
"targetAxis": "LEFT_AXIS",
400+
}
401+
for series_source in series
402+
]
403+
formatted_axis = []
404+
if complete_chart_args["x_axis_title"]:
405+
formatted_axis.append({
406+
"title": complete_chart_args["x_axis_title"],
407+
"position": "BOTTOM_AXIS",
408+
})
409+
if complete_chart_args["y_axis_title"]:
410+
formatted_axis.append({
411+
"title": complete_chart_args["y_axis_title"],
412+
"position": "LEFT_AXIS",
413+
})
414+
print(formatted_domains)
415+
print(formatted_series)
416+
request = {
417+
"addChart": {
418+
"chart": {
419+
"spec": {
420+
"title": complete_chart_args["title"],
421+
#TODO: insert legend position
422+
#TODO: insert axis positions
423+
"basicChart": {
424+
"axis": formatted_axis,
425+
"chartType": chart_type.value,
426+
"domains": formatted_domains,
427+
"headerCount": 1, #TODO: not sure what this means
428+
"series": formatted_series,
429+
},
430+
},
431+
"position": position_dict
432+
},
433+
},
434+
}
435+
print(request)
436+
437+
response = update_sheet_raw(sheets_authentication_response, sheet, request)
438+
return response

0 commit comments

Comments
 (0)