2
2
import gspread_formatting
3
3
from enum import Enum
4
4
from googleapiclient .discovery import build
5
+ import numpy as np
6
+
7
+ FONT_SIZE_PTS = 10
8
+ PTS_PIXELS_RATIO = 4 / 3
9
+ DEFAULT_BUFFER_CHARS = 2
10
+ GREEN_COLOR = "#00FF00"
11
+ RED_COLOR = "#FF0000"
12
+
5
13
6
14
class FILE_OVERRIDE_BEHAVIORS (Enum ):
7
15
OVERRIDE_IF_IN_SAME_PLACE = 1
@@ -12,9 +20,17 @@ class WORKSHEET_OVERRIDE_BEHAVIORS(Enum):
12
20
OVERRIDE = 1
13
21
EXIT = 2
14
22
15
- FONT_SIZE_PTS = 10
16
- PTS_PIXELS_RATIO = 4 / 3
17
- DEFAULT_BUFFER_CHARS = 2
23
+ class COLUMN_FORMAT_OPTIONS (Enum ):
24
+ DEFAULT = 1
25
+ PERCENT_UNCOLORED = 2
26
+ PERCENT_COLORED = 3
27
+
28
+ DEFAULT_SHEET_FORMATTING_OPTIONS = {
29
+ "bold_header" : True ,
30
+ "center_header" : True ,
31
+ "freeze_header" : True ,
32
+ "column_widths" : {"justify" : True , "buffer_chars" : DEFAULT_BUFFER_CHARS }
33
+ }
18
34
19
35
def extract_credentials (authentication_response ):
20
36
"""Extracts the credentials from the tuple from api.authenticate"""
@@ -141,17 +157,14 @@ def create_sheet_in_folder(authentication_response, sheet_name, parent_folder_na
141
157
# Open new file
142
158
return gc .open_by_key (spread_id )
143
159
160
+
144
161
def fill_worksheet_with_df (
145
162
sheet ,
146
163
df ,
147
164
worksheet_name ,
148
165
overlapBehavior ,
149
- options = {
150
- "bold_header" : True ,
151
- "center_header" : True ,
152
- "freeze_header" : True ,
153
- "column_widths" : {"justify" : True , "buffer_chars" : DEFAULT_BUFFER_CHARS }
154
- }
166
+ sheet_formatting_options = DEFAULT_SHEET_FORMATTING_OPTIONS ,
167
+ column_formatting_options = {}
155
168
):
156
169
"""
157
170
Fill a worksheet with the contents of a DataFrame.
@@ -162,8 +175,10 @@ def fill_worksheet_with_df(
162
175
:param df: the DataFrame to fill the worksheet with
163
176
:param worksheet_name: the name of the worksheet to fill. Cannot be "Sheet1"
164
177
:param overlapBehavior: the behavior to take if the worksheet already exists.
165
- :param options : the formatting options for the worksheet.
178
+ :param sheet_formatting_options : the formatting options for the worksheet.
166
179
Should be a dictionary with optional elements "bold_header", "center_header", "freeze_header", and "column_widths", optional
180
+ :param column_formatting_options: the column formatting options for the worksheet.
181
+ Should be a dictionary with dataframe columns as keys and instances of COLUMN_FORMAT_OPTIONS as values, optional
167
182
"""
168
183
# Sheet1 is special since it's created by default, so it's not allowed
169
184
assert worksheet_name != "Sheet1"
@@ -179,19 +194,19 @@ def fill_worksheet_with_df(
179
194
)
180
195
181
196
# Add data to worksheet
182
- worksheet .update ([df .columns .values .tolist ()] + df .values .tolist ())
197
+ worksheet .update ([df .columns .values .tolist ()] + df .fillna ( "NA" ). values .tolist ())
183
198
184
199
# Format worksheet
185
200
# Justify Column Widths
186
- if "column_widths" not in options or options ["column_widths" ]["justify" ]:
201
+ if "column_widths" not in sheet_formatting_options or sheet_formatting_options ["column_widths" ]["justify" ]:
187
202
text_widths = df .astype (str ).columns .map (
188
203
lambda column_name : df [column_name ].astype (str ).str .len ().max ()
189
204
)
190
205
header_widths = df .columns .str .len ()
191
206
buffer_chars = (
192
207
DEFAULT_BUFFER_CHARS
193
- if ("column_widths" not in options or "buffer_chars" not in options ["column_widths" ])
194
- else options ["column_widths" ]["buffer_chars" ]
208
+ if ("column_widths" not in sheet_formatting_options or "buffer_chars" not in sheet_formatting_options ["column_widths" ])
209
+ else sheet_formatting_options ["column_widths" ]["buffer_chars" ]
195
210
)
196
211
column_widths = [
197
212
round ((max (len_tuple ) + buffer_chars ) * FONT_SIZE_PTS * 1 / PTS_PIXELS_RATIO )
@@ -202,26 +217,71 @@ def fill_worksheet_with_df(
202
217
]
203
218
gspread_formatting .set_column_widths (worksheet , zip (column_positions , column_widths ))
204
219
# Freeze Header
205
- if "freeze_header" not in options or options ["freeze_header" ]:
220
+ if "freeze_header" not in sheet_formatting_options or sheet_formatting_options ["freeze_header" ]:
206
221
gspread_formatting .set_frozen (worksheet , rows = 1 )
207
- format_options = gspread_formatting .CellFormat ()
222
+ base_format_options = gspread_formatting .CellFormat ()
208
223
# Bold Header
209
- if "bold_header" not in options or options ["bold_header" ]:
210
- format_options += gspread_formatting .CellFormat (textFormat = gspread_formatting .TextFormat (bold = True ))
224
+ if "bold_header" not in sheet_formatting_options or sheet_formatting_options ["bold_header" ]:
225
+ base_format_options += gspread_formatting .CellFormat (textFormat = gspread_formatting .TextFormat (bold = True ))
211
226
# Center Header
212
- if "center_header" not in options or options ["center_header" ]:
213
- format_options += gspread_formatting .CellFormat (horizontalAlignment = "CENTER" )
227
+ if "center_header" not in sheet_formatting_options or sheet_formatting_options ["center_header" ]:
228
+ base_format_options += gspread_formatting .CellFormat (horizontalAlignment = "CENTER" )
229
+ # Handle column specific formatting
230
+ for column in column_formatting_options :
231
+ if column not in df .columns :
232
+ raise KeyError ("Formatting column is not in the dataframe" )
233
+ # Skip if the column is set to default
234
+ if column_formatting_options [column ] == COLUMN_FORMAT_OPTIONS .DEFAULT :
235
+ continue
236
+ # Get the column position
237
+ column_position_numeric = df .columns .get_loc (column ) + 1
238
+ column_range_top = gspread .utils .rowcol_to_a1 (1 , column_position_numeric )
239
+ column_range_bottom = gspread .utils .rowcol_to_a1 (df .index .size + 1 , column_position_numeric )
240
+ column_range = f"{ column_range_top } :{ column_range_bottom } "
241
+ column_worksheet_range = gspread_formatting .GridRange .from_a1_range (column_range , worksheet )
242
+ # Get conditional formatting rules
243
+ if column_formatting_options [column ] == COLUMN_FORMAT_OPTIONS .PERCENT_COLORED :
244
+ green_rule = gspread_formatting .ConditionalFormatRule (
245
+ ranges = [column_worksheet_range ],
246
+ booleanRule = gspread_formatting .BooleanRule (
247
+ condition = gspread_formatting .BooleanCondition ('NUMBER_GREATER_THAN_EQ' , ['0' ]),
248
+ format = gspread_formatting .CellFormat (
249
+ textFormat = gspread_formatting .TextFormat (foregroundColor = gspread_formatting .Color (0 ,1 ,0 )))
250
+ )
251
+ )
252
+ red_rule = gspread_formatting .ConditionalFormatRule (
253
+ ranges = [column_worksheet_range ],
254
+ booleanRule = gspread_formatting .BooleanRule (
255
+ condition = gspread_formatting .BooleanCondition ('NUMBER_LESS_THAN_EQ' , ['0' ]),
256
+ format = gspread_formatting .CellFormat (
257
+ textFormat = gspread_formatting .TextFormat (foregroundColor = gspread_formatting .Color (1 ,0 ,0 )))
258
+ )
259
+ )
260
+ # Apply conditional formatting rules
261
+ conditional_formatting_rules = gspread_formatting .get_conditional_format_rules (worksheet )
262
+ conditional_formatting_rules .append (green_rule )
263
+ conditional_formatting_rules .append (red_rule )
264
+ conditional_formatting_rules .save ()
265
+ if column_formatting_options [column ] in (COLUMN_FORMAT_OPTIONS .PERCENT_COLORED , COLUMN_FORMAT_OPTIONS .PERCENT_UNCOLORED ):
266
+ # Apply percent format rule
267
+ gspread_formatting .format_cell_range (
268
+ worksheet ,
269
+ column_range ,
270
+ gspread_formatting .CellFormat (numberFormat = gspread_formatting .NumberFormat (type = 'PERCENT' , pattern = '0.0%' ))
271
+ )
272
+
273
+ # Apply base formatting options
214
274
gspread_formatting .format_cell_range (
215
275
worksheet ,
216
276
f"A1:{ gspread .utils .rowcol_to_a1 (1 , len (df .columns ))} " ,
217
- format_options
277
+ base_format_options
218
278
)
219
279
220
280
# Delete Sheet1 if it has been created by default
221
281
if "Sheet1" in [i .title for i in sheet .worksheets ()]:
222
282
sheet .del_worksheet (sheet .worksheet ("Sheet1" ))
223
283
224
- def fill_spreadsheet_with_df_dict (sheet , df_dict , overlapBehavior , options = {}):
284
+ def fill_spreadsheet_with_df_dict (sheet , df_dict , overlapBehavior , sheet_formatting_options = {}, column_formatting_options = {}):
225
285
"""
226
286
Fill a sheet with the contents of a dictionary of DataFrames.
227
287
The keys of the dictionary are the names of the worksheets, and the values contain the data to be placed in the sheet.
@@ -230,8 +290,12 @@ def fill_spreadsheet_with_df_dict(sheet, df_dict, overlapBehavior, options={}):
230
290
:param sheet: the gspread.Spreadsheet object
231
291
:param df_dict: the dictionary of DataFrames to fill the worksheets with
232
292
:param overlapBehavior: the behavior to take if any of the worksheets already exist
233
- :param options: the formatting options for the worksheets.
234
- Should be a dictionary with optional elements "bold_header", "center_header", "freeze_header", and "column_widths", optional
293
+ :param sheet_formatting_options: the formatting options for the worksheets.
294
+ Should be a 2 level dictionary with outer keys being names of worksheets and inner keys being some of
295
+ "bold_header", "center_header", "freeze_header", and "column_widths", optional
296
+ :param column_formatting_options: the column formatting options for the worksheets.
297
+ Should be a 2 level dictionary with outer keys being names of worksheets and inner keys being column names.
298
+ The inner keys should be an instance of COLUMN_FORMATTING_OPTIONS, optional
235
299
"""
236
300
if overlapBehavior == WORKSHEET_OVERRIDE_BEHAVIORS .EXIT :
237
301
for worksheet_name in df_dict .keys ():
@@ -241,5 +305,8 @@ def fill_spreadsheet_with_df_dict(sheet, df_dict, overlapBehavior, options={}):
241
305
except gspread .exceptions .WorksheetNotFound :
242
306
pass
243
307
for worksheet_name , df in df_dict .items ():
244
- fill_worksheet_with_df (sheet , df , worksheet_name , overlapBehavior , options = options )
245
-
308
+ fill_worksheet_with_df (
309
+ sheet , df , worksheet_name , overlapBehavior ,
310
+ sheet_formatting_options = sheet_formatting_options .get (worksheet_name , DEFAULT_SHEET_FORMATTING_OPTIONS ),
311
+ column_formatting_options = column_formatting_options .get (worksheet_name , {})
312
+ )
0 commit comments