1+ from dataclasses import dataclass
2+ import typing
13import gspread
24import gspread_formatting
35from enum import Enum
4- from googleapiclient .discovery import build
5- import numpy as np
66
77FONT_SIZE_PTS = 10
88PTS_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+
1920class WORKSHEET_OVERRIDE_BEHAVIORS (Enum ):
2021 OVERRIDE = 1
2122 EXIT = 2
2223
24+
2325class 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+
2834DEFAULT_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